From b8b206057c5b464420a6d115a1116ef5cc695bf7 Mon Sep 17 00:00:00 2001 From: awasthi21 <107559116+awasthi21@users.noreply.github.com> Date: Wed, 6 Nov 2024 22:44:03 +0530 Subject: [PATCH 01/25] fix(core): PMD Not Getting Populated for Saved Card Transactions (#6497) --- .../src/connector/paybox/transformers.rs | 11 ++- .../payments/operations/payment_confirm.rs | 70 +++++++++---------- 2 files changed, 45 insertions(+), 36 deletions(-) diff --git a/crates/router/src/connector/paybox/transformers.rs b/crates/router/src/connector/paybox/transformers.rs index 4e1270f419c8..27096c2a63b5 100644 --- a/crates/router/src/connector/paybox/transformers.rs +++ b/crates/router/src/connector/paybox/transformers.rs @@ -1109,7 +1109,16 @@ impl TryFrom<&PayboxRouterData<&types::PaymentsCompleteAuthorizeRouterData>> for |data| Some(data.clone()), ), customer_id: match item.router_data.request.is_mandate_payment() { - true => Some(Secret::new(item.router_data.payment_id.clone())), + true => { + let reference_id = item + .router_data + .connector_mandate_request_reference_id + .clone() + .ok_or_else(|| errors::ConnectorError::MissingRequiredField { + field_name: "connector_mandate_request_reference_id", + })?; + Some(Secret::new(reference_id)) + } false => None, }, }) diff --git a/crates/router/src/core/payments/operations/payment_confirm.rs b/crates/router/src/core/payments/operations/payment_confirm.rs index 1bd3ddce7351..635b3c24cf21 100644 --- a/crates/router/src/core/payments/operations/payment_confirm.rs +++ b/crates/router/src/core/payments/operations/payment_confirm.rs @@ -576,41 +576,6 @@ impl GetTracker, api::PaymentsRequest> for Pa payment_method_info, } = mandate_details; - let additional_pm_data_from_locker = if let Some(ref pm) = payment_method_info { - let card_detail_from_locker: Option = pm - .payment_method_data - .clone() - .map(|x| x.into_inner().expose()) - .and_then(|v| { - v.parse_value("PaymentMethodsData") - .map_err(|err| { - router_env::logger::info!( - "PaymentMethodsData deserialization failed: {:?}", - err - ) - }) - .ok() - }) - .and_then(|pmd| match pmd { - PaymentMethodsData::Card(crd) => Some(api::CardDetailFromLocker::from(crd)), - _ => None, - }); - card_detail_from_locker.map(|card_details| { - let additional_data = card_details.into(); - api_models::payments::AdditionalPaymentData::Card(Box::new(additional_data)) - }) - } else { - None - }; - payment_attempt.payment_method_data = additional_pm_data_from_locker - .as_ref() - .map(Encode::encode_to_value) - .transpose() - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Failed to encode additional pm data")?; - - payment_attempt.payment_method = payment_method.or(payment_attempt.payment_method); - payment_attempt.payment_method_type = payment_method_type .or(payment_attempt.payment_method_type) .or(payment_method_info @@ -650,6 +615,41 @@ impl GetTracker, api::PaymentsRequest> for Pa } else { (None, payment_method_info) }; + let additional_pm_data_from_locker = if let Some(ref pm) = payment_method_info { + let card_detail_from_locker: Option = pm + .payment_method_data + .clone() + .map(|x| x.into_inner().expose()) + .and_then(|v| { + v.parse_value("PaymentMethodsData") + .map_err(|err| { + router_env::logger::info!( + "PaymentMethodsData deserialization failed: {:?}", + err + ) + }) + .ok() + }) + .and_then(|pmd| match pmd { + PaymentMethodsData::Card(crd) => Some(api::CardDetailFromLocker::from(crd)), + _ => None, + }); + card_detail_from_locker.map(|card_details| { + let additional_data = card_details.into(); + api_models::payments::AdditionalPaymentData::Card(Box::new(additional_data)) + }) + } else { + None + }; + payment_attempt.payment_method_data = additional_pm_data_from_locker + .as_ref() + .map(Encode::encode_to_value) + .transpose() + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed to encode additional pm data")?; + + payment_attempt.payment_method = payment_method.or(payment_attempt.payment_method); + // The operation merges mandate data from both request and payment_attempt let setup_mandate = mandate_data.map(|mut sm| { sm.mandate_type = payment_attempt.mandate_details.clone().or(sm.mandate_type); From 90d9ffc1d2e13b83931762b05632056520eea07f Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 7 Nov 2024 00:20:39 +0000 Subject: [PATCH 02/25] chore(version): 2024.11.07.0 --- CHANGELOG.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8d5eb5d0517b..d54f9e4429ec 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,20 @@ All notable changes to HyperSwitch will be documented here. - - - +## 2024.11.07.0 + +### Features + +- **analytics:** Implement currency conversion to power multi-currency aggregation ([#6418](https://github.com/juspay/hyperswitch/pull/6418)) ([`01c5216`](https://github.com/juspay/hyperswitch/commit/01c5216fdd6f1d841082868cccea6054b64e9e07)) + +### Bug Fixes + +- **core:** PMD Not Getting Populated for Saved Card Transactions ([#6497](https://github.com/juspay/hyperswitch/pull/6497)) ([`b8b2060`](https://github.com/juspay/hyperswitch/commit/b8b206057c5b464420a6d115a1116ef5cc695bf7)) + +**Full Changelog:** [`2024.11.06.0...2024.11.07.0`](https://github.com/juspay/hyperswitch/compare/2024.11.06.0...2024.11.07.0) + +- - - + ## 2024.11.06.0 ### Features From 1ba3d84df1e93d2286db1a262c4a67b3861b90c0 Mon Sep 17 00:00:00 2001 From: Maneesh Shukla <143504391+shuklamaneesh23@users.noreply.github.com> Date: Thu, 7 Nov 2024 12:14:29 +0530 Subject: [PATCH 03/25] refactor(connector): Added amount conversion framework to Wise. (#6469) --- crates/router/src/connector/wise.rs | 33 ++++++++++++++--- .../router/src/connector/wise/transformers.rs | 35 +++++++++++++++---- crates/router/src/types/api.rs | 2 +- crates/router/tests/connectors/wise.rs | 2 +- 4 files changed, 58 insertions(+), 14 deletions(-) diff --git a/crates/router/src/connector/wise.rs b/crates/router/src/connector/wise.rs index 8123ae7ec796..9f41651b2f92 100644 --- a/crates/router/src/connector/wise.rs +++ b/crates/router/src/connector/wise.rs @@ -1,8 +1,8 @@ pub mod transformers; -use std::fmt::Debug; #[cfg(feature = "payouts")] use common_utils::request::RequestContent; +use common_utils::types::{AmountConvertor, MinorUnit, MinorUnitForConnector}; use error_stack::{report, ResultExt}; #[cfg(feature = "payouts")] use masking::PeekInterface; @@ -10,6 +10,7 @@ use masking::PeekInterface; use router_env::{instrument, tracing}; use self::transformers as wise; +use super::utils::convert_amount; use crate::{ configs::settings, core::errors::{self, CustomResult}, @@ -27,8 +28,18 @@ use crate::{ utils::BytesExt, }; -#[derive(Debug, Clone)] -pub struct Wise; +#[derive(Clone)] +pub struct Wise { + amount_converter: &'static (dyn AmountConvertor + Sync), +} + +impl Wise { + pub fn new() -> &'static Self { + &Self { + amount_converter: &MinorUnitForConnector, + } + } +} impl ConnectorCommonExt for Wise where @@ -362,7 +373,13 @@ impl services::ConnectorIntegration, _connectors: &settings::Connectors, ) -> CustomResult { - let connector_req = wise::WisePayoutQuoteRequest::try_from(req)?; + let amount = convert_amount( + self.amount_converter, + req.request.minor_amount, + req.request.source_currency, + )?; + let connector_router_data = wise::WiseRouterData::from((amount, req)); + let connector_req = wise::WisePayoutQuoteRequest::try_from(&connector_router_data)?; Ok(RequestContent::Json(Box::new(connector_req))) } @@ -441,7 +458,13 @@ impl req: &types::PayoutsRouterData, _connectors: &settings::Connectors, ) -> CustomResult { - let connector_req = wise::WiseRecipientCreateRequest::try_from(req)?; + let amount = convert_amount( + self.amount_converter, + req.request.minor_amount, + req.request.source_currency, + )?; + let connector_router_data = wise::WiseRouterData::from((amount, req)); + let connector_req = wise::WiseRecipientCreateRequest::try_from(&connector_router_data)?; Ok(RequestContent::Json(Box::new(connector_req))) } diff --git a/crates/router/src/connector/wise/transformers.rs b/crates/router/src/connector/wise/transformers.rs index fe82ffa59547..94849f2b1ca7 100644 --- a/crates/router/src/connector/wise/transformers.rs +++ b/crates/router/src/connector/wise/transformers.rs @@ -2,6 +2,7 @@ use api_models::payouts::PayoutMethodData; #[cfg(feature = "payouts")] use common_utils::pii::Email; +use common_utils::types::MinorUnit; use masking::Secret; use serde::{Deserialize, Serialize}; @@ -16,6 +17,20 @@ use crate::{ }, }; use crate::{core::errors, types}; +#[derive(Debug, Serialize)] +pub struct WiseRouterData { + pub amount: MinorUnit, + pub router_data: T, +} + +impl From<(MinorUnit, T)> for WiseRouterData { + fn from((amount, router_data): (MinorUnit, T)) -> Self { + Self { + amount, + router_data, + } + } +} pub struct WiseAuthType { pub(super) api_key: Secret, @@ -156,8 +171,8 @@ pub struct WiseRecipientCreateResponse { pub struct WisePayoutQuoteRequest { source_currency: String, target_currency: String, - source_amount: Option, - target_amount: Option, + source_amount: Option, + target_amount: Option, pay_out: WisePayOutOption, } @@ -348,9 +363,12 @@ fn get_payout_bank_details( // Payouts recipient create request transform #[cfg(feature = "payouts")] -impl TryFrom<&types::PayoutsRouterData> for WiseRecipientCreateRequest { +impl TryFrom<&WiseRouterData<&types::PayoutsRouterData>> for WiseRecipientCreateRequest { type Error = Error; - fn try_from(item: &types::PayoutsRouterData) -> Result { + fn try_from( + item_data: &WiseRouterData<&types::PayoutsRouterData>, + ) -> Result { + let item = item_data.router_data; let request = item.request.to_owned(); let customer_details = request.customer_details.to_owned(); let payout_method_data = item.get_payout_method_data()?; @@ -420,14 +438,17 @@ impl TryFrom // Payouts quote request transform #[cfg(feature = "payouts")] -impl TryFrom<&types::PayoutsRouterData> for WisePayoutQuoteRequest { +impl TryFrom<&WiseRouterData<&types::PayoutsRouterData>> for WisePayoutQuoteRequest { type Error = Error; - fn try_from(item: &types::PayoutsRouterData) -> Result { + fn try_from( + item_data: &WiseRouterData<&types::PayoutsRouterData>, + ) -> Result { + let item = item_data.router_data; let request = item.request.to_owned(); let payout_type = request.get_payout_type()?; match payout_type { storage_enums::PayoutType::Bank => Ok(Self { - source_amount: Some(request.amount), + source_amount: Some(item_data.amount), source_currency: request.source_currency.to_string(), target_amount: None, target_currency: request.destination_currency.to_string(), diff --git a/crates/router/src/types/api.rs b/crates/router/src/types/api.rs index fbc55dce4445..361fd93d80b0 100644 --- a/crates/router/src/types/api.rs +++ b/crates/router/src/types/api.rs @@ -484,7 +484,7 @@ impl ConnectorData { enums::Connector::Stripe => { Ok(ConnectorEnum::Old(Box::new(connector::Stripe::new()))) } - enums::Connector::Wise => Ok(ConnectorEnum::Old(Box::new(&connector::Wise))), + enums::Connector::Wise => Ok(ConnectorEnum::Old(Box::new(connector::Wise::new()))), enums::Connector::Worldline => { Ok(ConnectorEnum::Old(Box::new(&connector::Worldline))) } diff --git a/crates/router/tests/connectors/wise.rs b/crates/router/tests/connectors/wise.rs index 761678046701..984a43d48a76 100644 --- a/crates/router/tests/connectors/wise.rs +++ b/crates/router/tests/connectors/wise.rs @@ -28,7 +28,7 @@ impl utils::Connector for WiseTest { fn get_payout_data(&self) -> Option { use router::connector::Wise; Some(utils::construct_connector_data_old( - Box::new(&Wise), + Box::new(Wise::new()), types::Connector::Wise, api::GetToken::Connector, None, From b43033c2d9530d291651326cd987476e4924132b Mon Sep 17 00:00:00 2001 From: Mani Chandra <84711804+ThisIsMani@users.noreply.github.com> Date: Thu, 7 Nov 2024 16:07:29 +0530 Subject: [PATCH 04/25] fix(users): Add force rotate password on first login for non-email flow (#6483) --- crates/router/src/types/domain/user.rs | 68 ++++++++++++++++++++------ 1 file changed, 53 insertions(+), 15 deletions(-) diff --git a/crates/router/src/types/domain/user.rs b/crates/router/src/types/domain/user.rs index 8331dff95c1b..c48fe3320f3d 100644 --- a/crates/router/src/types/domain/user.rs +++ b/crates/router/src/types/domain/user.rs @@ -1,4 +1,8 @@ -use std::{collections::HashSet, ops, str::FromStr}; +use std::{ + collections::HashSet, + ops::{Deref, Not}, + str::FromStr, +}; use api_models::{ admin as admin_api, organization as api_org, user as user_api, user_role as user_role_api, @@ -153,7 +157,7 @@ impl TryFrom for UserEmail { } } -impl ops::Deref for UserEmail { +impl Deref for UserEmail { type Target = Secret; fn deref(&self) -> &Self::Target { @@ -565,10 +569,24 @@ pub struct NewUser { user_id: String, name: UserName, email: UserEmail, - password: Option, + password: Option, new_merchant: NewUserMerchant, } +#[derive(Clone)] +pub struct NewUserPassword { + password: UserPassword, + is_temporary: bool, +} + +impl Deref for NewUserPassword { + type Target = UserPassword; + + fn deref(&self) -> &Self::Target { + &self.password + } +} + impl NewUser { pub fn get_user_id(&self) -> String { self.user_id.clone() @@ -587,7 +605,9 @@ impl NewUser { } pub fn get_password(&self) -> Option { - self.password.clone() + self.password + .as_ref() + .map(|password| password.deref().clone()) } pub async fn insert_user_in_db( @@ -697,7 +717,9 @@ impl TryFrom for storage_user::UserNew { totp_status: TotpStatus::NotSet, totp_secret: None, totp_recovery_codes: None, - last_password_modified_at: value.password.is_some().then_some(now), + last_password_modified_at: value + .password + .and_then(|password_inner| password_inner.is_temporary.not().then_some(now)), }) } } @@ -708,7 +730,10 @@ impl TryFrom for NewUser { fn try_from(value: user_api::SignUpWithMerchantIdRequest) -> UserResult { let email = value.email.clone().try_into()?; let name = UserName::new(value.name.clone())?; - let password = UserPassword::new(value.password.clone())?; + let password = NewUserPassword { + password: UserPassword::new(value.password.clone())?, + is_temporary: false, + }; let user_id = uuid::Uuid::new_v4().to_string(); let new_merchant = NewUserMerchant::try_from(value)?; @@ -729,7 +754,10 @@ impl TryFrom for NewUser { 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 password = NewUserPassword { + password: UserPassword::new(value.password.clone())?, + is_temporary: false, + }; let new_merchant = NewUserMerchant::try_from(value)?; Ok(Self { @@ -770,7 +798,10 @@ impl TryFrom<(user_api::CreateInternalUserRequest, id_type::OrganizationId)> for 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 password = NewUserPassword { + password: UserPassword::new(value.password.clone())?, + is_temporary: false, + }; let new_merchant = NewUserMerchant::try_from((value, org_id))?; Ok(Self { @@ -789,16 +820,21 @@ impl TryFrom for NewUser { fn try_from(value: UserMerchantCreateRequestWithToken) -> Result { let user = value.0.clone(); let new_merchant = NewUserMerchant::try_from(value)?; + let password = user + .0 + .password + .map(UserPassword::new_password_without_validation) + .transpose()? + .map(|password| NewUserPassword { + password, + is_temporary: false, + }); Ok(Self { user_id: user.0.user_id, name: UserName::new(user.0.name)?, email: user.0.email.clone().try_into()?, - password: user - .0 - .password - .map(UserPassword::new_password_without_validation) - .transpose()?, + password, new_merchant, }) } @@ -810,8 +846,10 @@ impl TryFrom for NewUser { let user_id = uuid::Uuid::new_v4().to_string(); let email = value.0.email.clone().try_into()?; let name = UserName::new(value.0.name.clone())?; - let password = cfg!(not(feature = "email")) - .then_some(UserPassword::new(password::get_temp_password())?); + let password = cfg!(not(feature = "email")).then_some(NewUserPassword { + password: UserPassword::new(password::get_temp_password())?, + is_temporary: true, + }); let new_merchant = NewUserMerchant::try_from(value)?; Ok(Self { From cf126b940812b8adce26d7a0f957ed92309130d6 Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 7 Nov 2024 10:41:25 +0000 Subject: [PATCH 05/25] chore(version): 2024.11.07.1 --- CHANGELOG.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index d54f9e4429ec..d29420df89d1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,20 @@ All notable changes to HyperSwitch will be documented here. - - - +## 2024.11.07.1 + +### Bug Fixes + +- **users:** Add force rotate password on first login for non-email flow ([#6483](https://github.com/juspay/hyperswitch/pull/6483)) ([`b43033c`](https://github.com/juspay/hyperswitch/commit/b43033c2d9530d291651326cd987476e4924132b)) + +### Refactors + +- **connector:** Added amount conversion framework to Wise. ([#6469](https://github.com/juspay/hyperswitch/pull/6469)) ([`1ba3d84`](https://github.com/juspay/hyperswitch/commit/1ba3d84df1e93d2286db1a262c4a67b3861b90c0)) + +**Full Changelog:** [`2024.11.07.0...2024.11.07.1`](https://github.com/juspay/hyperswitch/compare/2024.11.07.0...2024.11.07.1) + +- - - + ## 2024.11.07.0 ### Features From 063fe904c66c9af3d7ce0a82ad712eac69e41786 Mon Sep 17 00:00:00 2001 From: Rutam Prita Mishra Date: Thu, 7 Nov 2024 18:38:07 +0530 Subject: [PATCH 06/25] feat(payments): Add audit events for PaymentCreate update (#6427) --- .../router/src/core/payments/operations/payment_create.rs | 8 +++++++- crates/router/src/events/audit_events.rs | 2 ++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/crates/router/src/core/payments/operations/payment_create.rs b/crates/router/src/core/payments/operations/payment_create.rs index d4057c523bc6..c65e58ea2d48 100644 --- a/crates/router/src/core/payments/operations/payment_create.rs +++ b/crates/router/src/core/payments/operations/payment_create.rs @@ -42,6 +42,7 @@ use crate::{ utils as core_utils, }, db::StorageInterface, + events::audit_events::{AuditEvent, AuditEventType}, routes::{app::ReqState, SessionState}, services, types::{ @@ -818,7 +819,7 @@ impl UpdateTracker, api::PaymentsRequest> for Paymen async fn update_trackers<'b>( &'b self, state: &'b SessionState, - _req_state: ReqState, + req_state: ReqState, mut payment_data: PaymentData, customer: Option, storage_scheme: enums::MerchantStorageScheme, @@ -923,6 +924,11 @@ impl UpdateTracker, api::PaymentsRequest> for Paymen ) .await .to_not_found_response(errors::ApiErrorResponse::PaymentNotFound)?; + req_state + .event_context + .event(AuditEvent::new(AuditEventType::PaymentCreate)) + .with(payment_data.to_event()) + .emit(); // payment_data.mandate_id = response.and_then(|router_data| router_data.request.mandate_id); Ok(( diff --git a/crates/router/src/events/audit_events.rs b/crates/router/src/events/audit_events.rs index 9b7a688f7eba..54c1934e36f7 100644 --- a/crates/router/src/events/audit_events.rs +++ b/crates/router/src/events/audit_events.rs @@ -27,6 +27,7 @@ pub enum AuditEventType { capture_amount: Option, multiple_capture_count: Option, }, + PaymentCreate, } #[derive(Debug, Clone, Serialize)] @@ -65,6 +66,7 @@ impl Event for AuditEvent { AuditEventType::RefundSuccess => "refund_success", AuditEventType::RefundFail => "refund_fail", AuditEventType::PaymentCancelled { .. } => "payment_cancelled", + AuditEventType::PaymentCreate { .. } => "payment_create", }; format!( "{event_type}-{}", From 01951de7aaa930dd4700b716b7debc9a7f05466e Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 8 Nov 2024 00:20:22 +0000 Subject: [PATCH 07/25] chore(version): 2024.11.08.0 --- CHANGELOG.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index d29420df89d1..21fc88e5e6fb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,16 @@ All notable changes to HyperSwitch will be documented here. - - - +## 2024.11.08.0 + +### Features + +- **payments:** Add audit events for PaymentCreate update ([#6427](https://github.com/juspay/hyperswitch/pull/6427)) ([`063fe90`](https://github.com/juspay/hyperswitch/commit/063fe904c66c9af3d7ce0a82ad712eac69e41786)) + +**Full Changelog:** [`2024.11.07.1...2024.11.08.0`](https://github.com/juspay/hyperswitch/compare/2024.11.07.1...2024.11.08.0) + +- - - + ## 2024.11.07.1 ### Bug Fixes From a5ac69d1a77e772e430df8c4187942de44f23079 Mon Sep 17 00:00:00 2001 From: Hrithikesh <61539176+hrithikesh026@users.noreply.github.com> Date: Fri, 8 Nov 2024 12:48:01 +0530 Subject: [PATCH 08/25] chore: change serde value to strict type in payment intent domain and diesel model (#6393) Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com> --- Cargo.lock | 1 + crates/api_models/src/payments.rs | 19 +- crates/common_enums/Cargo.toml | 1 + crates/common_enums/src/enums.rs | 4 + crates/common_enums/src/enums/payments.rs | 14 + crates/common_utils/src/hashing.rs | 2 +- crates/diesel_models/src/lib.rs | 1 + crates/diesel_models/src/payment_intent.rs | 14 +- crates/diesel_models/src/types.rs | 57 ++ crates/hyperswitch_connectors/src/utils.rs | 3 +- crates/hyperswitch_domain_models/src/lib.rs | 166 +++++ .../hyperswitch_domain_models/src/payments.rs | 29 +- .../src/payments/payment_intent.rs | 80 +-- .../src/router_request_types.rs | 10 +- .../src/router_request_types/fraud_check.rs | 7 +- crates/hyperswitch_domain_models/src/types.rs | 2 + crates/masking/src/strategy.rs | 2 +- crates/openapi/src/openapi.rs | 2 +- crates/openapi/src/openapi_v2.rs | 2 +- crates/router/src/analytics.rs | 25 +- .../src/connector/adyen/transformers.rs | 3 +- .../connector/riskified/transformers/api.rs | 2 +- .../connector/signifyd/transformers/api.rs | 4 +- crates/router/src/connector/utils.rs | 4 +- crates/router/src/core/fraud_check/types.rs | 11 +- .../router/src/core/payments/transformers.rs | 9 +- crates/router/src/core/utils.rs | 7 +- crates/router/src/db/kafka_store.rs | 5 +- crates/router/src/db/merchant_account.rs | 56 +- crates/router/src/routes/admin.rs | 4 +- crates/router/src/routes/app.rs | 30 +- crates/router/src/routes/cards_info.rs | 1 + crates/router/src/routes/currency.rs | 2 + crates/router/src/routes/disputes.rs | 9 + crates/router/src/routes/files.rs | 5 + crates/router/src/routes/payment_methods.rs | 12 +- crates/router/src/routes/payments.rs | 6 +- crates/router/src/routes/payout_link.rs | 1 + crates/router/src/routes/payouts.rs | 8 +- crates/router/src/routes/profiles.rs | 2 + crates/router/src/routes/routing.rs | 89 ++- crates/router/src/routes/verification.rs | 1 + crates/router/src/routes/verify_connector.rs | 1 + crates/router/src/services/authentication.rs | 575 ++++++++++++++---- crates/router/tests/connectors/payme.rs | 3 +- crates/router/tests/connectors/zen.rs | 2 +- 46 files changed, 1030 insertions(+), 263 deletions(-) create mode 100644 crates/common_enums/src/enums/payments.rs create mode 100644 crates/diesel_models/src/types.rs diff --git a/Cargo.lock b/Cargo.lock index d232fcbf024c..0a08788ecfc6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2050,6 +2050,7 @@ name = "common_enums" version = "0.1.0" dependencies = [ "diesel", + "masking", "router_derive", "serde", "serde_json", diff --git a/crates/api_models/src/payments.rs b/crates/api_models/src/payments.rs index 8fd3b5d25dc6..2ace2b956e4a 100644 --- a/crates/api_models/src/payments.rs +++ b/crates/api_models/src/payments.rs @@ -5,6 +5,9 @@ use std::{ }; pub mod additional_info; use cards::CardNumber; +use common_enums::ProductType; +#[cfg(feature = "v2")] +use common_utils::id_type::GlobalPaymentId; use common_utils::{ consts::default_payments_list_limit, crypto, @@ -374,7 +377,7 @@ pub struct PaymentsIntentResponse { /// Use this parameter to restrict the Payment Method Types to show for a given PaymentIntent #[schema(value_type = Option>)] - pub allowed_payment_method_types: Option, + pub allowed_payment_method_types: Option>, /// Metadata is useful for storing additional, unstructured information on an object. #[schema(value_type = Option, example = r#"{ "udf1": "some-value", "udf2": "some-value" }"#)] @@ -386,7 +389,7 @@ pub struct PaymentsIntentResponse { /// Additional data that might be required by hyperswitch based on the requested features by the merchants. #[schema(value_type = Option)] - pub feature_metadata: Option, + pub feature_metadata: Option, /// Whether to generate the payment link for this payment or not (if applicable) #[schema(value_type = EnablePaymentLinkRequest)] @@ -5107,18 +5110,6 @@ pub struct OrderDetailsWithAmount { impl masking::SerializableSecret for OrderDetailsWithAmount {} -#[derive(Debug, Default, Eq, PartialEq, serde::Deserialize, serde::Serialize, Clone, ToSchema)] -#[serde(rename_all = "snake_case")] -pub enum ProductType { - #[default] - Physical, - Digital, - Travel, - Ride, - Event, - Accommodation, -} - #[derive(Debug, Default, Eq, PartialEq, serde::Deserialize, serde::Serialize, Clone, ToSchema)] pub struct OrderDetails { /// Name of the product that is being purchased diff --git a/crates/common_enums/Cargo.toml b/crates/common_enums/Cargo.toml index da03b530eb8c..92fc2f02066b 100644 --- a/crates/common_enums/Cargo.toml +++ b/crates/common_enums/Cargo.toml @@ -22,6 +22,7 @@ utoipa = { version = "4.2.0", features = ["preserve_order", "preserve_path_order # First party crates router_derive = { version = "0.1.0", path = "../router_derive" } +masking = { version = "0.1.0", path = "../masking" } [dev-dependencies] serde_json = "1.0.115" diff --git a/crates/common_enums/src/enums.rs b/crates/common_enums/src/enums.rs index 917030c1e80e..4d2e117a8809 100644 --- a/crates/common_enums/src/enums.rs +++ b/crates/common_enums/src/enums.rs @@ -1,5 +1,7 @@ +mod payments; use std::num::{ParseFloatError, TryFromIntError}; +pub use payments::ProductType; use serde::{Deserialize, Serialize}; use utoipa::ToSchema; @@ -1575,6 +1577,8 @@ pub enum PaymentMethodType { OpenBankingPIS, } +impl masking::SerializableSecret for PaymentMethodType {} + /// Indicates the type of payment method. Eg: 'card', 'wallet', etc. #[derive( Clone, diff --git a/crates/common_enums/src/enums/payments.rs b/crates/common_enums/src/enums/payments.rs new file mode 100644 index 000000000000..895303bab4f5 --- /dev/null +++ b/crates/common_enums/src/enums/payments.rs @@ -0,0 +1,14 @@ +use serde; +use utoipa::ToSchema; + +#[derive(Debug, Default, Eq, PartialEq, serde::Deserialize, serde::Serialize, Clone, ToSchema)] +#[serde(rename_all = "snake_case")] +pub enum ProductType { + #[default] + Physical, + Digital, + Travel, + Ride, + Event, + Accommodation, +} diff --git a/crates/common_utils/src/hashing.rs b/crates/common_utils/src/hashing.rs index d08cd9f0868a..0982ca537881 100644 --- a/crates/common_utils/src/hashing.rs +++ b/crates/common_utils/src/hashing.rs @@ -1,7 +1,7 @@ use masking::{PeekInterface, Secret, Strategy}; use serde::{Deserialize, Serialize, Serializer}; -#[derive(Clone, Debug, Deserialize)] +#[derive(Clone, PartialEq, Debug, Deserialize)] /// Represents a hashed string using blake3's hashing strategy. pub struct HashedString>(Secret); diff --git a/crates/diesel_models/src/lib.rs b/crates/diesel_models/src/lib.rs index 598035524a72..c7a3818d5fea 100644 --- a/crates/diesel_models/src/lib.rs +++ b/crates/diesel_models/src/lib.rs @@ -41,6 +41,7 @@ pub mod refund; pub mod reverse_lookup; pub mod role; pub mod routing_algorithm; +pub mod types; pub mod unified_translations; #[allow(unused_qualifications)] diff --git a/crates/diesel_models/src/payment_intent.rs b/crates/diesel_models/src/payment_intent.rs index 210b12b7aadc..883e23930977 100644 --- a/crates/diesel_models/src/payment_intent.rs +++ b/crates/diesel_models/src/payment_intent.rs @@ -9,6 +9,8 @@ use crate::enums as storage_enums; use crate::schema::payment_intent; #[cfg(feature = "v2")] use crate::schema_v2::payment_intent; +#[cfg(feature = "v2")] +use crate::types::{FeatureMetadata, OrderDetailsWithAmount}; #[cfg(feature = "v2")] #[derive(Clone, Debug, PartialEq, Identifiable, Queryable, Serialize, Deserialize, Selectable)] @@ -32,11 +34,11 @@ pub struct PaymentIntent { pub setup_future_usage: Option, pub client_secret: common_utils::types::ClientSecret, pub active_attempt_id: Option, - #[diesel(deserialize_as = super::OptionalDieselArray)] - pub order_details: Option>, + #[diesel(deserialize_as = super::OptionalDieselArray>)] + pub order_details: Option>>, pub allowed_payment_method_types: Option, pub connector_metadata: Option, - pub feature_metadata: Option, + pub feature_metadata: Option, pub attempt_count: i16, pub profile_id: common_utils::id_type::ProfileId, pub payment_link_id: Option, @@ -249,11 +251,11 @@ pub struct PaymentIntentNew { pub setup_future_usage: Option, pub client_secret: common_utils::types::ClientSecret, pub active_attempt_id: Option, - #[diesel(deserialize_as = super::OptionalDieselArray)] - pub order_details: Option>, + #[diesel(deserialize_as = super::OptionalDieselArray>)] + pub order_details: Option>>, pub allowed_payment_method_types: Option, pub connector_metadata: Option, - pub feature_metadata: Option, + pub feature_metadata: Option, pub attempt_count: i16, pub profile_id: common_utils::id_type::ProfileId, pub payment_link_id: Option, diff --git a/crates/diesel_models/src/types.rs b/crates/diesel_models/src/types.rs new file mode 100644 index 000000000000..94f1fa8d2664 --- /dev/null +++ b/crates/diesel_models/src/types.rs @@ -0,0 +1,57 @@ +use common_utils::{hashing::HashedString, pii, types::MinorUnit}; +use diesel::{ + sql_types::{Json, Jsonb}, + AsExpression, FromSqlRow, +}; +use masking::{Secret, WithType}; +use serde::{Deserialize, Serialize}; +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, FromSqlRow, AsExpression)] +#[diesel(sql_type = Jsonb)] +pub struct OrderDetailsWithAmount { + /// Name of the product that is being purchased + pub product_name: String, + /// The quantity of the product to be purchased + pub quantity: u16, + /// the amount per quantity of product + pub amount: MinorUnit, + // Does the order includes shipping + pub requires_shipping: Option, + /// The image URL of the product + pub product_img_link: Option, + /// ID of the product that is being purchased + pub product_id: Option, + /// Category of the product that is being purchased + pub category: Option, + /// Sub category of the product that is being purchased + pub sub_category: Option, + /// Brand of the product that is being purchased + pub brand: Option, + /// Type of the product that is being purchased + pub product_type: Option, + /// The tax code for the product + pub product_tax_code: Option, +} + +impl masking::SerializableSecret for OrderDetailsWithAmount {} + +common_utils::impl_to_sql_from_sql_json!(OrderDetailsWithAmount); + +#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, FromSqlRow, AsExpression)] +#[diesel(sql_type = Json)] +pub struct FeatureMetadata { + /// Redirection response coming in request as metadata field only for redirection scenarios + pub redirect_response: Option, + // TODO: Convert this to hashedstrings to avoid PII sensitive data + /// Additional tags to be used for global search + pub search_tags: Option>>, +} +impl masking::SerializableSecret for FeatureMetadata {} +common_utils::impl_to_sql_from_sql_json!(FeatureMetadata); + +#[derive(Default, Debug, Eq, PartialEq, Deserialize, Serialize, Clone)] +pub struct RedirectResponse { + pub param: Option>, + pub json_payload: Option, +} +impl masking::SerializableSecret for RedirectResponse {} +common_utils::impl_to_sql_from_sql_json!(RedirectResponse); diff --git a/crates/hyperswitch_connectors/src/utils.rs b/crates/hyperswitch_connectors/src/utils.rs index a0daab47a24e..110917226fe7 100644 --- a/crates/hyperswitch_connectors/src/utils.rs +++ b/crates/hyperswitch_connectors/src/utils.rs @@ -1,6 +1,6 @@ use std::collections::{HashMap, HashSet}; -use api_models::payments::{self, Address, AddressDetails, OrderDetailsWithAmount, PhoneDetails}; +use api_models::payments::{self, Address, AddressDetails, PhoneDetails}; use base64::Engine; use common_enums::{ enums, @@ -26,6 +26,7 @@ use hyperswitch_domain_models::{ PaymentsCaptureData, PaymentsPreProcessingData, PaymentsSyncData, RefundsData, ResponseId, SetupMandateRequestData, }, + types::OrderDetailsWithAmount, }; use hyperswitch_interfaces::{api, consts, errors, types::Response}; use image::Luma; diff --git a/crates/hyperswitch_domain_models/src/lib.rs b/crates/hyperswitch_domain_models/src/lib.rs index af6c8e65c848..02795481ad9c 100644 --- a/crates/hyperswitch_domain_models/src/lib.rs +++ b/crates/hyperswitch_domain_models/src/lib.rs @@ -30,6 +30,12 @@ pub trait PayoutAttemptInterface {} #[cfg(not(feature = "payouts"))] pub trait PayoutsInterface {} +use api_models::payments::{ + FeatureMetadata as ApiFeatureMetadata, OrderDetailsWithAmount as ApiOrderDetailsWithAmount, + RedirectResponse as ApiRedirectResponse, +}; +use diesel_models::types::{FeatureMetadata, OrderDetailsWithAmount, RedirectResponse}; + #[derive(Clone, Debug, Eq, PartialEq, serde::Serialize)] pub enum RemoteStorageObject { ForeignID(String), @@ -60,6 +66,116 @@ use std::fmt::Debug; pub trait ApiModelToDieselModelConvertor { /// Convert from a foreign type to the current type fn convert_from(from: F) -> Self; + fn convert_back(self) -> F; +} + +impl ApiModelToDieselModelConvertor for FeatureMetadata { + fn convert_from(from: ApiFeatureMetadata) -> Self { + let ApiFeatureMetadata { + redirect_response, + search_tags, + } = from; + Self { + redirect_response: redirect_response.map(RedirectResponse::convert_from), + search_tags, + } + } + + fn convert_back(self) -> ApiFeatureMetadata { + let Self { + redirect_response, + search_tags, + } = self; + ApiFeatureMetadata { + redirect_response: redirect_response + .map(|redirect_response| redirect_response.convert_back()), + search_tags, + } + } +} + +impl ApiModelToDieselModelConvertor for RedirectResponse { + fn convert_from(from: ApiRedirectResponse) -> Self { + let ApiRedirectResponse { + param, + json_payload, + } = from; + Self { + param, + json_payload, + } + } + + fn convert_back(self) -> ApiRedirectResponse { + let Self { + param, + json_payload, + } = self; + ApiRedirectResponse { + param, + json_payload, + } + } +} + +impl ApiModelToDieselModelConvertor for OrderDetailsWithAmount { + fn convert_from(from: ApiOrderDetailsWithAmount) -> Self { + let ApiOrderDetailsWithAmount { + product_name, + quantity, + amount, + requires_shipping, + product_img_link, + product_id, + category, + sub_category, + brand, + product_type, + product_tax_code, + } = from; + Self { + product_name, + quantity, + amount, + requires_shipping, + product_img_link, + product_id, + category, + sub_category, + brand, + product_type, + product_tax_code, + } + } + + fn convert_back(self) -> ApiOrderDetailsWithAmount { + let Self { + product_name, + quantity, + amount, + requires_shipping, + product_img_link, + product_id, + category, + sub_category, + brand, + product_type, + product_tax_code, + } = self; + ApiOrderDetailsWithAmount { + product_name, + quantity, + amount, + requires_shipping, + product_img_link, + product_id, + category, + sub_category, + brand, + product_type, + product_tax_code, + } + } } #[cfg(feature = "v2")] @@ -86,6 +202,31 @@ impl ApiModelToDieselModelConvertor }), } } + fn convert_back(self) -> api_models::admin::PaymentLinkConfigRequest { + let Self { + theme, + logo, + seller_name, + sdk_layout, + display_sdk_only, + enabled_saved_payment_method, + transaction_details, + } = self; + api_models::admin::PaymentLinkConfigRequest { + theme, + logo, + seller_name, + sdk_layout, + display_sdk_only, + enabled_saved_payment_method, + transaction_details: transaction_details.map(|transaction_details| { + transaction_details + .into_iter() + .map(|transaction_detail| transaction_detail.convert_back()) + .collect() + }), + } + } } #[cfg(feature = "v2")] @@ -101,6 +242,19 @@ impl ApiModelToDieselModelConvertor api_models::admin::PaymentLinkTransactionDetails { + let Self { + key, + value, + ui_configuration, + } = self; + api_models::admin::PaymentLinkTransactionDetails { + key, + value, + ui_configuration: ui_configuration + .map(|ui_configuration| ui_configuration.convert_back()), + } + } } #[cfg(feature = "v2")] @@ -114,6 +268,18 @@ impl ApiModelToDieselModelConvertor api_models::admin::TransactionDetailsUiConfiguration { + let Self { + position, + is_key_bold, + is_value_bold, + } = self; + api_models::admin::TransactionDetailsUiConfiguration { + position, + is_key_bold, + is_value_bold, + } + } } #[cfg(feature = "v2")] diff --git a/crates/hyperswitch_domain_models/src/payments.rs b/crates/hyperswitch_domain_models/src/payments.rs index cd41458dfbf5..a21788e1bb0a 100644 --- a/crates/hyperswitch_domain_models/src/payments.rs +++ b/crates/hyperswitch_domain_models/src/payments.rs @@ -3,8 +3,6 @@ use std::marker::PhantomData; #[cfg(feature = "v2")] use api_models::payments::Address; -#[cfg(feature = "v2")] -use api_models::payments::OrderDetailsWithAmount; use common_utils::{ self, crypto::Encryptable, @@ -26,6 +24,8 @@ pub mod payment_attempt; pub mod payment_intent; use common_enums as storage_enums; +#[cfg(feature = "v2")] +use diesel_models::types::{FeatureMetadata, OrderDetailsWithAmount}; use self::payment_attempt::PaymentAttempt; use crate::RemoteStorageObject; @@ -278,10 +278,10 @@ pub struct PaymentIntent { pub order_details: Option>>, /// This is the list of payment method types that are allowed for the payment intent. /// This field allows the merchant to restrict the payment methods that can be used for the payment intent. - pub allowed_payment_method_types: Option, + pub allowed_payment_method_types: Option>, /// This metadata contains details about pub connector_metadata: Option, - pub feature_metadata: Option, + pub feature_metadata: Option, /// Number of attempts that have been made for the order pub attempt_count: i16, /// The profile id for the payment. @@ -381,20 +381,14 @@ impl PaymentIntent { billing_address: Option>>, shipping_address: Option>>, ) -> CustomResult { - let allowed_payment_method_types = request - .get_allowed_payment_method_types_as_value() - .change_context(errors::api_error_response::ApiErrorResponse::InternalServerError) - .attach_printable("Error getting allowed payment method types as value")?; let connector_metadata = request .get_connector_metadata_as_value() .change_context(errors::api_error_response::ApiErrorResponse::InternalServerError) .attach_printable("Error getting connector metadata as value")?; - let feature_metadata = request - .get_feature_metadata_as_value() - .change_context(errors::api_error_response::ApiErrorResponse::InternalServerError) - .attach_printable("Error getting feature metadata as value")?; let request_incremental_authorization = Self::get_request_incremental_authorization_value(&request)?; + let allowed_payment_method_types = request.allowed_payment_method_types; + let session_expiry = common_utils::date_time::now().saturating_add(time::Duration::seconds( request.session_expiry.map(i64::from).unwrap_or( @@ -404,9 +398,12 @@ impl PaymentIntent { ), )); let client_secret = payment_id.generate_client_secret(); - let order_details = request - .order_details - .map(|order_details| order_details.into_iter().map(Secret::new).collect()); + let order_details = request.order_details.map(|order_details| { + order_details + .into_iter() + .map(|order_detail| Secret::new(OrderDetailsWithAmount::convert_from(order_detail))) + .collect() + }); Ok(Self { id: payment_id.clone(), merchant_id: merchant_account.get_id().clone(), @@ -428,7 +425,7 @@ impl PaymentIntent { order_details, allowed_payment_method_types, connector_metadata, - feature_metadata, + feature_metadata: request.feature_metadata.map(FeatureMetadata::convert_from), // Attempt count is 0 in create intent as no attempt is made yet attempt_count: 0, profile_id: profile.get_id().clone(), diff --git a/crates/hyperswitch_domain_models/src/payments/payment_intent.rs b/crates/hyperswitch_domain_models/src/payments/payment_intent.rs index c9f7a5e2ae52..3b26ff94e547 100644 --- a/crates/hyperswitch_domain_models/src/payments/payment_intent.rs +++ b/crates/hyperswitch_domain_models/src/payments/payment_intent.rs @@ -14,6 +14,8 @@ use common_utils::{ MinorUnit, }, }; +#[cfg(feature = "v2")] +use diesel_models::types::OrderDetailsWithAmount; use diesel_models::{ PaymentIntent as DieselPaymentIntent, PaymentIntentNew as DieselPaymentIntentNew, }; @@ -33,6 +35,7 @@ use crate::{ type_encryption::{crypto_operation, CryptoOperation}, RemoteStorageObject, }; + #[async_trait::async_trait] pub trait PaymentIntentInterface { async fn update_payment_intent( @@ -1180,18 +1183,22 @@ impl behaviour::Conversion for PaymentIntent { setup_future_usage: Some(setup_future_usage), client_secret, active_attempt_id: active_attempt.map(|attempt| attempt.get_id()), - order_details: order_details - .map(|order_details| { - order_details - .into_iter() - .map(|order_detail| order_detail.encode_to_value().map(Secret::new)) - .collect::, _>>() + order_details: order_details.map(|order_details| { + order_details + .into_iter() + .map(|order_detail| Secret::new(order_detail.expose())) + .collect::>() + }), + allowed_payment_method_types: allowed_payment_method_types + .map(|allowed_payment_method_types| { + allowed_payment_method_types + .encode_to_value() + .change_context(ValidationError::InvalidValue { + message: "Failed to serialize allowed_payment_method_types".to_string(), + }) }) - .transpose() - .change_context(ValidationError::InvalidValue { - message: "invalid value found for order_details".to_string(), - })?, - allowed_payment_method_types, + .transpose()? + .map(Secret::new), connector_metadata, feature_metadata, attempt_count, @@ -1290,7 +1297,13 @@ impl behaviour::Conversion for PaymentIntent { .transpose() .change_context(common_utils::errors::CryptoError::DecodingFailed) .attach_printable("Error while deserializing Address")?; - + let allowed_payment_method_types = storage_model + .allowed_payment_method_types + .map(|allowed_payment_method_types| { + allowed_payment_method_types.parse_value("Vec") + }) + .transpose() + .change_context(common_utils::errors::CryptoError::DecodingFailed)?; Ok::>(Self { merchant_id: storage_model.merchant_id, status: storage_model.status, @@ -1309,19 +1322,13 @@ impl behaviour::Conversion for PaymentIntent { active_attempt: storage_model .active_attempt_id .map(RemoteStorageObject::ForeignID), - order_details: storage_model - .order_details - .map(|order_details| { - order_details - .into_iter() - .map(|order_detail| { - order_detail.expose().parse_value("OrderDetailsWithAmount") - }) - .collect::, _>>() - }) - .transpose() - .change_context(common_utils::errors::CryptoError::DecodingFailed)?, - allowed_payment_method_types: storage_model.allowed_payment_method_types, + order_details: storage_model.order_details.map(|order_details| { + order_details + .into_iter() + .map(|order_detail| Secret::new(order_detail.expose())) + .collect::>() + }), + allowed_payment_method_types, connector_metadata: storage_model.connector_metadata, feature_metadata: storage_model.feature_metadata, attempt_count: storage_model.attempt_count, @@ -1389,19 +1396,18 @@ impl behaviour::Conversion for PaymentIntent { setup_future_usage: Some(self.setup_future_usage), client_secret: self.client_secret, active_attempt_id: self.active_attempt.map(|attempt| attempt.get_id()), - order_details: self - .order_details - .map(|order_details| { - order_details - .into_iter() - .map(|order_detail| order_detail.encode_to_value().map(Secret::new)) - .collect::, _>>() + order_details: self.order_details, + allowed_payment_method_types: self + .allowed_payment_method_types + .map(|allowed_payment_method_types| { + allowed_payment_method_types + .encode_to_value() + .change_context(ValidationError::InvalidValue { + message: "Failed to serialize allowed_payment_method_types".to_string(), + }) }) - .transpose() - .change_context(ValidationError::InvalidValue { - message: "Invalid value found for ".to_string(), - })?, - allowed_payment_method_types: self.allowed_payment_method_types, + .transpose()? + .map(Secret::new), connector_metadata: self.connector_metadata, feature_metadata: self.feature_metadata, attempt_count: self.attempt_count, diff --git a/crates/hyperswitch_domain_models/src/router_request_types.rs b/crates/hyperswitch_domain_models/src/router_request_types.rs index 344eb632f4ba..c645d0b4b532 100644 --- a/crates/hyperswitch_domain_models/src/router_request_types.rs +++ b/crates/hyperswitch_domain_models/src/router_request_types.rs @@ -7,7 +7,7 @@ use common_utils::{ id_type, pii, types::{self as common_types, MinorUnit}, }; -use diesel_models::enums as storage_enums; +use diesel_models::{enums as storage_enums, types::OrderDetailsWithAmount}; use error_stack::ResultExt; use masking::Secret; use serde::Serialize; @@ -49,7 +49,7 @@ pub struct PaymentsAuthorizeData { pub customer_acceptance: Option, pub setup_mandate_details: Option, pub browser_info: Option, - pub order_details: Option>, + pub order_details: Option>, pub order_category: Option, pub session_token: Option, pub enrolled_for_3ds: bool, @@ -288,7 +288,7 @@ pub struct PaymentsPreProcessingData { pub payment_method_type: Option, pub setup_mandate_details: Option, pub capture_method: Option, - pub order_details: Option>, + pub order_details: Option>, pub router_return_url: Option, pub webhook_url: Option, pub complete_authorize_url: Option, @@ -823,7 +823,7 @@ pub struct PaymentsSessionData { pub currency: common_enums::Currency, pub country: Option, pub surcharge_details: Option, - pub order_details: Option>, + pub order_details: Option>, pub email: Option, // Minor Unit amount for amount frame work pub minor_amount: MinorUnit, @@ -834,7 +834,7 @@ pub struct PaymentsTaxCalculationData { pub amount: MinorUnit, pub currency: storage_enums::Currency, pub shipping_cost: Option, - pub order_details: Option>, + pub order_details: Option>, pub shipping_address: Address, } diff --git a/crates/hyperswitch_domain_models/src/router_request_types/fraud_check.rs b/crates/hyperswitch_domain_models/src/router_request_types/fraud_check.rs index 30a87c5c13d2..4d7f4bfdf495 100644 --- a/crates/hyperswitch_domain_models/src/router_request_types/fraud_check.rs +++ b/crates/hyperswitch_domain_models/src/router_request_types/fraud_check.rs @@ -4,6 +4,7 @@ use common_utils::{ events::{ApiEventMetric, ApiEventsType}, pii::Email, }; +use diesel_models::types::OrderDetailsWithAmount; use masking::Secret; use serde::{Deserialize, Serialize}; use utoipa::ToSchema; @@ -12,7 +13,7 @@ use crate::router_request_types; #[derive(Debug, Clone)] pub struct FraudCheckSaleData { pub amount: i64, - pub order_details: Option>, + pub order_details: Option>, pub currency: Option, pub email: Option, } @@ -20,7 +21,7 @@ pub struct FraudCheckSaleData { #[derive(Debug, Clone)] pub struct FraudCheckCheckoutData { pub amount: i64, - pub order_details: Option>, + pub order_details: Option>, pub currency: Option, pub browser_info: Option, pub payment_method_data: Option, @@ -31,7 +32,7 @@ pub struct FraudCheckCheckoutData { #[derive(Debug, Clone)] pub struct FraudCheckTransactionData { pub amount: i64, - pub order_details: Option>, + pub order_details: Option>, pub currency: Option, pub payment_method: Option, pub error_code: Option, diff --git a/crates/hyperswitch_domain_models/src/types.rs b/crates/hyperswitch_domain_models/src/types.rs index a629f8e6392a..201a073dd3e3 100644 --- a/crates/hyperswitch_domain_models/src/types.rs +++ b/crates/hyperswitch_domain_models/src/types.rs @@ -1,3 +1,5 @@ +pub use diesel_models::types::OrderDetailsWithAmount; + use crate::{ router_data::{AccessToken, RouterData}, router_flow_types::{ diff --git a/crates/masking/src/strategy.rs b/crates/masking/src/strategy.rs index eb705ca490a7..b497cc3ed4f4 100644 --- a/crates/masking/src/strategy.rs +++ b/crates/masking/src/strategy.rs @@ -8,7 +8,7 @@ pub trait Strategy { /// Debug with type #[cfg_attr(feature = "serde", derive(serde::Deserialize))] -#[derive(Debug, Copy, Clone)] +#[derive(Debug, Copy, Clone, PartialEq)] pub enum WithType {} impl Strategy for WithType { diff --git a/crates/openapi/src/openapi.rs b/crates/openapi/src/openapi.rs index 75d1bb9f2751..c4a57830ae94 100644 --- a/crates/openapi/src/openapi.rs +++ b/crates/openapi/src/openapi.rs @@ -400,7 +400,7 @@ Never share your secret api keys. Keep them guarded and secure. api_models::payments::ApplePayShippingContactFields, api_models::payments::ApplePayAddressParameters, api_models::payments::AmountInfo, - api_models::payments::ProductType, + api_models::enums::ProductType, api_models::payments::GooglePayWalletData, api_models::payments::PayPalWalletData, api_models::payments::PaypalRedirection, diff --git a/crates/openapi/src/openapi_v2.rs b/crates/openapi/src/openapi_v2.rs index 40f694b80a23..2201e3c49282 100644 --- a/crates/openapi/src/openapi_v2.rs +++ b/crates/openapi/src/openapi_v2.rs @@ -187,6 +187,7 @@ Never share your secret api keys. Keep them guarded and secure. api_models::customers::CustomerResponse, api_models::admin::AcceptedCountries, api_models::admin::AcceptedCurrencies, + api_models::enums::ProductType, api_models::enums::PaymentType, api_models::enums::PaymentMethod, api_models::enums::PaymentMethodType, @@ -339,7 +340,6 @@ Never share your secret api keys. Keep them guarded and secure. api_models::payments::ApplePayShippingContactFields, api_models::payments::ApplePayAddressParameters, api_models::payments::AmountInfo, - api_models::payments::ProductType, api_models::payments::GooglePayWalletData, api_models::payments::PayPalWalletData, api_models::payments::PaypalRedirection, diff --git a/crates/router/src/analytics.rs b/crates/router/src/analytics.rs index aba1c2e2efaa..ed25725358a0 100644 --- a/crates/router/src/analytics.rs +++ b/crates/router/src/analytics.rs @@ -50,6 +50,11 @@ pub mod routes { pub struct Analytics; impl Analytics { + #[cfg(feature = "v2")] + pub fn server(_state: AppState) -> Scope { + todo!() + } + #[cfg(feature = "v1")] pub fn server(state: AppState) -> Scope { web::scope("/analytics") .app_data(web::Data::new(state)) @@ -452,6 +457,7 @@ pub mod routes { .await } + #[cfg(feature = "v1")] /// # Panics /// /// Panics if `json_payload` array does not contain one `GetPaymentMetricRequest` element. @@ -578,6 +584,7 @@ pub mod routes { .await } + #[cfg(feature = "v1")] /// # Panics /// /// Panics if `json_payload` array does not contain one `GetPaymentIntentMetricRequest` element. @@ -702,6 +709,7 @@ pub mod routes { .await } + #[cfg(feature = "v1")] /// # Panics /// /// Panics if `json_payload` array does not contain one `GetRefundMetricRequest` element. @@ -955,6 +963,7 @@ pub mod routes { .await } + #[cfg(feature = "v1")] pub async fn get_profile_payment_filters( state: web::Data, req: actix_web::HttpRequest, @@ -1076,6 +1085,7 @@ pub mod routes { .await } + #[cfg(feature = "v1")] pub async fn get_profile_refund_filters( state: web::Data, req: actix_web::HttpRequest, @@ -1259,6 +1269,7 @@ pub mod routes { .await } + #[cfg(feature = "v1")] pub async fn generate_merchant_refund_report( state: web::Data, req: actix_web::HttpRequest, @@ -1309,6 +1320,7 @@ pub mod routes { .await } + #[cfg(feature = "v1")] pub async fn generate_org_refund_report( state: web::Data, req: actix_web::HttpRequest, @@ -1357,6 +1369,7 @@ pub mod routes { .await } + #[cfg(feature = "v1")] pub async fn generate_profile_refund_report( state: web::Data, req: actix_web::HttpRequest, @@ -1411,7 +1424,7 @@ pub mod routes { )) .await } - + #[cfg(feature = "v1")] pub async fn generate_merchant_dispute_report( state: web::Data, req: actix_web::HttpRequest, @@ -1461,7 +1474,7 @@ pub mod routes { )) .await } - + #[cfg(feature = "v1")] pub async fn generate_org_dispute_report( state: web::Data, req: actix_web::HttpRequest, @@ -1510,6 +1523,7 @@ pub mod routes { .await } + #[cfg(feature = "v1")] pub async fn generate_profile_dispute_report( state: web::Data, req: actix_web::HttpRequest, @@ -1565,6 +1579,7 @@ pub mod routes { .await } + #[cfg(feature = "v1")] pub async fn generate_merchant_payment_report( state: web::Data, req: actix_web::HttpRequest, @@ -1614,7 +1629,7 @@ pub mod routes { )) .await } - + #[cfg(feature = "v1")] pub async fn generate_org_payment_report( state: web::Data, req: actix_web::HttpRequest, @@ -1663,6 +1678,7 @@ pub mod routes { .await } + #[cfg(feature = "v1")] pub async fn generate_profile_payment_report( state: web::Data, req: actix_web::HttpRequest, @@ -2073,6 +2089,7 @@ pub mod routes { .await } + #[cfg(feature = "v1")] pub async fn get_profile_dispute_filters( state: web::Data, req: actix_web::HttpRequest, @@ -2176,6 +2193,7 @@ pub mod routes { .await } + #[cfg(feature = "v1")] /// # Panics /// /// Panics if `json_payload` array does not contain one `GetDisputeMetricRequest` element. @@ -2319,6 +2337,7 @@ pub mod routes { .await } + #[cfg(feature = "v1")] pub async fn get_profile_sankey( state: web::Data, req: actix_web::HttpRequest, diff --git a/crates/router/src/connector/adyen/transformers.rs b/crates/router/src/connector/adyen/transformers.rs index 95ac2a67a533..bba9fc2f8568 100644 --- a/crates/router/src/connector/adyen/transformers.rs +++ b/crates/router/src/connector/adyen/transformers.rs @@ -1763,8 +1763,7 @@ pub fn get_address_info( } fn get_line_items(item: &AdyenRouterData<&types::PaymentsAuthorizeRouterData>) -> Vec { - let order_details: Option> = - item.router_data.request.order_details.clone(); + let order_details = item.router_data.request.order_details.clone(); match order_details { Some(od) => od .iter() diff --git a/crates/router/src/connector/riskified/transformers/api.rs b/crates/router/src/connector/riskified/transformers/api.rs index d2d38855dafc..cfa183e9e833 100644 --- a/crates/router/src/connector/riskified/transformers/api.rs +++ b/crates/router/src/connector/riskified/transformers/api.rs @@ -113,7 +113,7 @@ pub struct LineItem { price: i64, quantity: i32, title: String, - product_type: Option, + product_type: Option, requires_shipping: Option, product_id: Option, category: Option, diff --git a/crates/router/src/connector/signifyd/transformers/api.rs b/crates/router/src/connector/signifyd/transformers/api.rs index eed0e9937b25..348ca3d099a6 100644 --- a/crates/router/src/connector/signifyd/transformers/api.rs +++ b/crates/router/src/connector/signifyd/transformers/api.rs @@ -153,7 +153,7 @@ impl TryFrom<&frm_types::FrmSaleRouterData> for SignifydPaymentsSaleRequest { item_is_digital: order_detail .product_type .as_ref() - .map(|product| (product == &api_models::payments::ProductType::Digital)), + .map(|product| (product == &common_enums::ProductType::Digital)), }) .collect::>(); let metadata: SignifydFrmMetadata = item @@ -390,7 +390,7 @@ impl TryFrom<&frm_types::FrmCheckoutRouterData> for SignifydPaymentsCheckoutRequ item_is_digital: order_detail .product_type .as_ref() - .map(|product| (product == &api_models::payments::ProductType::Digital)), + .map(|product| (product == &common_enums::ProductType::Digital)), }) .collect::>(); let metadata: SignifydFrmMetadata = item diff --git a/crates/router/src/connector/utils.rs b/crates/router/src/connector/utils.rs index 76def480cffe..a9a5a01eef63 100644 --- a/crates/router/src/connector/utils.rs +++ b/crates/router/src/connector/utils.rs @@ -7,7 +7,7 @@ use std::{ use api_models::payouts::{self, PayoutVendorAccountDetails}; use api_models::{ enums::{CanadaStatesAbbreviation, UsStatesAbbreviation}, - payments::{self, OrderDetailsWithAmount}, + payments, }; use base64::Engine; use common_utils::{ @@ -18,7 +18,7 @@ use common_utils::{ pii::{self, Email, IpAddress}, types::{AmountConvertor, MinorUnit}, }; -use diesel_models::enums; +use diesel_models::{enums, types::OrderDetailsWithAmount}; use error_stack::{report, ResultExt}; use hyperswitch_domain_models::{ mandates, diff --git a/crates/router/src/core/fraud_check/types.rs b/crates/router/src/core/fraud_check/types.rs index 3f5988777fd5..2aa486fdb3c7 100644 --- a/crates/router/src/core/fraud_check/types.rs +++ b/crates/router/src/core/fraud_check/types.rs @@ -7,8 +7,11 @@ use api_models::{ use common_enums::FrmSuggestion; use common_utils::pii::SecretSerdeValue; use hyperswitch_domain_models::payments::{payment_attempt::PaymentAttempt, PaymentIntent}; -pub use hyperswitch_domain_models::router_request_types::fraud_check::{ - Address, Destination, FrmFulfillmentRequest, FulfillmentStatus, Fulfillments, Product, +pub use hyperswitch_domain_models::{ + router_request_types::fraud_check::{ + Address, Destination, FrmFulfillmentRequest, FulfillmentStatus, Fulfillments, Product, + }, + types::OrderDetailsWithAmount, }; use masking::Serialize; use serde::Deserialize; @@ -54,7 +57,7 @@ pub struct FrmData { pub fraud_check: FraudCheck, pub address: PaymentAddress, pub connector_details: ConnectorDetailsCore, - pub order_details: Option>, + pub order_details: Option>, pub refund: Option, pub frm_metadata: Option, } @@ -79,7 +82,7 @@ pub struct PaymentToFrmData { pub merchant_account: MerchantAccount, pub address: PaymentAddress, pub connector_details: ConnectorDetailsCore, - pub order_details: Option>, + pub order_details: Option>, pub frm_metadata: Option, } diff --git a/crates/router/src/core/payments/transformers.rs b/crates/router/src/core/payments/transformers.rs index 38b475590fdb..45d0f75ccec8 100644 --- a/crates/router/src/core/payments/transformers.rs +++ b/crates/router/src/core/payments/transformers.rs @@ -18,6 +18,8 @@ use diesel_models::{ use error_stack::{report, ResultExt}; #[cfg(feature = "v2")] use hyperswitch_domain_models::payments::PaymentConfirmData; +#[cfg(feature = "v2")] +use hyperswitch_domain_models::ApiModelToDieselModelConvertor; use hyperswitch_domain_models::{payments::payment_intent::CustomerData, router_request_types}; use masking::{ExposeInterface, Maskable, PeekInterface, Secret}; use router_env::{instrument, metrics::add_attributes, tracing}; @@ -822,13 +824,16 @@ where order_details: payment_intent.order_details.clone().map(|order_details| { order_details .into_iter() - .map(|order_detail| order_detail.expose()) + .map(|order_detail| order_detail.expose().convert_back()) .collect() }), allowed_payment_method_types: payment_intent.allowed_payment_method_types.clone(), metadata: payment_intent.metadata.clone(), connector_metadata: payment_intent.connector_metadata.clone(), - feature_metadata: payment_intent.feature_metadata.clone(), + feature_metadata: payment_intent + .feature_metadata + .clone() + .map(|feature_metadata| feature_metadata.convert_back()), payment_link_enabled: payment_intent.enable_payment_link.clone(), payment_link_config: payment_intent .payment_link_config diff --git a/crates/router/src/core/utils.rs b/crates/router/src/core/utils.rs index abd602760fae..fea112bc3b8b 100644 --- a/crates/router/src/core/utils.rs +++ b/crates/router/src/core/utils.rs @@ -1,11 +1,8 @@ use std::{collections::HashSet, marker::PhantomData, str::FromStr}; +use api_models::enums::{DisputeStage, DisputeStatus}; #[cfg(feature = "payouts")] use api_models::payouts::PayoutVendorAccountDetails; -use api_models::{ - enums::{DisputeStage, DisputeStatus}, - payments::OrderDetailsWithAmount, -}; use common_enums::{IntentStatus, RequestIncrementalAuthorization}; #[cfg(feature = "payouts")] use common_utils::{crypto::Encryptable, pii::Email}; @@ -17,7 +14,7 @@ use common_utils::{ use error_stack::{report, ResultExt}; use hyperswitch_domain_models::{ merchant_connector_account::MerchantConnectorAccount, payment_address::PaymentAddress, - router_data::ErrorResponse, + router_data::ErrorResponse, types::OrderDetailsWithAmount, }; #[cfg(feature = "payouts")] use masking::{ExposeInterface, PeekInterface}; diff --git a/crates/router/src/db/kafka_store.rs b/crates/router/src/db/kafka_store.rs index 8ca0b2937666..cf73bd5b111b 100644 --- a/crates/router/src/db/kafka_store.rs +++ b/crates/router/src/db/kafka_store.rs @@ -73,7 +73,7 @@ use crate::{ unified_translations::UnifiedTranslationsInterface, CommonStorageInterface, GlobalStorageInterface, MasterKeyInterface, StorageInterface, }, - services::{authentication, kafka::KafkaProducer, Store}, + services::{kafka::KafkaProducer, Store}, types::{domain, storage, AccessToken}, }; @@ -1007,7 +1007,8 @@ impl MerchantAccountInterface for KafkaStore { &self, state: &KeyManagerState, publishable_key: &str, - ) -> CustomResult { + ) -> CustomResult<(domain::MerchantAccount, domain::MerchantKeyStore), errors::StorageError> + { self.diesel_store .find_merchant_account_by_publishable_key(state, publishable_key) .await diff --git a/crates/router/src/db/merchant_account.rs b/crates/router/src/db/merchant_account.rs index 398445038922..6eb355e4434f 100644 --- a/crates/router/src/db/merchant_account.rs +++ b/crates/router/src/db/merchant_account.rs @@ -13,7 +13,6 @@ use crate::{ connection, core::errors::{self, CustomResult}, db::merchant_key_store::MerchantKeyStoreInterface, - services::authentication, types::{ domain::{ self, @@ -68,7 +67,7 @@ where &self, state: &KeyManagerState, publishable_key: &str, - ) -> CustomResult; + ) -> CustomResult<(domain::MerchantAccount, domain::MerchantKeyStore), errors::StorageError>; #[cfg(feature = "olap")] async fn list_merchant_accounts_by_organization_id( @@ -229,7 +228,8 @@ impl MerchantAccountInterface for Store { &self, state: &KeyManagerState, publishable_key: &str, - ) -> CustomResult { + ) -> CustomResult<(domain::MerchantAccount, domain::MerchantKeyStore), errors::StorageError> + { let fetch_by_pub_key_func = || async { let conn = connection::pg_connection_read(self).await?; @@ -261,20 +261,15 @@ impl MerchantAccountInterface for Store { &self.get_master_key().to_vec().into(), ) .await?; - - Ok(authentication::AuthenticationData { - merchant_account: merchant_account - .convert( - state, - key_store.key.get_inner(), - key_store.merchant_id.clone().into(), - ) - .await - .change_context(errors::StorageError::DecryptionError)?, - - key_store, - profile_id: None, - }) + let domain_merchant_account = merchant_account + .convert( + state, + key_store.key.get_inner(), + key_store.merchant_id.clone().into(), + ) + .await + .change_context(errors::StorageError::DecryptionError)?; + Ok((domain_merchant_account, key_store)) } #[cfg(feature = "olap")] @@ -578,7 +573,8 @@ impl MerchantAccountInterface for MockDb { &self, state: &KeyManagerState, publishable_key: &str, - ) -> CustomResult { + ) -> CustomResult<(domain::MerchantAccount, domain::MerchantKeyStore), errors::StorageError> + { let accounts = self.merchant_accounts.lock().await; let account = accounts .iter() @@ -599,20 +595,16 @@ impl MerchantAccountInterface for MockDb { &self.get_master_key().to_vec().into(), ) .await?; - Ok(authentication::AuthenticationData { - merchant_account: account - .clone() - .convert( - state, - key_store.key.get_inner(), - key_store.merchant_id.clone().into(), - ) - .await - .change_context(errors::StorageError::DecryptionError)?, - - key_store, - profile_id: None, - }) + let merchant_account = account + .clone() + .convert( + state, + key_store.key.get_inner(), + key_store.merchant_id.clone().into(), + ) + .await + .change_context(errors::StorageError::DecryptionError)?; + Ok((merchant_account, key_store)) } async fn update_all_merchant_account( diff --git a/crates/router/src/routes/admin.rs b/crates/router/src/routes/admin.rs index 0996838b4cfa..64a0630c4013 100644 --- a/crates/router/src/routes/admin.rs +++ b/crates/router/src/routes/admin.rs @@ -357,7 +357,7 @@ pub async fn connector_create( state, &req, payload, - |state, auth_data, req, _| { + |state, auth_data: auth::AuthenticationData, req, _| { create_connector( state, req, @@ -554,6 +554,8 @@ pub async fn connector_list( ) .await } + +#[cfg(all(feature = "v1", feature = "olap"))] /// Merchant Connector - List /// /// List Merchant Connector Details for the merchant diff --git a/crates/router/src/routes/app.rs b/crates/router/src/routes/app.rs index ae099a36e444..5ad8f3864418 100644 --- a/crates/router/src/routes/app.rs +++ b/crates/router/src/routes/app.rs @@ -46,7 +46,7 @@ use super::pm_auth; use super::poll; #[cfg(feature = "olap")] use super::routing; -#[cfg(feature = "olap")] +#[cfg(all(feature = "olap", feature = "v1"))] use super::verification::{apple_pay_merchant_registration, retrieve_apple_pay_verified_domains}; #[cfg(all(feature = "oltp", feature = "v1"))] use super::webhooks::*; @@ -66,6 +66,8 @@ pub use crate::analytics::opensearch::OpenSearchClient; use crate::analytics::AnalyticsProvider; #[cfg(feature = "partial-auth")] use crate::errors::RouterResult; +#[cfg(feature = "v1")] +use crate::routes::cards_info::card_iin_info; #[cfg(all(feature = "frm", feature = "oltp"))] use crate::routes::fraud_check as frm_routes; #[cfg(all(feature = "recon", feature = "olap"))] @@ -74,7 +76,6 @@ pub use crate::{ configs::settings, db::{CommonStorageInterface, GlobalStorageInterface, StorageImpl, StorageInterface}, events::EventsHandler, - routes::cards_info::card_iin_info, services::{get_cache_store, get_store}, }; use crate::{ @@ -670,6 +671,7 @@ pub struct Forex; #[cfg(any(feature = "olap", feature = "oltp"))] impl Forex { + #[cfg(feature = "v1")] pub fn server(state: AppState) -> Scope { web::scope("/forex") .app_data(web::Data::new(state.clone())) @@ -679,6 +681,10 @@ impl Forex { web::resource("/convert_from_minor").route(web::get().to(currency::convert_forex)), ) } + #[cfg(feature = "v2")] + pub fn server(state: AppState) -> Scope { + todo!() + } } #[cfg(feature = "olap")] @@ -1055,6 +1061,11 @@ pub struct Payouts; #[cfg(feature = "payouts")] impl Payouts { + #[cfg(feature = "v2")] + pub fn server(state: AppState) -> Scope { + todo!() + } + #[cfg(feature = "v1")] pub fn server(state: AppState) -> Scope { let mut route = web::scope("/payouts").app_data(web::Data::new(state)); route = route.service(web::resource("/create").route(web::post().to(payouts_create))); @@ -1563,11 +1574,16 @@ impl Disputes { pub struct Cards; impl Cards { + #[cfg(feature = "v1")] pub fn server(state: AppState) -> Scope { web::scope("/cards") .app_data(web::Data::new(state)) .service(web::resource("/{bin}").route(web::get().to(card_iin_info))) } + #[cfg(feature = "v2")] + pub fn server(state: AppState) -> Scope { + todo!() + } } pub struct Files; @@ -1628,6 +1644,7 @@ pub struct PayoutLink; #[cfg(feature = "payouts")] impl PayoutLink { + #[cfg(feature = "v1")] pub fn server(state: AppState) -> Scope { let mut route = web::scope("/payout_link").app_data(web::Data::new(state)); route = route.service( @@ -1635,6 +1652,10 @@ impl PayoutLink { ); route } + #[cfg(feature = "v2")] + pub fn server(state: AppState) -> Scope { + todo!() + } } pub struct Profile; @@ -1750,6 +1771,7 @@ pub struct ProfileNew; #[cfg(feature = "olap")] impl ProfileNew { + #[cfg(feature = "v1")] pub fn server(state: AppState) -> Scope { web::scope("/account/{account_id}/profile") .app_data(web::Data::new(state)) @@ -1760,6 +1782,10 @@ impl ProfileNew { web::resource("/connectors").route(web::get().to(admin::connector_list_profile)), ) } + #[cfg(feature = "v2")] + pub fn server(state: AppState) -> Scope { + todo!() + } } pub struct Gsm; diff --git a/crates/router/src/routes/cards_info.rs b/crates/router/src/routes/cards_info.rs index 889b6e0ec401..1fe2d6db34dc 100644 --- a/crates/router/src/routes/cards_info.rs +++ b/crates/router/src/routes/cards_info.rs @@ -7,6 +7,7 @@ use crate::{ services::{api, authentication as auth}, }; +#[cfg(feature = "v1")] /// Cards Info - Retrieve /// /// Retrieve the card information given the card bin diff --git a/crates/router/src/routes/currency.rs b/crates/router/src/routes/currency.rs index 4d800ddf1a5e..b3969509bf13 100644 --- a/crates/router/src/routes/currency.rs +++ b/crates/router/src/routes/currency.rs @@ -7,6 +7,7 @@ use crate::{ services::{api, authentication as auth}, }; +#[cfg(feature = "v1")] pub async fn retrieve_forex(state: web::Data, req: HttpRequest) -> HttpResponse { let flow = Flow::RetrieveForexFlow; Box::pin(api::server_wrap( @@ -25,6 +26,7 @@ pub async fn retrieve_forex(state: web::Data, req: HttpRequest) -> Htt .await } +#[cfg(feature = "v1")] pub async fn convert_forex( state: web::Data, req: HttpRequest, diff --git a/crates/router/src/routes/disputes.rs b/crates/router/src/routes/disputes.rs index 5577bb96ef01..1935b8cd00ec 100644 --- a/crates/router/src/routes/disputes.rs +++ b/crates/router/src/routes/disputes.rs @@ -13,6 +13,7 @@ use crate::{ types::api::disputes as dispute_types, }; +#[cfg(feature = "v1")] /// Disputes - Retrieve Dispute #[utoipa::path( get, @@ -109,6 +110,7 @@ pub async fn retrieve_disputes_list( .await } +#[cfg(feature = "v1")] /// Disputes - List Disputes for The Given Business Profiles #[utoipa::path( get, @@ -200,6 +202,7 @@ pub async fn get_disputes_filters(state: web::Data, req: HttpRequest) .await } +#[cfg(feature = "v1")] /// Disputes - Disputes Filters Profile #[utoipa::path( get, @@ -241,6 +244,7 @@ pub async fn get_disputes_filters_profile( .await } +#[cfg(feature = "v1")] /// Disputes - Accept Dispute #[utoipa::path( get, @@ -291,6 +295,8 @@ pub async fn accept_dispute( )) .await } + +#[cfg(feature = "v1")] /// Disputes - Submit Dispute Evidence #[utoipa::path( post, @@ -336,6 +342,7 @@ pub async fn submit_dispute_evidence( )) .await } +#[cfg(feature = "v1")] /// Disputes - Attach Evidence to Dispute /// /// To attach an evidence file to dispute @@ -390,6 +397,7 @@ pub async fn attach_dispute_evidence( .await } +#[cfg(feature = "v1")] /// Disputes - Retrieve Dispute #[utoipa::path( get, @@ -506,6 +514,7 @@ pub async fn get_disputes_aggregate( .await } +#[cfg(feature = "v1")] #[instrument(skip_all, fields(flow = ?Flow::DisputesAggregate))] pub async fn get_disputes_aggregate_profile( state: web::Data, diff --git a/crates/router/src/routes/files.rs b/crates/router/src/routes/files.rs index 2ebb1176aeaa..e68a7a63b338 100644 --- a/crates/router/src/routes/files.rs +++ b/crates/router/src/routes/files.rs @@ -12,6 +12,7 @@ use crate::{ types::api::files, }; +#[cfg(feature = "v1")] /// Files - Create /// /// To create a file @@ -56,6 +57,8 @@ pub async fn files_create( )) .await } + +#[cfg(feature = "v1")] /// Files - Delete /// /// To delete a file @@ -100,6 +103,8 @@ pub async fn files_delete( )) .await } + +#[cfg(feature = "v1")] /// Files - Retrieve /// /// To retrieve a file diff --git a/crates/router/src/routes/payment_methods.rs b/crates/router/src/routes/payment_methods.rs index 3b47a06cf7c6..b92e2f260fcb 100644 --- a/crates/router/src/routes/payment_methods.rs +++ b/crates/router/src/routes/payment_methods.rs @@ -86,7 +86,7 @@ pub async fn create_payment_method_api( state, &req, json_payload.into_inner(), - |state, auth: auth::AuthenticationDataV2, req, _| async move { + |state, auth: auth::AuthenticationData, req, _| async move { Box::pin(create_payment_method( &state, req, @@ -115,7 +115,7 @@ pub async fn create_payment_method_intent_api( state, &req, json_payload.into_inner(), - |state, auth: auth::AuthenticationDataV2, req, _| async move { + |state, auth: auth::AuthenticationData, req, _| async move { Box::pin(payment_method_intent_create( &state, req, @@ -197,7 +197,7 @@ pub async fn payment_method_update_api( state, &req, payload, - |state, auth: auth::AuthenticationDataV2, req, _| { + |state, auth: auth::AuthenticationData, req, _| { update_payment_method( state, auth.merchant_account, @@ -230,7 +230,7 @@ pub async fn payment_method_retrieve_api( state, &req, payload, - |state, auth: auth::AuthenticationDataV2, pm, _| { + |state, auth: auth::AuthenticationData, pm, _| { retrieve_payment_method(state, pm, auth.key_store, auth.merchant_account) }, &auth::HeaderAuth(auth::ApiKeyAuth), @@ -257,7 +257,7 @@ pub async fn payment_method_delete_api( state, &req, payload, - |state, auth: auth::AuthenticationDataV2, pm, _| { + |state, auth: auth::AuthenticationData, pm, _| { delete_payment_method(state, pm, auth.key_store, auth.merchant_account) }, &auth::HeaderAuth(auth::ApiKeyAuth), @@ -726,6 +726,7 @@ pub async fn initiate_pm_collect_link_flow( .await } +#[cfg(all(any(feature = "v1", feature = "v2"), not(feature = "customer_v2")))] /// Generate a form link for collecting payment methods for a customer #[instrument(skip_all, fields(flow = ?Flow::PaymentMethodCollectLink))] pub async fn render_pm_collect_link( @@ -861,6 +862,7 @@ pub async fn payment_method_delete_api( .await } +#[cfg(feature = "v1")] #[instrument(skip_all, fields(flow = ?Flow::ListCountriesCurrencies))] pub async fn list_countries_currencies_for_connector_payment_method( state: web::Data, diff --git a/crates/router/src/routes/payments.rs b/crates/router/src/routes/payments.rs index 62cb4a0b79b1..ba785fbee8e6 100644 --- a/crates/router/src/routes/payments.rs +++ b/crates/router/src/routes/payments.rs @@ -123,7 +123,7 @@ pub async fn payments_create_intent( state, &req, json_payload.into_inner(), - |state, auth: auth::AuthenticationDataV2, req, req_state| { + |state, auth: auth::AuthenticationData, req, req_state| { payments::payments_intent_core::< api_types::PaymentCreateIntent, payment_types::PaymentsIntentResponse, @@ -183,7 +183,7 @@ pub async fn payments_get_intent( state, &req, payload, - |state, auth: auth::AuthenticationDataV2, req, req_state| { + |state, auth: auth::AuthenticationData, req, req_state| { payments::payments_intent_core::< api_types::PaymentGetIntent, payment_types::PaymentsIntentResponse, @@ -2103,7 +2103,7 @@ pub async fn payment_confirm_intent( state, &req, internal_payload, - |state, auth: auth::AuthenticationDataV2, req, req_state| async { + |state, auth: auth::AuthenticationData, req, req_state| async { let payment_id = req.global_payment_id; let request = req.payload; diff --git a/crates/router/src/routes/payout_link.rs b/crates/router/src/routes/payout_link.rs index 87157435721c..25528b21ed85 100644 --- a/crates/router/src/routes/payout_link.rs +++ b/crates/router/src/routes/payout_link.rs @@ -12,6 +12,7 @@ use crate::{ }, AppState, }; +#[cfg(feature = "v1")] pub async fn render_payout_link( state: web::Data, req: actix_web::HttpRequest, diff --git a/crates/router/src/routes/payouts.rs b/crates/router/src/routes/payouts.rs index 62d16c4c4d56..2329a48ef2bc 100644 --- a/crates/router/src/routes/payouts.rs +++ b/crates/router/src/routes/payouts.rs @@ -49,6 +49,8 @@ pub async fn payouts_create( )) .await } + +#[cfg(all(feature = "v1", feature = "payouts"))] /// Payouts - Retrieve #[instrument(skip_all, fields(flow = ?Flow::PayoutsRetrieve))] pub async fn payouts_retrieve( @@ -245,7 +247,7 @@ pub async fn payouts_list( } /// Payouts - List Profile -#[cfg(feature = "olap")] +#[cfg(all(feature = "olap", feature = "payouts", feature = "v1"))] #[instrument(skip_all, fields(flow = ?Flow::PayoutsList))] pub async fn payouts_list_profile( state: web::Data, @@ -323,7 +325,7 @@ pub async fn payouts_list_by_filter( } /// Payouts - Filtered list -#[cfg(feature = "olap")] +#[cfg(all(feature = "olap", feature = "payouts", feature = "v1"))] #[instrument(skip_all, fields(flow = ?Flow::PayoutsList))] pub async fn payouts_list_by_filter_profile( state: web::Data, @@ -394,7 +396,7 @@ pub async fn payouts_list_available_filters_for_merchant( } /// Payouts - Available filters for Profile -#[cfg(feature = "olap")] +#[cfg(all(feature = "olap", feature = "payouts", feature = "v1"))] #[instrument(skip_all, fields(flow = ?Flow::PayoutsFilter))] pub async fn payouts_list_available_filters_for_profile( state: web::Data, diff --git a/crates/router/src/routes/profiles.rs b/crates/router/src/routes/profiles.rs index cbb7957987fc..6c2e5407cff1 100644 --- a/crates/router/src/routes/profiles.rs +++ b/crates/router/src/routes/profiles.rs @@ -245,6 +245,7 @@ pub async fn profiles_list( .await } +#[cfg(all(feature = "olap", feature = "v1"))] #[instrument(skip_all, fields(flow = ?Flow::ProfileList))] pub async fn profiles_list_at_profile_level( state: web::Data, @@ -337,6 +338,7 @@ pub async fn toggle_extended_card_info( .await } +#[cfg(feature = "v1")] #[instrument(skip_all, fields(flow = ?Flow::MerchantConnectorsList))] pub async fn payment_connector_list_profile( state: web::Data, diff --git a/crates/router/src/routes/routing.rs b/crates/router/src/routes/routing.rs index f3a589524a16..29c759425566 100644 --- a/crates/router/src/routes/routing.rs +++ b/crates/router/src/routes/routing.rs @@ -15,7 +15,7 @@ use crate::{ routes::AppState, services::{api as oss_api, authentication as auth, authorization::permissions::Permission}, }; -#[cfg(feature = "olap")] +#[cfg(all(feature = "olap", feature = "v1"))] #[instrument(skip_all)] pub async fn routing_create_config( state: web::Data, @@ -56,6 +56,48 @@ pub async fn routing_create_config( .await } +#[cfg(all(feature = "olap", feature = "v2"))] +#[instrument(skip_all)] +pub async fn routing_create_config( + state: web::Data, + req: HttpRequest, + json_payload: web::Json, + transaction_type: &enums::TransactionType, +) -> impl Responder { + let flow = Flow::RoutingCreateConfig; + Box::pin(oss_api::server_wrap( + flow, + state, + &req, + json_payload.into_inner(), + |state, auth: auth::AuthenticationData, payload, _| { + routing::create_routing_algorithm_under_profile( + state, + auth.merchant_account, + auth.key_store, + Some(auth.profile.get_id().clone()), + payload, + transaction_type, + ) + }, + #[cfg(not(feature = "release"))] + auth::auth_type( + &auth::HeaderAuth(auth::ApiKeyAuth), + &auth::JWTAuth { + permission: Permission::RoutingWrite, + minimum_entity_level: EntityType::Profile, + }, + req.headers(), + ), + #[cfg(feature = "release")] + &auth::JWTAuth { + permission: Permission::ProfileRoutingWrite, + }, + api_locking::LockAction::NotApplicable, + )) + .await +} + #[cfg(all(feature = "olap", feature = "v1"))] #[instrument(skip_all)] pub async fn routing_link_config( @@ -146,7 +188,7 @@ pub async fn routing_link_config( .await } -#[cfg(feature = "olap")] +#[cfg(all(feature = "olap", feature = "v1"))] #[instrument(skip_all)] pub async fn routing_retrieve_config( state: web::Data, @@ -186,6 +228,47 @@ pub async fn routing_retrieve_config( .await } +#[cfg(all(feature = "olap", feature = "v2"))] +#[instrument(skip_all)] +pub async fn routing_retrieve_config( + state: web::Data, + req: HttpRequest, + path: web::Path, +) -> impl Responder { + let algorithm_id = path.into_inner(); + let flow = Flow::RoutingRetrieveConfig; + Box::pin(oss_api::server_wrap( + flow, + state, + &req, + algorithm_id, + |state, auth: auth::AuthenticationData, algorithm_id, _| { + routing::retrieve_routing_algorithm_from_algorithm_id( + state, + auth.merchant_account, + auth.key_store, + Some(auth.profile.get_id().clone()), + algorithm_id, + ) + }, + #[cfg(not(feature = "release"))] + auth::auth_type( + &auth::HeaderAuth(auth::ApiKeyAuth), + &auth::JWTAuth { + permission: Permission::RoutingRead, + minimum_entity_level: EntityType::Profile, + }, + req.headers(), + ), + #[cfg(feature = "release")] + &auth::JWTAuth { + permission: Permission::ProfileRoutingRead, + }, + api_locking::LockAction::NotApplicable, + )) + .await +} + #[cfg(feature = "olap")] #[instrument(skip_all)] pub async fn list_routing_configs( @@ -226,7 +309,7 @@ pub async fn list_routing_configs( .await } -#[cfg(feature = "olap")] +#[cfg(all(feature = "olap", feature = "v1"))] #[instrument(skip_all)] pub async fn list_routing_configs_for_profile( state: web::Data, diff --git a/crates/router/src/routes/verification.rs b/crates/router/src/routes/verification.rs index 56ad42947c24..d89f3c65cbe6 100644 --- a/crates/router/src/routes/verification.rs +++ b/crates/router/src/routes/verification.rs @@ -8,6 +8,7 @@ use crate::{ services::{api, authentication as auth, authorization::permissions::Permission}, }; +#[cfg(all(feature = "olap", feature = "v1"))] #[instrument(skip_all, fields(flow = ?Flow::Verification))] pub async fn apple_pay_merchant_registration( state: web::Data, diff --git a/crates/router/src/routes/verify_connector.rs b/crates/router/src/routes/verify_connector.rs index b8e089f0660e..76e10652a313 100644 --- a/crates/router/src/routes/verify_connector.rs +++ b/crates/router/src/routes/verify_connector.rs @@ -8,6 +8,7 @@ use crate::{ services::{self, authentication as auth, authorization::permissions::Permission}, }; +#[cfg(feature = "v1")] #[instrument(skip_all, fields(flow = ?Flow::VerifyPaymentConnector))] pub async fn payment_connector_verify( state: web::Data, diff --git a/crates/router/src/services/authentication.rs b/crates/router/src/services/authentication.rs index a3cc54de34f9..cbd694f3acdb 100644 --- a/crates/router/src/services/authentication.rs +++ b/crates/router/src/services/authentication.rs @@ -19,8 +19,10 @@ use router_env::logger; use serde::Serialize; use self::blacklist::BlackList; +#[cfg(all(feature = "partial-auth", feature = "v1"))] +use self::detached::ExtractedPayload; #[cfg(feature = "partial-auth")] -use self::detached::{ExtractedPayload, GetAuthType}; +use self::detached::GetAuthType; use super::authorization::{self, permissions::Permission}; #[cfg(feature = "olap")] use super::jwt; @@ -30,7 +32,7 @@ use crate::configs::Settings; use crate::consts; #[cfg(feature = "olap")] use crate::core::errors::UserResult; -#[cfg(feature = "partial-auth")] +#[cfg(all(feature = "partial-auth", feature = "v1"))] use crate::core::metrics; use crate::{ core::{ @@ -51,6 +53,7 @@ pub mod decision; #[cfg(feature = "partial-auth")] mod detached; +#[cfg(feature = "v1")] #[derive(Clone, Debug)] pub struct AuthenticationData { pub merchant_account: domain::MerchantAccount, @@ -60,7 +63,7 @@ pub struct AuthenticationData { #[cfg(feature = "v2")] #[derive(Clone, Debug)] -pub struct AuthenticationDataV2 { +pub struct AuthenticationData { pub merchant_account: domain::MerchantAccount, pub key_store: domain::MerchantKeyStore, pub profile: domain::Profile, @@ -277,6 +280,7 @@ impl AuthInfo for () { } } +#[cfg(feature = "v1")] impl AuthInfo for AuthenticationData { fn get_merchant_id(&self) -> Option<&id_type::MerchantId> { Some(self.merchant_account.get_id()) @@ -284,7 +288,7 @@ impl AuthInfo for AuthenticationData { } #[cfg(feature = "v2")] -impl AuthInfo for AuthenticationDataV2 { +impl AuthInfo for AuthenticationData { fn get_merchant_id(&self) -> Option<&id_type::MerchantId> { Some(self.merchant_account.get_id()) } @@ -369,7 +373,7 @@ where #[cfg(feature = "v2")] #[async_trait] -impl AuthenticateAndFetch for ApiKeyAuth +impl AuthenticateAndFetch for ApiKeyAuth where A: SessionStateInfo + Sync, { @@ -377,7 +381,7 @@ where &self, request_headers: &HeaderMap, state: &A, - ) -> RouterResult<(AuthenticationDataV2, AuthenticationType)> { + ) -> RouterResult<(AuthenticationData, AuthenticationType)> { let api_key = get_api_key(request_headers) .change_context(errors::ApiErrorResponse::Unauthorized)? .trim(); @@ -429,7 +433,12 @@ where let profile = state .store() - .find_business_profile_by_profile_id(key_manager_state, &key_store, &profile_id) + .find_business_profile_by_merchant_id_profile_id( + key_manager_state, + &key_store, + &stored_api_key.merchant_id, + &profile_id, + ) .await .to_not_found_response(errors::ApiErrorResponse::Unauthorized)?; @@ -443,7 +452,7 @@ where .await .to_not_found_response(errors::ApiErrorResponse::Unauthorized)?; - let auth = AuthenticationDataV2 { + let auth = AuthenticationData { merchant_account: merchant, key_store, profile, @@ -458,6 +467,7 @@ where } } +#[cfg(feature = "v1")] #[async_trait] impl AuthenticateAndFetch for ApiKeyAuth where @@ -555,7 +565,7 @@ where } } -#[cfg(feature = "partial-auth")] +#[cfg(all(feature = "partial-auth", feature = "v1"))] #[async_trait] impl AuthenticateAndFetch for HeaderAuth where @@ -642,10 +652,10 @@ where #[cfg(all(feature = "partial-auth", feature = "v2"))] #[async_trait] -impl AuthenticateAndFetch for HeaderAuth +impl AuthenticateAndFetch for HeaderAuth where A: SessionStateInfo + Sync, - I: AuthenticateAndFetch + I: AuthenticateAndFetch + AuthenticateAndFetch + GetAuthType + Sync @@ -655,7 +665,7 @@ where &self, request_headers: &HeaderMap, state: &A, - ) -> RouterResult<(AuthenticationDataV2, AuthenticationType)> { + ) -> RouterResult<(AuthenticationData, AuthenticationType)> { let (auth_data, auth_type): (AuthenticationData, AuthenticationType) = self .0 .authenticate_and_fetch(request_headers, state) @@ -667,14 +677,15 @@ where let key_manager_state = &(&state.session_state()).into(); let profile = state .store() - .find_business_profile_by_profile_id( + .find_business_profile_by_merchant_id_profile_id( key_manager_state, &auth_data.key_store, + auth_data.merchant_account.get_id(), &profile_id, ) .await .to_not_found_response(errors::ApiErrorResponse::Unauthorized)?; - let auth_data_v2 = AuthenticationDataV2 { + let auth_data_v2 = AuthenticationData { merchant_account: auth_data.merchant_account, key_store: auth_data.key_store, profile, @@ -683,7 +694,7 @@ where } } -#[cfg(feature = "partial-auth")] +#[cfg(all(feature = "partial-auth", feature = "v1"))] async fn construct_authentication_data( state: &A, merchant_id: &id_type::MerchantId, @@ -870,6 +881,7 @@ where #[derive(Debug)] pub struct AdminApiAuthWithMerchantIdFromRoute(pub id_type::MerchantId); +#[cfg(feature = "v1")] #[async_trait] impl AuthenticateAndFetch for AdminApiAuthWithMerchantIdFromRoute where @@ -895,27 +907,13 @@ where &state.store().get_master_key().to_vec().into(), ) .await - .map_err(|e| { - if e.current_context().is_db_not_found() { - e.change_context(errors::ApiErrorResponse::Unauthorized) - } else { - e.change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Failed to fetch merchant key store for the merchant id") - } - })?; + .to_not_found_response(errors::ApiErrorResponse::Unauthorized)?; let merchant = state .store() .find_merchant_account_by_merchant_id(key_manager_state, &merchant_id, &key_store) .await - .map_err(|e| { - if e.current_context().is_db_not_found() { - e.change_context(errors::ApiErrorResponse::Unauthorized) - } else { - e.change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Failed to fetch merchant account for the merchant id") - } - })?; + .to_not_found_response(errors::ApiErrorResponse::Unauthorized)?; let auth = AuthenticationData { merchant_account: merchant, @@ -930,6 +928,64 @@ where } } +#[cfg(feature = "v2")] +#[async_trait] +impl AuthenticateAndFetch for AdminApiAuthWithMerchantIdFromRoute +where + A: SessionStateInfo + Sync, +{ + async fn authenticate_and_fetch( + &self, + request_headers: &HeaderMap, + state: &A, + ) -> RouterResult<(AuthenticationData, AuthenticationType)> { + AdminApiAuth + .authenticate_and_fetch(request_headers, state) + .await?; + + let merchant_id = self.0.clone(); + let profile_id = + get_id_type_by_key_from_headers(headers::X_PROFILE_ID.to_string(), request_headers)? + .get_required_value(headers::X_PROFILE_ID)?; + let key_manager_state = &(&state.session_state()).into(); + let key_store = state + .store() + .get_merchant_key_store_by_merchant_id( + key_manager_state, + &merchant_id, + &state.store().get_master_key().to_vec().into(), + ) + .await + .to_not_found_response(errors::ApiErrorResponse::Unauthorized)?; + let profile = state + .store() + .find_business_profile_by_merchant_id_profile_id( + key_manager_state, + &key_store, + &merchant_id, + &profile_id, + ) + .await + .to_not_found_response(errors::ApiErrorResponse::Unauthorized)?; + let merchant = state + .store() + .find_merchant_account_by_merchant_id(key_manager_state, &merchant_id, &key_store) + .await + .to_not_found_response(errors::ApiErrorResponse::Unauthorized)?; + + let auth = AuthenticationData { + merchant_account: merchant, + key_store, + profile, + }; + + Ok(( + auth, + AuthenticationType::AdminApiAuthWithMerchantId { merchant_id }, + )) + } +} + /// A helper struct to extract headers from the request pub(crate) struct HeaderMapStruct<'a> { headers: &'a HeaderMap, @@ -999,6 +1055,7 @@ impl<'a> HeaderMapStruct<'a> { #[derive(Debug)] pub struct AdminApiAuthWithMerchantIdFromHeader; +#[cfg(feature = "v1")] #[async_trait] impl AuthenticateAndFetch for AdminApiAuthWithMerchantIdFromHeader where @@ -1025,27 +1082,13 @@ where &state.store().get_master_key().to_vec().into(), ) .await - .map_err(|e| { - if e.current_context().is_db_not_found() { - e.change_context(errors::ApiErrorResponse::MerchantAccountNotFound) - } else { - e.change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Failed to fetch merchant key store for the merchant id") - } - })?; + .to_not_found_response(errors::ApiErrorResponse::MerchantAccountNotFound)?; let merchant = state .store() .find_merchant_account_by_merchant_id(key_manager_state, &merchant_id, &key_store) .await - .map_err(|e| { - if e.current_context().is_db_not_found() { - e.change_context(errors::ApiErrorResponse::Unauthorized) - } else { - e.change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Failed to fetch merchant account for the merchant id") - } - })?; + .to_not_found_response(errors::ApiErrorResponse::Unauthorized)?; let auth = AuthenticationData { merchant_account: merchant, @@ -1059,9 +1102,69 @@ where } } +#[cfg(feature = "v2")] +#[async_trait] +impl AuthenticateAndFetch for AdminApiAuthWithMerchantIdFromHeader +where + A: SessionStateInfo + Sync, +{ + async fn authenticate_and_fetch( + &self, + request_headers: &HeaderMap, + state: &A, + ) -> RouterResult<(AuthenticationData, AuthenticationType)> { + AdminApiAuth + .authenticate_and_fetch(request_headers, state) + .await?; + + let merchant_id = HeaderMapStruct::new(request_headers) + .get_id_type_from_header::(headers::X_MERCHANT_ID)?; + let profile_id = + get_id_type_by_key_from_headers(headers::X_PROFILE_ID.to_string(), request_headers)? + .get_required_value(headers::X_PROFILE_ID)?; + + let key_manager_state = &(&state.session_state()).into(); + let key_store = state + .store() + .get_merchant_key_store_by_merchant_id( + key_manager_state, + &merchant_id, + &state.store().get_master_key().to_vec().into(), + ) + .await + .to_not_found_response(errors::ApiErrorResponse::MerchantAccountNotFound)?; + let profile = state + .store() + .find_business_profile_by_merchant_id_profile_id( + key_manager_state, + &key_store, + &merchant_id, + &profile_id, + ) + .await + .to_not_found_response(errors::ApiErrorResponse::Unauthorized)?; + let merchant = state + .store() + .find_merchant_account_by_merchant_id(key_manager_state, &merchant_id, &key_store) + .await + .to_not_found_response(errors::ApiErrorResponse::Unauthorized)?; + + let auth = AuthenticationData { + merchant_account: merchant, + key_store, + profile, + }; + Ok(( + auth, + AuthenticationType::AdminApiAuthWithMerchantId { merchant_id }, + )) + } +} + #[derive(Debug)] pub struct EphemeralKeyAuth; +// #[cfg(feature = "v1")] #[async_trait] impl AuthenticateAndFetch for EphemeralKeyAuth where @@ -1088,6 +1191,7 @@ where #[derive(Debug)] pub struct MerchantIdAuth(pub id_type::MerchantId); +#[cfg(feature = "v1")] #[async_trait] impl AuthenticateAndFetch for MerchantIdAuth where @@ -1107,26 +1211,13 @@ where &state.store().get_master_key().to_vec().into(), ) .await - .map_err(|e| { - if e.current_context().is_db_not_found() { - e.change_context(errors::ApiErrorResponse::Unauthorized) - } else { - e.change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Failed to fetch merchant key store for the merchant id") - } - })?; + .to_not_found_response(errors::ApiErrorResponse::Unauthorized)?; let merchant = state .store() .find_merchant_account_by_merchant_id(key_manager_state, &self.0, &key_store) .await - .map_err(|e| { - if e.current_context().is_db_not_found() { - e.change_context(errors::ApiErrorResponse::Unauthorized) - } else { - e.change_context(errors::ApiErrorResponse::InternalServerError) - } - })?; + .to_not_found_response(errors::ApiErrorResponse::Unauthorized)?; let auth = AuthenticationData { merchant_account: merchant, @@ -1142,6 +1233,61 @@ where } } +#[cfg(feature = "v2")] +#[async_trait] +impl AuthenticateAndFetch for MerchantIdAuth +where + A: SessionStateInfo + Sync, +{ + async fn authenticate_and_fetch( + &self, + request_headers: &HeaderMap, + state: &A, + ) -> RouterResult<(AuthenticationData, AuthenticationType)> { + let key_manager_state = &(&state.session_state()).into(); + let profile_id = + get_id_type_by_key_from_headers(headers::X_PROFILE_ID.to_string(), request_headers)? + .get_required_value(headers::X_PROFILE_ID)?; + let key_store = state + .store() + .get_merchant_key_store_by_merchant_id( + key_manager_state, + &self.0, + &state.store().get_master_key().to_vec().into(), + ) + .await + .to_not_found_response(errors::ApiErrorResponse::Unauthorized)?; + + let profile = state + .store() + .find_business_profile_by_merchant_id_profile_id( + key_manager_state, + &key_store, + &self.0, + &profile_id, + ) + .await + .to_not_found_response(errors::ApiErrorResponse::Unauthorized)?; + let merchant = state + .store() + .find_merchant_account_by_merchant_id(key_manager_state, &self.0, &key_store) + .await + .to_not_found_response(errors::ApiErrorResponse::Unauthorized)?; + + let auth = AuthenticationData { + merchant_account: merchant, + key_store, + profile, + }; + Ok(( + auth.clone(), + AuthenticationType::MerchantId { + merchant_id: auth.merchant_account.get_id().clone(), + }, + )) + } +} + #[derive(Debug)] pub struct PublishableKeyAuth; @@ -1152,6 +1298,7 @@ impl GetAuthType for PublishableKeyAuth { } } +#[cfg(feature = "v1")] #[async_trait] impl AuthenticateAndFetch for PublishableKeyAuth where @@ -1169,19 +1316,16 @@ where .store() .find_merchant_account_by_publishable_key(key_manager_state, publishable_key) .await - .map_err(|e| { - if e.current_context().is_db_not_found() { - e.change_context(errors::ApiErrorResponse::Unauthorized) - } else { - e.change_context(errors::ApiErrorResponse::InternalServerError) - } - }) - .map(|auth| { + .to_not_found_response(errors::ApiErrorResponse::Unauthorized) + .map(|(merchant_account, key_store)| { + let merchant_id = merchant_account.get_id().clone(); ( - auth.clone(), - AuthenticationType::PublishableKey { - merchant_id: auth.merchant_account.get_id().clone(), + AuthenticationData { + merchant_account, + key_store, + profile_id: None, }, + AuthenticationType::PublishableKey { merchant_id }, ) }) } @@ -1189,7 +1333,7 @@ where #[cfg(feature = "v2")] #[async_trait] -impl AuthenticateAndFetch for PublishableKeyAuth +impl AuthenticateAndFetch for PublishableKeyAuth where A: SessionStateInfo + Sync, { @@ -1197,43 +1341,34 @@ where &self, request_headers: &HeaderMap, state: &A, - ) -> RouterResult<(AuthenticationDataV2, AuthenticationType)> { + ) -> RouterResult<(AuthenticationData, AuthenticationType)> { let publishable_key = get_api_key(request_headers).change_context(errors::ApiErrorResponse::Unauthorized)?; let key_manager_state = &(&state.session_state()).into(); - let authentication_data = state + let profile_id = + get_id_type_by_key_from_headers(headers::X_PROFILE_ID.to_string(), request_headers)? + .get_required_value(headers::X_PROFILE_ID)?; + + let (merchant_account, key_store) = state .store() .find_merchant_account_by_publishable_key(key_manager_state, publishable_key) .await - .map_err(|e| { - if e.current_context().is_db_not_found() { - e.change_context(errors::ApiErrorResponse::Unauthorized) - } else { - e.change_context(errors::ApiErrorResponse::InternalServerError) - } - })?; - - let profile_id = HeaderMapStruct::new(request_headers) - .get_id_type_from_header::(headers::X_PROFILE_ID)?; - + .to_not_found_response(errors::ApiErrorResponse::Unauthorized)?; + let merchant_id = merchant_account.get_id().clone(); let profile = state .store() - .find_business_profile_by_profile_id( + .find_business_profile_by_merchant_id_profile_id( key_manager_state, - &authentication_data.key_store, + &key_store, + &merchant_id, &profile_id, ) .await - .to_not_found_response(errors::ApiErrorResponse::ProfileNotFound { - id: profile_id.get_string_repr().to_owned(), - })?; - - let merchant_id = authentication_data.merchant_account.get_id().clone(); - + .to_not_found_response(errors::ApiErrorResponse::Unauthorized)?; Ok(( - AuthenticationDataV2 { - merchant_account: authentication_data.merchant_account, - key_store: authentication_data.key_store, + AuthenticationData { + merchant_account, + key_store, profile, }, AuthenticationType::PublishableKey { merchant_id }, @@ -1445,6 +1580,7 @@ where } } +#[cfg(feature = "v1")] #[async_trait] impl AuthenticateAndFetch for JWTAuthMerchantFromHeader where @@ -1459,7 +1595,6 @@ where if payload.check_in_blacklist(state).await? { return Err(errors::ApiErrorResponse::InvalidJwtToken.into()); } - let role_info = authorization::get_role_info(state, &payload).await?; authorization::check_permission(&self.required_permission, &role_info)?; @@ -1511,6 +1646,86 @@ where } } +#[cfg(feature = "v2")] +#[async_trait] +impl AuthenticateAndFetch for JWTAuthMerchantFromHeader +where + A: SessionStateInfo + Sync, +{ + async fn authenticate_and_fetch( + &self, + request_headers: &HeaderMap, + state: &A, + ) -> RouterResult<(AuthenticationData, AuthenticationType)> { + let payload = parse_jwt_payload::(request_headers, state).await?; + if payload.check_in_blacklist(state).await? { + return Err(errors::ApiErrorResponse::InvalidJwtToken.into()); + } + let profile_id = + get_id_type_by_key_from_headers(headers::X_PROFILE_ID.to_string(), request_headers)? + .get_required_value(headers::X_PROFILE_ID)?; + + let role_info = authorization::get_role_info(state, &payload).await?; + authorization::check_permission(&self.required_permission, &role_info)?; + + let merchant_id_from_header = HeaderMapStruct::new(request_headers) + .get_id_type_from_header::(headers::X_MERCHANT_ID)?; + + // Check if token has access to MerchantId that has been requested through headers + if payload.merchant_id != merchant_id_from_header { + return Err(report!(errors::ApiErrorResponse::InvalidJwtToken)); + } + + let key_manager_state = &(&state.session_state()).into(); + + let key_store = state + .store() + .get_merchant_key_store_by_merchant_id( + key_manager_state, + &payload.merchant_id, + &state.store().get_master_key().to_vec().into(), + ) + .await + .to_not_found_response(errors::ApiErrorResponse::InvalidJwtToken) + .attach_printable("Failed to fetch merchant key store for the merchant id")?; + + let profile = state + .store() + .find_business_profile_by_merchant_id_profile_id( + key_manager_state, + &key_store, + &payload.merchant_id, + &profile_id, + ) + .await + .to_not_found_response(errors::ApiErrorResponse::Unauthorized)?; + let merchant = state + .store() + .find_merchant_account_by_merchant_id( + key_manager_state, + &payload.merchant_id, + &key_store, + ) + .await + .to_not_found_response(errors::ApiErrorResponse::InvalidJwtToken) + .attach_printable("Failed to fetch merchant account for the merchant id")?; + + let auth = AuthenticationData { + merchant_account: merchant, + key_store, + profile, + }; + + Ok(( + auth, + AuthenticationType::MerchantJwt { + merchant_id: payload.merchant_id, + user_id: Some(payload.user_id), + }, + )) + } +} + #[async_trait] impl AuthenticateAndFetch<(), A> for JWTAuthMerchantFromRoute where @@ -1543,6 +1758,7 @@ where } } +#[cfg(feature = "v1")] #[async_trait] impl AuthenticateAndFetch for JWTAuthMerchantFromRoute where @@ -1602,12 +1818,86 @@ where )) } } + +#[cfg(feature = "v2")] +#[async_trait] +impl AuthenticateAndFetch for JWTAuthMerchantFromRoute +where + A: SessionStateInfo + Sync, +{ + async fn authenticate_and_fetch( + &self, + request_headers: &HeaderMap, + state: &A, + ) -> RouterResult<(AuthenticationData, AuthenticationType)> { + let payload = parse_jwt_payload::(request_headers, state).await?; + let profile_id = + get_id_type_by_key_from_headers(headers::X_PROFILE_ID.to_string(), request_headers)? + .get_required_value(headers::X_PROFILE_ID)?; + if payload.check_in_blacklist(state).await? { + return Err(errors::ApiErrorResponse::InvalidJwtToken.into()); + } + + if payload.merchant_id != self.merchant_id { + return Err(report!(errors::ApiErrorResponse::InvalidJwtToken)); + } + + let role_info = authorization::get_role_info(state, &payload).await?; + authorization::check_permission(&self.required_permission, &role_info)?; + + let key_manager_state = &(&state.session_state()).into(); + let key_store = state + .store() + .get_merchant_key_store_by_merchant_id( + key_manager_state, + &payload.merchant_id, + &state.store().get_master_key().to_vec().into(), + ) + .await + .to_not_found_response(errors::ApiErrorResponse::InvalidJwtToken) + .attach_printable("Failed to fetch merchant key store for the merchant id")?; + let profile = state + .store() + .find_business_profile_by_merchant_id_profile_id( + key_manager_state, + &key_store, + &payload.merchant_id, + &profile_id, + ) + .await + .to_not_found_response(errors::ApiErrorResponse::Unauthorized)?; + let merchant = state + .store() + .find_merchant_account_by_merchant_id( + key_manager_state, + &payload.merchant_id, + &key_store, + ) + .await + .to_not_found_response(errors::ApiErrorResponse::InvalidJwtToken) + .attach_printable("Failed to fetch merchant account for the merchant id")?; + + let auth = AuthenticationData { + merchant_account: merchant, + key_store, + profile, + }; + Ok(( + auth.clone(), + AuthenticationType::MerchantJwt { + merchant_id: auth.merchant_account.get_id().clone(), + user_id: Some(payload.user_id), + }, + )) + } +} pub struct JWTAuthMerchantAndProfileFromRoute { pub merchant_id: id_type::MerchantId, pub profile_id: id_type::ProfileId, pub required_permission: Permission, } +#[cfg(feature = "v1")] #[async_trait] impl AuthenticateAndFetch for JWTAuthMerchantAndProfileFromRoute where @@ -1682,6 +1972,7 @@ pub struct JWTAuthProfileFromRoute { pub required_permission: Permission, } +#[cfg(feature = "v1")] #[async_trait] impl AuthenticateAndFetch for JWTAuthProfileFromRoute where @@ -1759,6 +2050,76 @@ where } } +#[cfg(feature = "v2")] +#[async_trait] +impl AuthenticateAndFetch for JWTAuthProfileFromRoute +where + A: SessionStateInfo + Sync, +{ + async fn authenticate_and_fetch( + &self, + request_headers: &HeaderMap, + state: &A, + ) -> RouterResult<(AuthenticationData, AuthenticationType)> { + let payload = parse_jwt_payload::(request_headers, state).await?; + if payload.check_in_blacklist(state).await? { + return Err(errors::ApiErrorResponse::InvalidJwtToken.into()); + } + let profile_id = + get_id_type_by_key_from_headers(headers::X_PROFILE_ID.to_string(), request_headers)? + .get_required_value(headers::X_PROFILE_ID)?; + + let role_info = authorization::get_role_info(state, &payload).await?; + authorization::check_permission(&self.required_permission, &role_info)?; + + let key_manager_state = &(&state.session_state()).into(); + let key_store = state + .store() + .get_merchant_key_store_by_merchant_id( + key_manager_state, + &payload.merchant_id, + &state.store().get_master_key().to_vec().into(), + ) + .await + .to_not_found_response(errors::ApiErrorResponse::InvalidJwtToken) + .attach_printable("Failed to fetch merchant key store for the merchant id")?; + + let profile = state + .store() + .find_business_profile_by_merchant_id_profile_id( + key_manager_state, + &key_store, + &payload.merchant_id, + &profile_id, + ) + .await + .to_not_found_response(errors::ApiErrorResponse::Unauthorized)?; + let merchant = state + .store() + .find_merchant_account_by_merchant_id( + key_manager_state, + &payload.merchant_id, + &key_store, + ) + .await + .to_not_found_response(errors::ApiErrorResponse::InvalidJwtToken) + .attach_printable("Failed to fetch merchant account for the merchant id")?; + + let auth = AuthenticationData { + merchant_account: merchant, + key_store, + profile, + }; + Ok(( + auth.clone(), + AuthenticationType::MerchantJwt { + merchant_id: auth.merchant_account.get_id().clone(), + user_id: Some(payload.user_id), + }, + )) + } +} + pub async fn parse_jwt_payload(headers: &HeaderMap, state: &A) -> RouterResult where T: serde::de::DeserializeOwned, @@ -1777,6 +2138,7 @@ where decode_jwt(&token, state).await } +#[cfg(feature = "v1")] #[async_trait] impl AuthenticateAndFetch for JWTAuth where @@ -1835,7 +2197,7 @@ where #[cfg(feature = "v2")] #[async_trait] -impl AuthenticateAndFetch for JWTAuth +impl AuthenticateAndFetch for JWTAuth where A: SessionStateInfo + Sync, { @@ -1843,7 +2205,7 @@ where &self, request_headers: &HeaderMap, state: &A, - ) -> RouterResult<(AuthenticationDataV2, AuthenticationType)> { + ) -> RouterResult<(AuthenticationData, AuthenticationType)> { let payload = parse_jwt_payload::(request_headers, state).await?; if payload.check_in_blacklist(state).await? { return Err(errors::ApiErrorResponse::InvalidJwtToken.into()); @@ -1869,7 +2231,12 @@ where let profile = state .store() - .find_business_profile_by_profile_id(key_manager_state, &key_store, &profile_id) + .find_business_profile_by_merchant_id_profile_id( + key_manager_state, + &key_store, + &payload.merchant_id, + &profile_id, + ) .await .to_not_found_response(errors::ApiErrorResponse::Unauthorized)?; let merchant = state @@ -1883,7 +2250,7 @@ where .to_not_found_response(errors::ApiErrorResponse::InvalidJwtToken) .attach_printable("Failed to fetch merchant account for the merchant id")?; let merchant_id = merchant.get_id().clone(); - let auth = AuthenticationDataV2 { + let auth = AuthenticationData { merchant_account: merchant, key_store, profile, @@ -1900,6 +2267,7 @@ where pub type AuthenticationDataWithUserId = (AuthenticationData, String); +#[cfg(feature = "v1")] #[async_trait] impl AuthenticateAndFetch for JWTAuth where @@ -2009,6 +2377,7 @@ where } } +#[cfg(feature = "v1")] #[async_trait] impl AuthenticateAndFetch for DashboardNoPermissionAuth where diff --git a/crates/router/tests/connectors/payme.rs b/crates/router/tests/connectors/payme.rs index 3b4cf5195f5e..dbc3795e7f15 100644 --- a/crates/router/tests/connectors/payme.rs +++ b/crates/router/tests/connectors/payme.rs @@ -1,7 +1,8 @@ use std::str::FromStr; -use api_models::payments::{Address, AddressDetails, OrderDetailsWithAmount}; +use api_models::payments::{Address, AddressDetails}; use common_utils::{pii::Email, types::MinorUnit}; +use diesel_models::types::OrderDetailsWithAmount; use masking::Secret; use router::types::{self, domain, storage::enums, PaymentAddress}; diff --git a/crates/router/tests/connectors/zen.rs b/crates/router/tests/connectors/zen.rs index da83bdc7d415..de60c5e8d86a 100644 --- a/crates/router/tests/connectors/zen.rs +++ b/crates/router/tests/connectors/zen.rs @@ -1,8 +1,8 @@ use std::str::FromStr; -use api_models::payments::OrderDetailsWithAmount; use cards::CardNumber; use common_utils::{pii::Email, types::MinorUnit}; +use hyperswitch_domain_models::types::OrderDetailsWithAmount; use masking::Secret; use router::types::{self, domain, storage::enums}; From 6823418e2a6416fe964eaf756b6418738a5e74e0 Mon Sep 17 00:00:00 2001 From: Rutam Prita Mishra Date: Fri, 8 Nov 2024 15:02:27 +0530 Subject: [PATCH 09/25] feat(payments): Add audit events for PaymentApprove update (#6432) --- .../src/core/payments/operations/payment_approve.rs | 8 +++++++- crates/router/src/events/audit_events.rs | 2 ++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/crates/router/src/core/payments/operations/payment_approve.rs b/crates/router/src/core/payments/operations/payment_approve.rs index fe5e7a7e72f3..2bc18122b227 100644 --- a/crates/router/src/core/payments/operations/payment_approve.rs +++ b/crates/router/src/core/payments/operations/payment_approve.rs @@ -12,6 +12,7 @@ use crate::{ errors::{self, RouterResult, StorageErrorExt}, payments::{helpers, operations, PaymentData}, }, + events::audit_events::{AuditEvent, AuditEventType}, routes::{app::ReqState, SessionState}, services, types::{ @@ -213,7 +214,7 @@ impl UpdateTracker, api::PaymentsCaptureRequest> for async fn update_trackers<'b>( &'b self, state: &'b SessionState, - _req_state: ReqState, + req_state: ReqState, mut payment_data: PaymentData, _customer: Option, storage_scheme: storage_enums::MerchantStorageScheme, @@ -257,6 +258,11 @@ impl UpdateTracker, api::PaymentsCaptureRequest> for ) .await .to_not_found_response(errors::ApiErrorResponse::PaymentNotFound)?; + req_state + .event_context + .event(AuditEvent::new(AuditEventType::PaymentApprove)) + .with(payment_data.to_event()) + .emit(); Ok((Box::new(self), payment_data)) } diff --git a/crates/router/src/events/audit_events.rs b/crates/router/src/events/audit_events.rs index 54c1934e36f7..9fbd754b43c0 100644 --- a/crates/router/src/events/audit_events.rs +++ b/crates/router/src/events/audit_events.rs @@ -27,6 +27,7 @@ pub enum AuditEventType { capture_amount: Option, multiple_capture_count: Option, }, + PaymentApprove, PaymentCreate, } @@ -66,6 +67,7 @@ impl Event for AuditEvent { AuditEventType::RefundSuccess => "refund_success", AuditEventType::RefundFail => "refund_fail", AuditEventType::PaymentCancelled { .. } => "payment_cancelled", + AuditEventType::PaymentApprove { .. } => "payment_approve", AuditEventType::PaymentCreate { .. } => "payment_create", }; format!( From 529f1a76be2e10759b44e6cfb21a7d43bbc53109 Mon Sep 17 00:00:00 2001 From: Sandeep Kumar <83278309+tsdk02@users.noreply.github.com> Date: Fri, 8 Nov 2024 15:35:24 +0530 Subject: [PATCH 10/25] feat(opensearch): refactor global search querybuilder and add case insensitivity opensearch filters (#6476) --- crates/analytics/src/opensearch.rs | 223 ++++++++++++++++++----------- crates/analytics/src/search.rs | 12 +- 2 files changed, 151 insertions(+), 84 deletions(-) diff --git a/crates/analytics/src/opensearch.rs b/crates/analytics/src/opensearch.rs index a6e6c486ebe1..84a2b9db3d4e 100644 --- a/crates/analytics/src/opensearch.rs +++ b/crates/analytics/src/opensearch.rs @@ -1,3 +1,5 @@ +use std::collections::HashSet; + use api_models::{ analytics::search::SearchIndex, errors::types::{ApiError, ApiErrorResponse}, @@ -456,7 +458,8 @@ pub struct OpenSearchQueryBuilder { pub count: Option, pub filters: Vec<(String, Vec)>, pub time_range: Option, - pub search_params: Vec, + search_params: Vec, + case_sensitive_fields: HashSet<&'static str>, } impl OpenSearchQueryBuilder { @@ -469,6 +472,12 @@ impl OpenSearchQueryBuilder { count: Default::default(), filters: Default::default(), time_range: Default::default(), + case_sensitive_fields: HashSet::from([ + "customer_email.keyword", + "search_tags.keyword", + "card_last_4.keyword", + "payment_id.keyword", + ]), } } @@ -490,48 +499,16 @@ impl OpenSearchQueryBuilder { pub fn get_status_field(&self, index: &SearchIndex) -> &str { match index { - SearchIndex::Refunds => "refund_status.keyword", - SearchIndex::Disputes => "dispute_status.keyword", + SearchIndex::Refunds | SearchIndex::SessionizerRefunds => "refund_status.keyword", + SearchIndex::Disputes | SearchIndex::SessionizerDisputes => "dispute_status.keyword", _ => "status.keyword", } } - pub fn replace_status_field(&self, filters: &[Value], index: &SearchIndex) -> Vec { - filters - .iter() - .map(|filter| { - if let Some(terms) = filter.get("terms").and_then(|v| v.as_object()) { - let mut new_filter = filter.clone(); - if let Some(new_terms) = - new_filter.get_mut("terms").and_then(|v| v.as_object_mut()) - { - let key = "status.keyword"; - if let Some(status_terms) = terms.get(key) { - new_terms.remove(key); - new_terms.insert( - self.get_status_field(index).to_string(), - status_terms.clone(), - ); - } - } - new_filter - } else { - filter.clone() - } - }) - .collect() - } - - /// # Panics - /// - /// This function will panic if: - /// - /// * The structure of the JSON query is not as expected (e.g., missing keys or incorrect types). - /// - /// Ensure that the input data and the structure of the query are valid and correctly handled. - pub fn construct_payload(&self, indexes: &[SearchIndex]) -> QueryResult> { - let mut query_obj = Map::new(); - let mut bool_obj = Map::new(); + pub fn build_filter_array( + &self, + case_sensitive_filters: Vec<&(String, Vec)>, + ) -> Vec { let mut filter_array = Vec::new(); filter_array.push(json!({ @@ -542,13 +519,12 @@ impl OpenSearchQueryBuilder { } })); - let mut filters = self - .filters - .iter() + let case_sensitive_json_filters = case_sensitive_filters + .into_iter() .map(|(k, v)| json!({"terms": {k: v}})) .collect::>(); - filter_array.append(&mut filters); + filter_array.extend(case_sensitive_json_filters); if let Some(ref time_range) = self.time_range { let range = json!(time_range); @@ -559,8 +535,72 @@ impl OpenSearchQueryBuilder { })); } - let should_array = self - .search_params + filter_array + } + + pub fn build_case_insensitive_filters( + &self, + mut payload: Value, + case_insensitive_filters: &[&(String, Vec)], + auth_array: Vec, + index: &SearchIndex, + ) -> Value { + let mut must_array = case_insensitive_filters + .iter() + .map(|(k, v)| { + let key = if *k == "status.keyword" { + self.get_status_field(index).to_string() + } else { + k.clone() + }; + json!({ + "bool": { + "must": [ + { + "bool": { + "should": v.iter().map(|value| { + json!({ + "term": { + format!("{}", key): { + "value": value, + "case_insensitive": true + } + } + }) + }).collect::>(), + "minimum_should_match": 1 + } + } + ] + } + }) + }) + .collect::>(); + + must_array.push(json!({ "bool": { + "must": [ + { + "bool": { + "should": auth_array, + "minimum_should_match": 1 + } + } + ] + }})); + + if let Some(query) = payload.get_mut("query") { + if let Some(bool_obj) = query.get_mut("bool") { + if let Some(bool_map) = bool_obj.as_object_mut() { + bool_map.insert("must".to_string(), Value::Array(must_array)); + } + } + } + + payload + } + + pub fn build_auth_array(&self) -> Vec { + self.search_params .iter() .map(|user_level| match user_level { AuthInfo::OrgLevel { org_id } => { @@ -579,11 +619,17 @@ impl OpenSearchQueryBuilder { }) } AuthInfo::MerchantLevel { - org_id: _, + org_id, merchant_ids, } => { let must_clauses = vec![ - // TODO: Add org_id field to the filters + json!({ + "term": { + "organization_id.keyword": { + "value": org_id + } + } + }), json!({ "terms": { "merchant_id.keyword": merchant_ids @@ -598,12 +644,18 @@ impl OpenSearchQueryBuilder { }) } AuthInfo::ProfileLevel { - org_id: _, + org_id, merchant_id, profile_ids, } => { let must_clauses = vec![ - // TODO: Add org_id field to the filters + json!({ + "term": { + "organization_id.keyword": { + "value": org_id + } + } + }), json!({ "term": { "merchant_id.keyword": { @@ -625,55 +677,60 @@ impl OpenSearchQueryBuilder { }) } }) - .collect::>(); + .collect::>() + } + + /// # Panics + /// + /// This function will panic if: + /// + /// * The structure of the JSON query is not as expected (e.g., missing keys or incorrect types). + /// + /// Ensure that the input data and the structure of the query are valid and correctly handled. + pub fn construct_payload(&self, indexes: &[SearchIndex]) -> QueryResult> { + let mut query_obj = Map::new(); + let mut bool_obj = Map::new(); + + let (case_sensitive_filters, case_insensitive_filters): (Vec<_>, Vec<_>) = self + .filters + .iter() + .partition(|(k, _)| self.case_sensitive_fields.contains(k.as_str())); + + let filter_array = self.build_filter_array(case_sensitive_filters); if !filter_array.is_empty() { bool_obj.insert("filter".to_string(), Value::Array(filter_array)); } + + let should_array = self.build_auth_array(); + if !bool_obj.is_empty() { query_obj.insert("bool".to_string(), Value::Object(bool_obj)); } - let mut query = Map::new(); - query.insert("query".to_string(), Value::Object(query_obj)); + let mut sort_obj = Map::new(); + sort_obj.insert( + "@timestamp".to_string(), + json!({ + "order": "desc" + }), + ); Ok(indexes .iter() .map(|index| { - let updated_query = query - .get("query") - .and_then(|q| q.get("bool")) - .and_then(|b| b.get("filter")) - .and_then(|f| f.as_array()) - .map(|filters| self.replace_status_field(filters, index)) - .unwrap_or_default(); - let mut final_bool_obj = Map::new(); - if !updated_query.is_empty() { - final_bool_obj.insert("filter".to_string(), Value::Array(updated_query)); - } - if !should_array.is_empty() { - final_bool_obj.insert("should".to_string(), Value::Array(should_array.clone())); - final_bool_obj - .insert("minimum_should_match".to_string(), Value::Number(1.into())); - } - let mut final_query = Map::new(); - if !final_bool_obj.is_empty() { - final_query.insert("bool".to_string(), Value::Object(final_bool_obj)); - } - - let mut sort_obj = Map::new(); - sort_obj.insert( - "@timestamp".to_string(), - json!({ - "order": "desc" - }), - ); - let payload = json!({ - "query": Value::Object(final_query), + let mut payload = json!({ + "query": query_obj.clone(), "sort": [ - Value::Object(sort_obj) + Value::Object(sort_obj.clone()) ] }); + payload = self.build_case_insensitive_filters( + payload, + &case_insensitive_filters, + should_array.clone(), + index, + ); payload }) .collect::>()) diff --git a/crates/analytics/src/search.rs b/crates/analytics/src/search.rs index f53b07b12321..9be0200030d2 100644 --- a/crates/analytics/src/search.rs +++ b/crates/analytics/src/search.rs @@ -190,7 +190,17 @@ pub async fn search_results( search_params: Vec, ) -> CustomResult { let search_req = req.search_req; - + if search_req.query.trim().is_empty() + && search_req + .filters + .as_ref() + .map_or(true, |filters| filters.is_all_none()) + { + return Err(OpenSearchError::BadRequestError( + "Both query and filters are empty".to_string(), + ) + .into()); + } let mut query_builder = OpenSearchQueryBuilder::new( OpenSearchQuery::Search(req.index), search_req.query, From fe4931a37e6030ea03ca83540f9a21877c7b6b34 Mon Sep 17 00:00:00 2001 From: Anurag Date: Fri, 8 Nov 2024 15:40:45 +0530 Subject: [PATCH 11/25] Feat(connector): [AMAZON PAY] Added Template code (#6486) Co-authored-by: Anurag Singh --- config/config.example.toml | 1 + config/deployments/integration_test.toml | 1 + config/deployments/production.toml | 1 + config/deployments/sandbox.toml | 1 + config/development.toml | 2 + config/docker_compose.toml | 2 + crates/api_models/src/connector_enums.rs | 2 + crates/common_enums/src/connector_enums.rs | 1 + .../hyperswitch_connectors/src/connectors.rs | 18 +- .../src/connectors/amazonpay.rs | 567 ++++++++++++++++++ .../src/connectors/amazonpay/transformers.rs | 228 +++++++ .../src/default_implementations.rs | 31 + .../src/default_implementations_v2.rs | 22 + crates/hyperswitch_interfaces/src/configs.rs | 1 + crates/router/src/connector.rs | 24 +- .../connector_integration_v2_impls.rs | 3 + crates/router/src/core/payments/flows.rs | 4 + crates/router/src/types/api.rs | 3 + crates/router/src/types/transformers.rs | 1 + crates/router/tests/connectors/amazonpay.rs | 421 +++++++++++++ crates/router/tests/connectors/main.rs | 1 + .../router/tests/connectors/sample_auth.toml | 3 + crates/test_utils/src/connector_auth.rs | 1 + loadtest/config/development.toml | 2 + scripts/add_connector.sh | 2 +- 25 files changed, 1322 insertions(+), 21 deletions(-) create mode 100644 crates/hyperswitch_connectors/src/connectors/amazonpay.rs create mode 100644 crates/hyperswitch_connectors/src/connectors/amazonpay/transformers.rs create mode 100644 crates/router/tests/connectors/amazonpay.rs diff --git a/config/config.example.toml b/config/config.example.toml index 3a4a2c271b01..ea458238ebe8 100644 --- a/config/config.example.toml +++ b/config/config.example.toml @@ -183,6 +183,7 @@ adyen.payout_base_url = "https://pal-test.adyen.com/" adyen.dispute_base_url = "https://ca-test.adyen.com/" adyenplatform.base_url = "https://balanceplatform-api-test.adyen.com/" airwallex.base_url = "https://api-demo.airwallex.com/" +amazonpay.base_url = "https://pay-api.amazon.com/v2" applepay.base_url = "https://apple-pay-gateway.apple.com/" authorizedotnet.base_url = "https://apitest.authorize.net/xml/v1/request.api" bambora.base_url = "https://api.na.bambora.com" diff --git a/config/deployments/integration_test.toml b/config/deployments/integration_test.toml index ebc1e36d1591..73444e966c6d 100644 --- a/config/deployments/integration_test.toml +++ b/config/deployments/integration_test.toml @@ -25,6 +25,7 @@ adyen.payout_base_url = "https://pal-test.adyen.com/" adyen.dispute_base_url = "https://ca-test.adyen.com/" adyenplatform.base_url = "https://balanceplatform-api-test.adyen.com/" airwallex.base_url = "https://api-demo.airwallex.com/" +amazonpay.base_url = "https://pay-api.amazon.com/v2" applepay.base_url = "https://apple-pay-gateway.apple.com/" authorizedotnet.base_url = "https://apitest.authorize.net/xml/v1/request.api" bambora.base_url = "https://api.na.bambora.com" diff --git a/config/deployments/production.toml b/config/deployments/production.toml index ef1ee42b9e67..0a12f06f1a5d 100644 --- a/config/deployments/production.toml +++ b/config/deployments/production.toml @@ -29,6 +29,7 @@ adyen.payout_base_url = "https://{{merchant_endpoint_prefix}}-pal-live.adyenpaym adyen.dispute_base_url = "https://{{merchant_endpoint_prefix}}-ca-live.adyen.com/" adyenplatform.base_url = "https://balanceplatform-api-live.adyen.com/" airwallex.base_url = "https://api-demo.airwallex.com/" +amazonpay.base_url = "https://pay-api.amazon.com/v2" applepay.base_url = "https://apple-pay-gateway.apple.com/" authorizedotnet.base_url = "https://api.authorize.net/xml/v1/request.api" bambora.base_url = "https://api.na.bambora.com" diff --git a/config/deployments/sandbox.toml b/config/deployments/sandbox.toml index a09dba1a9a66..a110bd990185 100644 --- a/config/deployments/sandbox.toml +++ b/config/deployments/sandbox.toml @@ -29,6 +29,7 @@ adyen.payout_base_url = "https://pal-test.adyen.com/" adyen.dispute_base_url = "https://ca-test.adyen.com/" adyenplatform.base_url = "https://balanceplatform-api-test.adyen.com/" airwallex.base_url = "https://api-demo.airwallex.com/" +amazonpay.base_url = "https://pay-api.amazon.com/v2" applepay.base_url = "https://apple-pay-gateway.apple.com/" authorizedotnet.base_url = "https://apitest.authorize.net/xml/v1/request.api" bambora.base_url = "https://api.na.bambora.com" diff --git a/config/development.toml b/config/development.toml index 46d6378f0832..e52cf1dff1ae 100644 --- a/config/development.toml +++ b/config/development.toml @@ -99,6 +99,7 @@ cards = [ "adyen", "adyenplatform", "airwallex", + "amazonpay", "authorizedotnet", "bambora", "bamboraapac", @@ -195,6 +196,7 @@ adyenplatform.base_url = "https://balanceplatform-api-test.adyen.com/" adyen.payout_base_url = "https://pal-test.adyen.com/" adyen.dispute_base_url = "https://ca-test.adyen.com/" airwallex.base_url = "https://api-demo.airwallex.com/" +amazonpay.base_url = "https://pay-api.amazon.com/v2" applepay.base_url = "https://apple-pay-gateway.apple.com/" authorizedotnet.base_url = "https://apitest.authorize.net/xml/v1/request.api" bambora.base_url = "https://api.na.bambora.com" diff --git a/config/docker_compose.toml b/config/docker_compose.toml index 5a2e7249fda4..65fc8ec67f5a 100644 --- a/config/docker_compose.toml +++ b/config/docker_compose.toml @@ -113,6 +113,7 @@ adyenplatform.base_url = "https://balanceplatform-api-test.adyen.com/" adyen.payout_base_url = "https://pal-test.adyen.com/" adyen.dispute_base_url = "https://ca-test.adyen.com/" airwallex.base_url = "https://api-demo.airwallex.com/" +amazonpay.base_url = "https://pay-api.amazon.com/v2" applepay.base_url = "https://apple-pay-gateway.apple.com/" authorizedotnet.base_url = "https://apitest.authorize.net/xml/v1/request.api" bambora.base_url = "https://api.na.bambora.com" @@ -215,6 +216,7 @@ cards = [ "adyen", "adyenplatform", "airwallex", + "amazonpay", "authorizedotnet", "bambora", "bamboraapac", diff --git a/crates/api_models/src/connector_enums.rs b/crates/api_models/src/connector_enums.rs index 68fd73cd0de0..14d477f077b5 100644 --- a/crates/api_models/src/connector_enums.rs +++ b/crates/api_models/src/connector_enums.rs @@ -54,6 +54,7 @@ pub enum Connector { Aci, Adyen, Airwallex, + // Amazonpay, Authorizedotnet, Bambora, Bamboraapac, @@ -196,6 +197,7 @@ impl Connector { | Self::Adyen | Self::Adyenplatform | Self::Airwallex + // | Self::Amazonpay | Self::Authorizedotnet | Self::Bambora | Self::Bamboraapac diff --git a/crates/common_enums/src/connector_enums.rs b/crates/common_enums/src/connector_enums.rs index 386b4c35a4b7..8828050052f3 100644 --- a/crates/common_enums/src/connector_enums.rs +++ b/crates/common_enums/src/connector_enums.rs @@ -51,6 +51,7 @@ pub enum RoutableConnectors { Aci, Adyen, Airwallex, + // Amazonpay, Authorizedotnet, Bankofamerica, Billwerk, diff --git a/crates/hyperswitch_connectors/src/connectors.rs b/crates/hyperswitch_connectors/src/connectors.rs index fb29417eaa08..b748eb1120aa 100644 --- a/crates/hyperswitch_connectors/src/connectors.rs +++ b/crates/hyperswitch_connectors/src/connectors.rs @@ -1,4 +1,5 @@ pub mod airwallex; +pub mod amazonpay; pub mod bambora; pub mod billwerk; pub mod bitpay; @@ -38,12 +39,13 @@ pub mod zen; pub mod zsl; pub use self::{ - airwallex::Airwallex, bambora::Bambora, billwerk::Billwerk, bitpay::Bitpay, - cashtocode::Cashtocode, coinbase::Coinbase, cryptopay::Cryptopay, deutschebank::Deutschebank, - digitalvirgo::Digitalvirgo, dlocal::Dlocal, elavon::Elavon, fiserv::Fiserv, - fiservemea::Fiservemea, fiuu::Fiuu, forte::Forte, globepay::Globepay, helcim::Helcim, - jpmorgan::Jpmorgan, mollie::Mollie, multisafepay::Multisafepay, nexinets::Nexinets, - nexixpay::Nexixpay, novalnet::Novalnet, payeezy::Payeezy, payu::Payu, powertranz::Powertranz, - razorpay::Razorpay, shift4::Shift4, square::Square, stax::Stax, taxjar::Taxjar, thunes::Thunes, - tsys::Tsys, volt::Volt, worldline::Worldline, worldpay::Worldpay, zen::Zen, zsl::Zsl, + airwallex::Airwallex, amazonpay::Amazonpay, bambora::Bambora, billwerk::Billwerk, + bitpay::Bitpay, cashtocode::Cashtocode, coinbase::Coinbase, cryptopay::Cryptopay, + deutschebank::Deutschebank, digitalvirgo::Digitalvirgo, dlocal::Dlocal, elavon::Elavon, + fiserv::Fiserv, fiservemea::Fiservemea, fiuu::Fiuu, forte::Forte, globepay::Globepay, + helcim::Helcim, jpmorgan::Jpmorgan, mollie::Mollie, multisafepay::Multisafepay, + nexinets::Nexinets, nexixpay::Nexixpay, novalnet::Novalnet, payeezy::Payeezy, payu::Payu, + powertranz::Powertranz, razorpay::Razorpay, shift4::Shift4, square::Square, stax::Stax, + taxjar::Taxjar, thunes::Thunes, tsys::Tsys, volt::Volt, worldline::Worldline, + worldpay::Worldpay, zen::Zen, zsl::Zsl, }; diff --git a/crates/hyperswitch_connectors/src/connectors/amazonpay.rs b/crates/hyperswitch_connectors/src/connectors/amazonpay.rs new file mode 100644 index 000000000000..da4d3d79bbcf --- /dev/null +++ b/crates/hyperswitch_connectors/src/connectors/amazonpay.rs @@ -0,0 +1,567 @@ +pub mod transformers; + +use common_utils::{ + errors::CustomResult, + ext_traits::BytesExt, + request::{Method, Request, RequestBuilder, RequestContent}, + types::{AmountConvertor, StringMinorUnit, StringMinorUnitForConnector}, +}; +use error_stack::{report, ResultExt}; +use hyperswitch_domain_models::{ + router_data::{AccessToken, ConnectorAuthType, ErrorResponse, RouterData}, + router_flow_types::{ + access_token_auth::AccessTokenAuth, + payments::{Authorize, Capture, PSync, PaymentMethodToken, Session, SetupMandate, Void}, + refunds::{Execute, RSync}, + }, + router_request_types::{ + AccessTokenRequestData, PaymentMethodTokenizationData, PaymentsAuthorizeData, + PaymentsCancelData, PaymentsCaptureData, PaymentsSessionData, PaymentsSyncData, + RefundsData, SetupMandateRequestData, + }, + router_response_types::{PaymentsResponseData, RefundsResponseData}, + types::{ + PaymentsAuthorizeRouterData, PaymentsCaptureRouterData, PaymentsSyncRouterData, + RefundSyncRouterData, RefundsRouterData, + }, +}; +use hyperswitch_interfaces::{ + api::{self, ConnectorCommon, ConnectorCommonExt, ConnectorIntegration, ConnectorValidation}, + configs::Connectors, + errors, + events::connector_api_logs::ConnectorEvent, + types::{self, Response}, + webhooks, +}; +use masking::{ExposeInterface, Mask}; +use transformers as amazonpay; + +use crate::{constants::headers, types::ResponseRouterData, utils}; + +#[derive(Clone)] +pub struct Amazonpay { + amount_converter: &'static (dyn AmountConvertor + Sync), +} + +impl Amazonpay { + pub fn new() -> &'static Self { + &Self { + amount_converter: &StringMinorUnitForConnector, + } + } +} + +impl api::Payment for Amazonpay {} +impl api::PaymentSession for Amazonpay {} +impl api::ConnectorAccessToken for Amazonpay {} +impl api::MandateSetup for Amazonpay {} +impl api::PaymentAuthorize for Amazonpay {} +impl api::PaymentSync for Amazonpay {} +impl api::PaymentCapture for Amazonpay {} +impl api::PaymentVoid for Amazonpay {} +impl api::Refund for Amazonpay {} +impl api::RefundExecute for Amazonpay {} +impl api::RefundSync for Amazonpay {} +impl api::PaymentToken for Amazonpay {} + +impl ConnectorIntegration + for Amazonpay +{ + // Not Implemented (R) +} + +impl ConnectorCommonExt for Amazonpay +where + Self: ConnectorIntegration, +{ + fn build_headers( + &self, + req: &RouterData, + _connectors: &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) + } +} + +impl ConnectorCommon for Amazonpay { + fn id(&self) -> &'static str { + "amazonpay" + } + + fn get_currency_unit(&self) -> api::CurrencyUnit { + api::CurrencyUnit::Base + // todo!() + // TODO! Check connector documentation, on which unit they are processing the currency. + // If the connector accepts amount in lower unit ( i.e cents for USD) then return api::CurrencyUnit::Minor, + // if connector accepts amount in base unit (i.e dollars for USD) then return api::CurrencyUnit::Base + } + + fn common_get_content_type(&self) -> &'static str { + "application/json" + } + + fn base_url<'a>(&self, connectors: &'a Connectors) -> &'a str { + connectors.amazonpay.base_url.as_ref() + } + + fn get_auth_header( + &self, + auth_type: &ConnectorAuthType, + ) -> CustomResult)>, errors::ConnectorError> { + let auth = amazonpay::AmazonpayAuthType::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, + event_builder: Option<&mut ConnectorEvent>, + ) -> CustomResult { + let response: amazonpay::AmazonpayErrorResponse = res + .response + .parse_struct("AmazonpayErrorResponse") + .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + + event_builder.map(|i| i.set_response_body(&response)); + router_env::logger::info!(connector_response=?response); + + Ok(ErrorResponse { + status_code: res.status_code, + code: response.code, + message: response.message, + reason: response.reason, + attempt_status: None, + connector_transaction_id: None, + }) + } +} + +impl ConnectorValidation for Amazonpay { + //TODO: implement functions when support enabled +} + +impl ConnectorIntegration for Amazonpay { + //TODO: implement sessions flow +} + +impl ConnectorIntegration for Amazonpay {} + +impl ConnectorIntegration + for Amazonpay +{ +} + +impl ConnectorIntegration for Amazonpay { + fn get_headers( + &self, + req: &PaymentsAuthorizeRouterData, + connectors: &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: &PaymentsAuthorizeRouterData, + _connectors: &Connectors, + ) -> CustomResult { + Err(errors::ConnectorError::NotImplemented("get_url method".to_string()).into()) + } + + fn get_request_body( + &self, + req: &PaymentsAuthorizeRouterData, + _connectors: &Connectors, + ) -> CustomResult { + let amount = utils::convert_amount( + self.amount_converter, + req.request.minor_amount, + req.request.currency, + )?; + + let connector_router_data = amazonpay::AmazonpayRouterData::from((amount, req)); + let connector_req = amazonpay::AmazonpayPaymentsRequest::try_from(&connector_router_data)?; + Ok(RequestContent::Json(Box::new(connector_req))) + } + + fn build_request( + &self, + req: &PaymentsAuthorizeRouterData, + connectors: &Connectors, + ) -> CustomResult, errors::ConnectorError> { + Ok(Some( + RequestBuilder::new() + .method(Method::Post) + .url(&types::PaymentsAuthorizeType::get_url( + self, req, connectors, + )?) + .attach_default_headers() + .headers(types::PaymentsAuthorizeType::get_headers( + self, req, connectors, + )?) + .set_body(types::PaymentsAuthorizeType::get_request_body( + self, req, connectors, + )?) + .build(), + )) + } + + fn handle_response( + &self, + data: &PaymentsAuthorizeRouterData, + event_builder: Option<&mut ConnectorEvent>, + res: Response, + ) -> CustomResult { + let response: amazonpay::AmazonpayPaymentsResponse = res + .response + .parse_struct("Amazonpay PaymentsAuthorizeResponse") + .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + event_builder.map(|i| i.set_response_body(&response)); + router_env::logger::info!(connector_response=?response); + RouterData::try_from(ResponseRouterData { + response, + data: data.clone(), + http_code: res.status_code, + }) + } + + fn get_error_response( + &self, + res: Response, + event_builder: Option<&mut ConnectorEvent>, + ) -> CustomResult { + self.build_error_response(res, event_builder) + } +} + +impl ConnectorIntegration for Amazonpay { + fn get_headers( + &self, + req: &PaymentsSyncRouterData, + connectors: &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: &PaymentsSyncRouterData, + _connectors: &Connectors, + ) -> CustomResult { + Err(errors::ConnectorError::NotImplemented("get_url method".to_string()).into()) + } + + fn build_request( + &self, + req: &PaymentsSyncRouterData, + connectors: &Connectors, + ) -> CustomResult, errors::ConnectorError> { + Ok(Some( + RequestBuilder::new() + .method(Method::Get) + .url(&types::PaymentsSyncType::get_url(self, req, connectors)?) + .attach_default_headers() + .headers(types::PaymentsSyncType::get_headers(self, req, connectors)?) + .build(), + )) + } + + fn handle_response( + &self, + data: &PaymentsSyncRouterData, + event_builder: Option<&mut ConnectorEvent>, + res: Response, + ) -> CustomResult { + let response: amazonpay::AmazonpayPaymentsResponse = res + .response + .parse_struct("amazonpay PaymentsSyncResponse") + .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + event_builder.map(|i| i.set_response_body(&response)); + router_env::logger::info!(connector_response=?response); + RouterData::try_from(ResponseRouterData { + response, + data: data.clone(), + http_code: res.status_code, + }) + } + + fn get_error_response( + &self, + res: Response, + event_builder: Option<&mut ConnectorEvent>, + ) -> CustomResult { + self.build_error_response(res, event_builder) + } +} + +impl ConnectorIntegration for Amazonpay { + fn get_headers( + &self, + req: &PaymentsCaptureRouterData, + connectors: &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: &PaymentsCaptureRouterData, + _connectors: &Connectors, + ) -> CustomResult { + Err(errors::ConnectorError::NotImplemented("get_url method".to_string()).into()) + } + + fn get_request_body( + &self, + _req: &PaymentsCaptureRouterData, + _connectors: &Connectors, + ) -> CustomResult { + Err(errors::ConnectorError::NotImplemented("get_request_body method".to_string()).into()) + } + + fn build_request( + &self, + req: &PaymentsCaptureRouterData, + connectors: &Connectors, + ) -> CustomResult, errors::ConnectorError> { + Ok(Some( + RequestBuilder::new() + .method(Method::Post) + .url(&types::PaymentsCaptureType::get_url(self, req, connectors)?) + .attach_default_headers() + .headers(types::PaymentsCaptureType::get_headers( + self, req, connectors, + )?) + .set_body(types::PaymentsCaptureType::get_request_body( + self, req, connectors, + )?) + .build(), + )) + } + + fn handle_response( + &self, + data: &PaymentsCaptureRouterData, + event_builder: Option<&mut ConnectorEvent>, + res: Response, + ) -> CustomResult { + let response: amazonpay::AmazonpayPaymentsResponse = res + .response + .parse_struct("Amazonpay PaymentsCaptureResponse") + .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + event_builder.map(|i| i.set_response_body(&response)); + router_env::logger::info!(connector_response=?response); + RouterData::try_from(ResponseRouterData { + response, + data: data.clone(), + http_code: res.status_code, + }) + } + + fn get_error_response( + &self, + res: Response, + event_builder: Option<&mut ConnectorEvent>, + ) -> CustomResult { + self.build_error_response(res, event_builder) + } +} + +impl ConnectorIntegration for Amazonpay {} + +impl ConnectorIntegration for Amazonpay { + fn get_headers( + &self, + req: &RefundsRouterData, + connectors: &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: &RefundsRouterData, + _connectors: &Connectors, + ) -> CustomResult { + Err(errors::ConnectorError::NotImplemented("get_url method".to_string()).into()) + } + + fn get_request_body( + &self, + req: &RefundsRouterData, + _connectors: &Connectors, + ) -> CustomResult { + let refund_amount = utils::convert_amount( + self.amount_converter, + req.request.minor_refund_amount, + req.request.currency, + )?; + + let connector_router_data = amazonpay::AmazonpayRouterData::from((refund_amount, req)); + let connector_req = amazonpay::AmazonpayRefundRequest::try_from(&connector_router_data)?; + Ok(RequestContent::Json(Box::new(connector_req))) + } + + fn build_request( + &self, + req: &RefundsRouterData, + connectors: &Connectors, + ) -> CustomResult, errors::ConnectorError> { + let request = RequestBuilder::new() + .method(Method::Post) + .url(&types::RefundExecuteType::get_url(self, req, connectors)?) + .attach_default_headers() + .headers(types::RefundExecuteType::get_headers( + self, req, connectors, + )?) + .set_body(types::RefundExecuteType::get_request_body( + self, req, connectors, + )?) + .build(); + Ok(Some(request)) + } + + fn handle_response( + &self, + data: &RefundsRouterData, + event_builder: Option<&mut ConnectorEvent>, + res: Response, + ) -> CustomResult, errors::ConnectorError> { + let response: amazonpay::RefundResponse = res + .response + .parse_struct("amazonpay RefundResponse") + .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + event_builder.map(|i| i.set_response_body(&response)); + router_env::logger::info!(connector_response=?response); + RouterData::try_from(ResponseRouterData { + response, + data: data.clone(), + http_code: res.status_code, + }) + } + + fn get_error_response( + &self, + res: Response, + event_builder: Option<&mut ConnectorEvent>, + ) -> CustomResult { + self.build_error_response(res, event_builder) + } +} + +impl ConnectorIntegration for Amazonpay { + fn get_headers( + &self, + req: &RefundSyncRouterData, + connectors: &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: &RefundSyncRouterData, + _connectors: &Connectors, + ) -> CustomResult { + Err(errors::ConnectorError::NotImplemented("get_url method".to_string()).into()) + } + + fn build_request( + &self, + req: &RefundSyncRouterData, + connectors: &Connectors, + ) -> CustomResult, errors::ConnectorError> { + Ok(Some( + RequestBuilder::new() + .method(Method::Get) + .url(&types::RefundSyncType::get_url(self, req, connectors)?) + .attach_default_headers() + .headers(types::RefundSyncType::get_headers(self, req, connectors)?) + .set_body(types::RefundSyncType::get_request_body( + self, req, connectors, + )?) + .build(), + )) + } + + fn handle_response( + &self, + data: &RefundSyncRouterData, + event_builder: Option<&mut ConnectorEvent>, + res: Response, + ) -> CustomResult { + let response: amazonpay::RefundResponse = res + .response + .parse_struct("amazonpay RefundSyncResponse") + .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + event_builder.map(|i| i.set_response_body(&response)); + router_env::logger::info!(connector_response=?response); + RouterData::try_from(ResponseRouterData { + response, + data: data.clone(), + http_code: res.status_code, + }) + } + + fn get_error_response( + &self, + res: Response, + event_builder: Option<&mut ConnectorEvent>, + ) -> CustomResult { + self.build_error_response(res, event_builder) + } +} + +#[async_trait::async_trait] +impl webhooks::IncomingWebhook for Amazonpay { + fn get_webhook_object_reference_id( + &self, + _request: &webhooks::IncomingWebhookRequestDetails<'_>, + ) -> CustomResult { + Err(report!(errors::ConnectorError::WebhooksNotImplemented)) + } + + fn get_webhook_event_type( + &self, + _request: &webhooks::IncomingWebhookRequestDetails<'_>, + ) -> CustomResult { + Err(report!(errors::ConnectorError::WebhooksNotImplemented)) + } + + fn get_webhook_resource_object( + &self, + _request: &webhooks::IncomingWebhookRequestDetails<'_>, + ) -> CustomResult, errors::ConnectorError> { + Err(report!(errors::ConnectorError::WebhooksNotImplemented)) + } +} diff --git a/crates/hyperswitch_connectors/src/connectors/amazonpay/transformers.rs b/crates/hyperswitch_connectors/src/connectors/amazonpay/transformers.rs new file mode 100644 index 000000000000..b61327814237 --- /dev/null +++ b/crates/hyperswitch_connectors/src/connectors/amazonpay/transformers.rs @@ -0,0 +1,228 @@ +use common_enums::enums; +use common_utils::types::StringMinorUnit; +use hyperswitch_domain_models::{ + payment_method_data::PaymentMethodData, + router_data::{ConnectorAuthType, RouterData}, + router_flow_types::refunds::{Execute, RSync}, + router_request_types::ResponseId, + router_response_types::{PaymentsResponseData, RefundsResponseData}, + types::{PaymentsAuthorizeRouterData, RefundsRouterData}, +}; +use hyperswitch_interfaces::errors; +use masking::Secret; +use serde::{Deserialize, Serialize}; + +use crate::{ + types::{RefundsResponseRouterData, ResponseRouterData}, + utils::PaymentsAuthorizeRequestData, +}; + +//TODO: Fill the struct with respective fields +pub struct AmazonpayRouterData { + pub amount: StringMinorUnit, // The type of amount that a connector accepts, for example, String, i64, f64, etc. + pub router_data: T, +} + +impl From<(StringMinorUnit, T)> for AmazonpayRouterData { + fn from((amount, item): (StringMinorUnit, T)) -> Self { + //Todo : use utils to convert the amount to the type of amount that a connector accepts + Self { + amount, + router_data: item, + } + } +} + +//TODO: Fill the struct with respective fields +#[derive(Default, Debug, Serialize, PartialEq)] +pub struct AmazonpayPaymentsRequest { + amount: StringMinorUnit, + card: AmazonpayCard, +} + +#[derive(Default, Debug, Serialize, Eq, PartialEq)] +pub struct AmazonpayCard { + number: cards::CardNumber, + expiry_month: Secret, + expiry_year: Secret, + cvc: Secret, + complete: bool, +} + +impl TryFrom<&AmazonpayRouterData<&PaymentsAuthorizeRouterData>> for AmazonpayPaymentsRequest { + type Error = error_stack::Report; + fn try_from( + item: &AmazonpayRouterData<&PaymentsAuthorizeRouterData>, + ) -> Result { + match item.router_data.request.payment_method_data.clone() { + PaymentMethodData::Card(req_card) => { + let card = AmazonpayCard { + 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.clone(), + card, + }) + } + _ => Err(errors::ConnectorError::NotImplemented("Payment method".to_string()).into()), + } + } +} + +//TODO: Fill the struct with respective fields +// Auth Struct +pub struct AmazonpayAuthType { + pub(super) api_key: Secret, +} + +impl TryFrom<&ConnectorAuthType> for AmazonpayAuthType { + type Error = error_stack::Report; + fn try_from(auth_type: &ConnectorAuthType) -> Result { + match auth_type { + ConnectorAuthType::HeaderKey { api_key } => Ok(Self { + api_key: api_key.to_owned(), + }), + _ => 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 AmazonpayPaymentStatus { + Succeeded, + Failed, + #[default] + Processing, +} + +impl From for common_enums::AttemptStatus { + fn from(item: AmazonpayPaymentStatus) -> Self { + match item { + AmazonpayPaymentStatus::Succeeded => Self::Charged, + AmazonpayPaymentStatus::Failed => Self::Failure, + AmazonpayPaymentStatus::Processing => Self::Authorizing, + } + } +} + +//TODO: Fill the struct with respective fields +#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq)] +pub struct AmazonpayPaymentsResponse { + status: AmazonpayPaymentStatus, + id: String, +} + +impl TryFrom> + for RouterData +{ + type Error = error_stack::Report; + fn try_from( + item: ResponseRouterData, + ) -> Result { + Ok(Self { + status: common_enums::AttemptStatus::from(item.response.status), + response: Ok(PaymentsResponseData::TransactionResponse { + resource_id: ResponseId::ConnectorTransactionId(item.response.id), + redirection_data: Box::new(None), + mandate_reference: Box::new(None), + connector_metadata: None, + network_txn_id: None, + connector_response_reference_id: None, + incremental_authorization_allowed: None, + charge_id: None, + }), + ..item.data + }) + } +} + +//TODO: Fill the struct with respective fields +// REFUND : +// Type definition for RefundRequest +#[derive(Default, Debug, Serialize)] +pub struct AmazonpayRefundRequest { + pub amount: StringMinorUnit, +} + +impl TryFrom<&AmazonpayRouterData<&RefundsRouterData>> for AmazonpayRefundRequest { + type Error = error_stack::Report; + fn try_from(item: &AmazonpayRouterData<&RefundsRouterData>) -> Result { + Ok(Self { + amount: item.amount.to_owned(), + }) + } +} + +// Type definition for Refund Response + +#[allow(dead_code)] +#[derive(Debug, Serialize, Default, Deserialize, Clone)] +pub enum RefundStatus { + Succeeded, + Failed, + #[default] + Processing, +} + +impl From for enums::RefundStatus { + fn from(item: RefundStatus) -> Self { + match item { + RefundStatus::Succeeded => Self::Success, + RefundStatus::Failed => Self::Failure, + RefundStatus::Processing => Self::Pending, + //TODO: Review mapping + } + } +} + +//TODO: Fill the struct with respective fields +#[derive(Default, Debug, Clone, Serialize, Deserialize)] +pub struct RefundResponse { + id: String, + status: RefundStatus, +} + +impl TryFrom> for RefundsRouterData { + type Error = error_stack::Report; + fn try_from( + item: RefundsResponseRouterData, + ) -> Result { + Ok(Self { + response: Ok(RefundsResponseData { + connector_refund_id: item.response.id.to_string(), + refund_status: enums::RefundStatus::from(item.response.status), + }), + ..item.data + }) + } +} + +impl TryFrom> for RefundsRouterData { + type Error = error_stack::Report; + fn try_from( + item: RefundsResponseRouterData, + ) -> Result { + Ok(Self { + response: Ok(RefundsResponseData { + connector_refund_id: item.response.id.to_string(), + refund_status: enums::RefundStatus::from(item.response.status), + }), + ..item.data + }) + } +} + +//TODO: Fill the struct with respective fields +#[derive(Default, Debug, Serialize, Deserialize, PartialEq)] +pub struct AmazonpayErrorResponse { + pub status_code: u16, + pub code: String, + pub message: String, + pub reason: Option, +} diff --git a/crates/hyperswitch_connectors/src/default_implementations.rs b/crates/hyperswitch_connectors/src/default_implementations.rs index fe866d2af7c9..0662b9d8ae2d 100644 --- a/crates/hyperswitch_connectors/src/default_implementations.rs +++ b/crates/hyperswitch_connectors/src/default_implementations.rs @@ -90,6 +90,7 @@ macro_rules! default_imp_for_authorize_session_token { default_imp_for_authorize_session_token!( connectors::Airwallex, + connectors::Amazonpay, connectors::Bambora, connectors::Billwerk, connectors::Bitpay, @@ -144,6 +145,7 @@ macro_rules! default_imp_for_calculate_tax { default_imp_for_calculate_tax!( connectors::Airwallex, + connectors::Amazonpay, connectors::Bambora, connectors::Billwerk, connectors::Bitpay, @@ -198,6 +200,7 @@ macro_rules! default_imp_for_session_update { default_imp_for_session_update!( connectors::Airwallex, + connectors::Amazonpay, connectors::Bambora, connectors::Billwerk, connectors::Bitpay, @@ -253,6 +256,7 @@ macro_rules! default_imp_for_post_session_tokens { default_imp_for_post_session_tokens!( connectors::Airwallex, + connectors::Amazonpay, connectors::Bambora, connectors::Bitpay, connectors::Billwerk, @@ -309,6 +313,7 @@ macro_rules! default_imp_for_complete_authorize { } default_imp_for_complete_authorize!( + connectors::Amazonpay, connectors::Billwerk, connectors::Bitpay, connectors::Cashtocode, @@ -358,6 +363,7 @@ macro_rules! default_imp_for_incremental_authorization { default_imp_for_incremental_authorization!( connectors::Airwallex, + connectors::Amazonpay, connectors::Bambora, connectors::Billwerk, connectors::Bitpay, @@ -414,6 +420,7 @@ macro_rules! default_imp_for_create_customer { default_imp_for_create_customer!( connectors::Airwallex, + connectors::Amazonpay, connectors::Bambora, connectors::Billwerk, connectors::Bitpay, @@ -470,6 +477,7 @@ macro_rules! default_imp_for_connector_redirect_response { } default_imp_for_connector_redirect_response!( + connectors::Amazonpay, connectors::Billwerk, connectors::Bitpay, connectors::Cashtocode, @@ -520,6 +528,7 @@ macro_rules! default_imp_for_pre_processing_steps{ } default_imp_for_pre_processing_steps!( + connectors::Amazonpay, connectors::Bambora, connectors::Billwerk, connectors::Bitpay, @@ -574,6 +583,7 @@ macro_rules! default_imp_for_post_processing_steps{ default_imp_for_post_processing_steps!( connectors::Airwallex, + connectors::Amazonpay, connectors::Bambora, connectors::Billwerk, connectors::Bitpay, @@ -630,6 +640,7 @@ macro_rules! default_imp_for_approve { default_imp_for_approve!( connectors::Airwallex, + connectors::Amazonpay, connectors::Bambora, connectors::Billwerk, connectors::Bitpay, @@ -686,6 +697,7 @@ macro_rules! default_imp_for_reject { default_imp_for_reject!( connectors::Airwallex, + connectors::Amazonpay, connectors::Bambora, connectors::Billwerk, connectors::Bitpay, @@ -742,6 +754,7 @@ macro_rules! default_imp_for_webhook_source_verification { default_imp_for_webhook_source_verification!( connectors::Airwallex, + connectors::Amazonpay, connectors::Bambora, connectors::Billwerk, connectors::Bitpay, @@ -799,6 +812,7 @@ macro_rules! default_imp_for_accept_dispute { default_imp_for_accept_dispute!( connectors::Airwallex, + connectors::Amazonpay, connectors::Bambora, connectors::Billwerk, connectors::Bitpay, @@ -855,6 +869,7 @@ macro_rules! default_imp_for_submit_evidence { default_imp_for_submit_evidence!( connectors::Airwallex, + connectors::Amazonpay, connectors::Bambora, connectors::Billwerk, connectors::Bitpay, @@ -911,6 +926,7 @@ macro_rules! default_imp_for_defend_dispute { default_imp_for_defend_dispute!( connectors::Airwallex, + connectors::Amazonpay, connectors::Bambora, connectors::Billwerk, connectors::Bitpay, @@ -976,6 +992,7 @@ macro_rules! default_imp_for_file_upload { default_imp_for_file_upload!( connectors::Airwallex, + connectors::Amazonpay, connectors::Bambora, connectors::Billwerk, connectors::Bitpay, @@ -1034,6 +1051,7 @@ macro_rules! default_imp_for_payouts_create { #[cfg(feature = "payouts")] default_imp_for_payouts_create!( connectors::Airwallex, + connectors::Amazonpay, connectors::Bambora, connectors::Billwerk, connectors::Bitpay, @@ -1092,6 +1110,7 @@ macro_rules! default_imp_for_payouts_retrieve { #[cfg(feature = "payouts")] default_imp_for_payouts_retrieve!( connectors::Airwallex, + connectors::Amazonpay, connectors::Bambora, connectors::Billwerk, connectors::Bitpay, @@ -1150,6 +1169,7 @@ macro_rules! default_imp_for_payouts_eligibility { #[cfg(feature = "payouts")] default_imp_for_payouts_eligibility!( connectors::Airwallex, + connectors::Amazonpay, connectors::Bambora, connectors::Billwerk, connectors::Bitpay, @@ -1208,6 +1228,7 @@ macro_rules! default_imp_for_payouts_fulfill { #[cfg(feature = "payouts")] default_imp_for_payouts_fulfill!( connectors::Airwallex, + connectors::Amazonpay, connectors::Bambora, connectors::Billwerk, connectors::Bitpay, @@ -1266,6 +1287,7 @@ macro_rules! default_imp_for_payouts_cancel { #[cfg(feature = "payouts")] default_imp_for_payouts_cancel!( connectors::Airwallex, + connectors::Amazonpay, connectors::Bambora, connectors::Billwerk, connectors::Bitpay, @@ -1324,6 +1346,7 @@ macro_rules! default_imp_for_payouts_quote { #[cfg(feature = "payouts")] default_imp_for_payouts_quote!( connectors::Airwallex, + connectors::Amazonpay, connectors::Bambora, connectors::Billwerk, connectors::Bitpay, @@ -1382,6 +1405,7 @@ macro_rules! default_imp_for_payouts_recipient { #[cfg(feature = "payouts")] default_imp_for_payouts_recipient!( connectors::Airwallex, + connectors::Amazonpay, connectors::Bambora, connectors::Billwerk, connectors::Bitpay, @@ -1440,6 +1464,7 @@ macro_rules! default_imp_for_payouts_recipient_account { #[cfg(feature = "payouts")] default_imp_for_payouts_recipient_account!( connectors::Airwallex, + connectors::Amazonpay, connectors::Bambora, connectors::Billwerk, connectors::Bitpay, @@ -1498,6 +1523,7 @@ macro_rules! default_imp_for_frm_sale { #[cfg(feature = "frm")] default_imp_for_frm_sale!( connectors::Airwallex, + connectors::Amazonpay, connectors::Bambora, connectors::Billwerk, connectors::Bitpay, @@ -1556,6 +1582,7 @@ macro_rules! default_imp_for_frm_checkout { #[cfg(feature = "frm")] default_imp_for_frm_checkout!( connectors::Airwallex, + connectors::Amazonpay, connectors::Bambora, connectors::Billwerk, connectors::Bitpay, @@ -1614,6 +1641,7 @@ macro_rules! default_imp_for_frm_transaction { #[cfg(feature = "frm")] default_imp_for_frm_transaction!( connectors::Airwallex, + connectors::Amazonpay, connectors::Bambora, connectors::Billwerk, connectors::Bitpay, @@ -1672,6 +1700,7 @@ macro_rules! default_imp_for_frm_fulfillment { #[cfg(feature = "frm")] default_imp_for_frm_fulfillment!( connectors::Airwallex, + connectors::Amazonpay, connectors::Bambora, connectors::Billwerk, connectors::Bitpay, @@ -1730,6 +1759,7 @@ macro_rules! default_imp_for_frm_record_return { #[cfg(feature = "frm")] default_imp_for_frm_record_return!( connectors::Airwallex, + connectors::Amazonpay, connectors::Bambora, connectors::Billwerk, connectors::Bitpay, @@ -1785,6 +1815,7 @@ macro_rules! default_imp_for_revoking_mandates { default_imp_for_revoking_mandates!( connectors::Airwallex, + connectors::Amazonpay, connectors::Bambora, connectors::Billwerk, connectors::Bitpay, diff --git a/crates/hyperswitch_connectors/src/default_implementations_v2.rs b/crates/hyperswitch_connectors/src/default_implementations_v2.rs index 47cf6b9c1e40..b338c72ded0e 100644 --- a/crates/hyperswitch_connectors/src/default_implementations_v2.rs +++ b/crates/hyperswitch_connectors/src/default_implementations_v2.rs @@ -206,6 +206,7 @@ macro_rules! default_imp_for_new_connector_integration_payment { default_imp_for_new_connector_integration_payment!( connectors::Airwallex, + connectors::Amazonpay, connectors::Bambora, connectors::Billwerk, connectors::Bitpay, @@ -263,6 +264,7 @@ macro_rules! default_imp_for_new_connector_integration_refund { default_imp_for_new_connector_integration_refund!( connectors::Airwallex, + connectors::Amazonpay, connectors::Bambora, connectors::Billwerk, connectors::Bitpay, @@ -315,6 +317,7 @@ macro_rules! default_imp_for_new_connector_integration_connector_access_token { default_imp_for_new_connector_integration_connector_access_token!( connectors::Airwallex, + connectors::Amazonpay, connectors::Bambora, connectors::Billwerk, connectors::Bitpay, @@ -373,6 +376,7 @@ macro_rules! default_imp_for_new_connector_integration_accept_dispute { default_imp_for_new_connector_integration_accept_dispute!( connectors::Airwallex, + connectors::Amazonpay, connectors::Bambora, connectors::Billwerk, connectors::Bitpay, @@ -430,6 +434,7 @@ macro_rules! default_imp_for_new_connector_integration_submit_evidence { default_imp_for_new_connector_integration_submit_evidence!( connectors::Airwallex, + connectors::Amazonpay, connectors::Bambora, connectors::Billwerk, connectors::Bitpay, @@ -487,6 +492,7 @@ macro_rules! default_imp_for_new_connector_integration_defend_dispute { default_imp_for_new_connector_integration_defend_dispute!( connectors::Airwallex, + connectors::Amazonpay, connectors::Bambora, connectors::Billwerk, connectors::Bitpay, @@ -554,6 +560,7 @@ macro_rules! default_imp_for_new_connector_integration_file_upload { default_imp_for_new_connector_integration_file_upload!( connectors::Airwallex, + connectors::Amazonpay, connectors::Bambora, connectors::Billwerk, connectors::Bitpay, @@ -613,6 +620,7 @@ macro_rules! default_imp_for_new_connector_integration_payouts_create { #[cfg(feature = "payouts")] default_imp_for_new_connector_integration_payouts_create!( connectors::Airwallex, + connectors::Amazonpay, connectors::Bambora, connectors::Billwerk, connectors::Bitpay, @@ -672,6 +680,7 @@ macro_rules! default_imp_for_new_connector_integration_payouts_eligibility { #[cfg(feature = "payouts")] default_imp_for_new_connector_integration_payouts_eligibility!( connectors::Airwallex, + connectors::Amazonpay, connectors::Bambora, connectors::Billwerk, connectors::Bitpay, @@ -731,6 +740,7 @@ macro_rules! default_imp_for_new_connector_integration_payouts_fulfill { #[cfg(feature = "payouts")] default_imp_for_new_connector_integration_payouts_fulfill!( connectors::Airwallex, + connectors::Amazonpay, connectors::Bambora, connectors::Billwerk, connectors::Bitpay, @@ -790,6 +800,7 @@ macro_rules! default_imp_for_new_connector_integration_payouts_cancel { #[cfg(feature = "payouts")] default_imp_for_new_connector_integration_payouts_cancel!( connectors::Airwallex, + connectors::Amazonpay, connectors::Bambora, connectors::Billwerk, connectors::Bitpay, @@ -849,6 +860,7 @@ macro_rules! default_imp_for_new_connector_integration_payouts_quote { #[cfg(feature = "payouts")] default_imp_for_new_connector_integration_payouts_quote!( connectors::Airwallex, + connectors::Amazonpay, connectors::Bambora, connectors::Billwerk, connectors::Bitpay, @@ -908,6 +920,7 @@ macro_rules! default_imp_for_new_connector_integration_payouts_recipient { #[cfg(feature = "payouts")] default_imp_for_new_connector_integration_payouts_recipient!( connectors::Airwallex, + connectors::Amazonpay, connectors::Bambora, connectors::Billwerk, connectors::Bitpay, @@ -967,6 +980,7 @@ macro_rules! default_imp_for_new_connector_integration_payouts_sync { #[cfg(feature = "payouts")] default_imp_for_new_connector_integration_payouts_sync!( connectors::Airwallex, + connectors::Amazonpay, connectors::Bambora, connectors::Billwerk, connectors::Bitpay, @@ -1026,6 +1040,7 @@ macro_rules! default_imp_for_new_connector_integration_payouts_recipient_account #[cfg(feature = "payouts")] default_imp_for_new_connector_integration_payouts_recipient_account!( connectors::Airwallex, + connectors::Amazonpay, connectors::Bambora, connectors::Billwerk, connectors::Bitpay, @@ -1083,6 +1098,7 @@ macro_rules! default_imp_for_new_connector_integration_webhook_source_verificati default_imp_for_new_connector_integration_webhook_source_verification!( connectors::Airwallex, + connectors::Amazonpay, connectors::Bambora, connectors::Billwerk, connectors::Bitpay, @@ -1142,6 +1158,7 @@ macro_rules! default_imp_for_new_connector_integration_frm_sale { #[cfg(feature = "frm")] default_imp_for_new_connector_integration_frm_sale!( connectors::Airwallex, + connectors::Amazonpay, connectors::Bambora, connectors::Billwerk, connectors::Bitpay, @@ -1201,6 +1218,7 @@ macro_rules! default_imp_for_new_connector_integration_frm_checkout { #[cfg(feature = "frm")] default_imp_for_new_connector_integration_frm_checkout!( connectors::Airwallex, + connectors::Amazonpay, connectors::Bambora, connectors::Billwerk, connectors::Bitpay, @@ -1260,6 +1278,7 @@ macro_rules! default_imp_for_new_connector_integration_frm_transaction { #[cfg(feature = "frm")] default_imp_for_new_connector_integration_frm_transaction!( connectors::Airwallex, + connectors::Amazonpay, connectors::Bambora, connectors::Billwerk, connectors::Bitpay, @@ -1319,6 +1338,7 @@ macro_rules! default_imp_for_new_connector_integration_frm_fulfillment { #[cfg(feature = "frm")] default_imp_for_new_connector_integration_frm_fulfillment!( connectors::Airwallex, + connectors::Amazonpay, connectors::Bambora, connectors::Billwerk, connectors::Bitpay, @@ -1378,6 +1398,7 @@ macro_rules! default_imp_for_new_connector_integration_frm_record_return { #[cfg(feature = "frm")] default_imp_for_new_connector_integration_frm_record_return!( connectors::Airwallex, + connectors::Amazonpay, connectors::Bambora, connectors::Billwerk, connectors::Bitpay, @@ -1434,6 +1455,7 @@ macro_rules! default_imp_for_new_connector_integration_revoking_mandates { default_imp_for_new_connector_integration_revoking_mandates!( connectors::Airwallex, + connectors::Amazonpay, connectors::Bambora, connectors::Billwerk, connectors::Bitpay, diff --git a/crates/hyperswitch_interfaces/src/configs.rs b/crates/hyperswitch_interfaces/src/configs.rs index 2c631849446d..0ac433e53dbd 100644 --- a/crates/hyperswitch_interfaces/src/configs.rs +++ b/crates/hyperswitch_interfaces/src/configs.rs @@ -13,6 +13,7 @@ pub struct Connectors { pub adyen: AdyenParamsWithThreeBaseUrls, pub adyenplatform: ConnectorParams, pub airwallex: ConnectorParams, + pub amazonpay: ConnectorParams, pub applepay: ConnectorParams, pub authorizedotnet: ConnectorParams, pub bambora: ConnectorParams, diff --git a/crates/router/src/connector.rs b/crates/router/src/connector.rs index c35856354de0..b240899c54bc 100644 --- a/crates/router/src/connector.rs +++ b/crates/router/src/connector.rs @@ -45,18 +45,18 @@ pub mod wellsfargopayout; pub mod wise; pub use hyperswitch_connectors::connectors::{ - airwallex, airwallex::Airwallex, bambora, bambora::Bambora, billwerk, billwerk::Billwerk, - bitpay, bitpay::Bitpay, cashtocode, cashtocode::Cashtocode, coinbase, coinbase::Coinbase, - cryptopay, cryptopay::Cryptopay, deutschebank, deutschebank::Deutschebank, digitalvirgo, - digitalvirgo::Digitalvirgo, dlocal, dlocal::Dlocal, elavon, elavon::Elavon, fiserv, - fiserv::Fiserv, fiservemea, fiservemea::Fiservemea, fiuu, fiuu::Fiuu, forte, forte::Forte, - globepay, globepay::Globepay, helcim, helcim::Helcim, jpmorgan, jpmorgan::Jpmorgan, mollie, - mollie::Mollie, multisafepay, multisafepay::Multisafepay, nexinets, nexinets::Nexinets, - nexixpay, nexixpay::Nexixpay, novalnet, novalnet::Novalnet, payeezy, payeezy::Payeezy, payu, - payu::Payu, powertranz, powertranz::Powertranz, razorpay, razorpay::Razorpay, shift4, - shift4::Shift4, square, square::Square, stax, stax::Stax, taxjar, taxjar::Taxjar, thunes, - thunes::Thunes, tsys, tsys::Tsys, volt, volt::Volt, worldline, worldline::Worldline, worldpay, - worldpay::Worldpay, zen, zen::Zen, zsl, zsl::Zsl, + airwallex, airwallex::Airwallex, amazonpay, amazonpay::Amazonpay, bambora, bambora::Bambora, + billwerk, billwerk::Billwerk, bitpay, bitpay::Bitpay, cashtocode, cashtocode::Cashtocode, + coinbase, coinbase::Coinbase, cryptopay, cryptopay::Cryptopay, deutschebank, + deutschebank::Deutschebank, digitalvirgo, digitalvirgo::Digitalvirgo, dlocal, dlocal::Dlocal, + elavon, elavon::Elavon, fiserv, fiserv::Fiserv, fiservemea, fiservemea::Fiservemea, fiuu, + fiuu::Fiuu, forte, forte::Forte, globepay, globepay::Globepay, helcim, helcim::Helcim, + jpmorgan, jpmorgan::Jpmorgan, mollie, mollie::Mollie, multisafepay, multisafepay::Multisafepay, + nexinets, nexinets::Nexinets, nexixpay, nexixpay::Nexixpay, novalnet, novalnet::Novalnet, + payeezy, payeezy::Payeezy, payu, payu::Payu, powertranz, powertranz::Powertranz, razorpay, + razorpay::Razorpay, shift4, shift4::Shift4, square, square::Square, stax, stax::Stax, taxjar, + taxjar::Taxjar, thunes, thunes::Thunes, tsys, tsys::Tsys, volt, volt::Volt, worldline, + worldline::Worldline, worldpay, worldpay::Worldpay, zen, zen::Zen, zsl, zsl::Zsl, }; #[cfg(feature = "dummy_connector")] diff --git a/crates/router/src/core/payments/connector_integration_v2_impls.rs b/crates/router/src/core/payments/connector_integration_v2_impls.rs index 10ed62c70c53..5d6915316331 100644 --- a/crates/router/src/core/payments/connector_integration_v2_impls.rs +++ b/crates/router/src/core/payments/connector_integration_v2_impls.rs @@ -1111,6 +1111,7 @@ default_imp_for_new_connector_integration_payouts!( connector::Adyen, connector::Adyenplatform, connector::Airwallex, + connector::Amazonpay, connector::Authorizedotnet, connector::Bambora, connector::Bamboraapac, @@ -1756,6 +1757,7 @@ default_imp_for_new_connector_integration_frm!( connector::Adyen, connector::Adyenplatform, connector::Airwallex, + connector::Amazonpay, connector::Authorizedotnet, connector::Bambora, connector::Bamboraapac, @@ -2249,6 +2251,7 @@ default_imp_for_new_connector_integration_connector_authentication!( connector::Adyen, connector::Adyenplatform, connector::Airwallex, + connector::Amazonpay, connector::Authorizedotnet, connector::Bambora, connector::Bamboraapac, diff --git a/crates/router/src/core/payments/flows.rs b/crates/router/src/core/payments/flows.rs index b28f39a51de1..3fed81a890f7 100644 --- a/crates/router/src/core/payments/flows.rs +++ b/crates/router/src/core/payments/flows.rs @@ -452,6 +452,7 @@ default_imp_for_connector_request_id!( connector::Aci, connector::Adyen, connector::Airwallex, + connector::Amazonpay, connector::Authorizedotnet, connector::Bambora, connector::Bamboraapac, @@ -978,6 +979,7 @@ impl api::Payouts for connector::DummyConnector {} default_imp_for_payouts!( connector::Aci, connector::Airwallex, + connector::Amazonpay, connector::Authorizedotnet, connector::Bambora, connector::Bamboraapac, @@ -1772,6 +1774,7 @@ default_imp_for_fraud_check!( connector::Aci, connector::Adyen, connector::Airwallex, + connector::Amazonpay, connector::Authorizedotnet, connector::Bambora, connector::Bamboraapac, @@ -2434,6 +2437,7 @@ default_imp_for_connector_authentication!( connector::Aci, connector::Adyen, connector::Airwallex, + connector::Amazonpay, connector::Authorizedotnet, connector::Bambora, connector::Bamboraapac, diff --git a/crates/router/src/types/api.rs b/crates/router/src/types/api.rs index 361fd93d80b0..6db9d29daabd 100644 --- a/crates/router/src/types/api.rs +++ b/crates/router/src/types/api.rs @@ -326,6 +326,9 @@ impl ConnectorData { enums::Connector::Airwallex => { Ok(ConnectorEnum::Old(Box::new(&connector::Airwallex))) } + // enums::Connector::Amazonpay => { + // Ok(ConnectorEnum::Old(Box::new(connector::Amazonpay))) + // } enums::Connector::Authorizedotnet => { Ok(ConnectorEnum::Old(Box::new(&connector::Authorizedotnet))) } diff --git a/crates/router/src/types/transformers.rs b/crates/router/src/types/transformers.rs index bc72431bd9c8..2507fa6170aa 100644 --- a/crates/router/src/types/transformers.rs +++ b/crates/router/src/types/transformers.rs @@ -247,6 +247,7 @@ impl ForeignTryFrom for common_enums::RoutableConnectors { api_enums::Connector::Adyen => Self::Adyen, api_enums::Connector::Adyenplatform => Self::Adyenplatform, api_enums::Connector::Airwallex => Self::Airwallex, + // api_enums::Connector::Amazonpay => Self::Amazonpay, api_enums::Connector::Authorizedotnet => Self::Authorizedotnet, api_enums::Connector::Bambora => Self::Bambora, api_enums::Connector::Bamboraapac => Self::Bamboraapac, diff --git a/crates/router/tests/connectors/amazonpay.rs b/crates/router/tests/connectors/amazonpay.rs new file mode 100644 index 000000000000..36ebb3178f4a --- /dev/null +++ b/crates/router/tests/connectors/amazonpay.rs @@ -0,0 +1,421 @@ +use hyperswitch_domain_models::payment_method_data::{Card, PaymentMethodData}; +use masking::Secret; +use router::types::{self, api, storage::enums}; +use test_utils::connector_auth; + +use crate::utils::{self, ConnectorActions}; + +#[derive(Clone, Copy)] +struct AmazonpayTest; +impl ConnectorActions for AmazonpayTest {} +impl utils::Connector for AmazonpayTest { + fn get_data(&self) -> api::ConnectorData { + use router::connector::Amazonpay; + utils::construct_connector_data_old( + Box::new(Amazonpay::new()), + types::Connector::Plaid, + api::GetToken::Connector, + None, + ) + } + + fn get_auth_token(&self) -> types::ConnectorAuthType { + utils::to_connector_auth_type( + connector_auth::ConnectorAuthentication::new() + .amazonpay + .expect("Missing connector authentication configuration") + .into(), + ) + } + + fn get_name(&self) -> String { + "amazonpay".to_string() + } +} + +static CONNECTOR: AmazonpayTest = AmazonpayTest {}; + +fn get_default_payment_info() -> Option { + None +} + +fn payment_method_details() -> Option { + None +} + +// Cards Positive Tests +// Creates a payment using the manual capture flow (Non 3DS). +#[actix_web::test] +async fn should_only_authorize_payment() { + let response = CONNECTOR + .authorize_payment(payment_method_details(), get_default_payment_info()) + .await + .expect("Authorize payment response"); + assert_eq!(response.status, enums::AttemptStatus::Authorized); +} + +// Captures a payment using the manual capture flow (Non 3DS). +#[actix_web::test] +async fn should_capture_authorized_payment() { + let response = CONNECTOR + .authorize_and_capture_payment(payment_method_details(), None, get_default_payment_info()) + .await + .expect("Capture payment response"); + assert_eq!(response.status, enums::AttemptStatus::Charged); +} + +// Partially captures a payment using the manual capture flow (Non 3DS). +#[actix_web::test] +async fn should_partially_capture_authorized_payment() { + let response = CONNECTOR + .authorize_and_capture_payment( + payment_method_details(), + Some(types::PaymentsCaptureData { + amount_to_capture: 50, + ..utils::PaymentCaptureType::default().0 + }), + get_default_payment_info(), + ) + .await + .expect("Capture payment response"); + assert_eq!(response.status, enums::AttemptStatus::Charged); +} + +// Synchronizes a payment using the manual capture flow (Non 3DS). +#[actix_web::test] +async fn should_sync_authorized_payment() { + let authorize_response = CONNECTOR + .authorize_payment(payment_method_details(), get_default_payment_info()) + .await + .expect("Authorize payment response"); + let txn_id = utils::get_connector_transaction_id(authorize_response.response); + let response = CONNECTOR + .psync_retry_till_status_matches( + enums::AttemptStatus::Authorized, + Some(types::PaymentsSyncData { + connector_transaction_id: types::ResponseId::ConnectorTransactionId( + txn_id.unwrap(), + ), + ..Default::default() + }), + get_default_payment_info(), + ) + .await + .expect("PSync response"); + assert_eq!(response.status, enums::AttemptStatus::Authorized,); +} + +// Voids a payment using the manual capture flow (Non 3DS). +#[actix_web::test] +async fn should_void_authorized_payment() { + let response = CONNECTOR + .authorize_and_void_payment( + payment_method_details(), + Some(types::PaymentsCancelData { + connector_transaction_id: String::from(""), + cancellation_reason: Some("requested_by_customer".to_string()), + ..Default::default() + }), + get_default_payment_info(), + ) + .await + .expect("Void payment response"); + assert_eq!(response.status, enums::AttemptStatus::Voided); +} + +// Refunds a payment using the manual capture flow (Non 3DS). +#[actix_web::test] +async fn should_refund_manually_captured_payment() { + let response = CONNECTOR + .capture_payment_and_refund( + payment_method_details(), + None, + None, + get_default_payment_info(), + ) + .await + .unwrap(); + assert_eq!( + response.response.unwrap().refund_status, + enums::RefundStatus::Success, + ); +} + +// Partially refunds a payment using the manual capture flow (Non 3DS). +#[actix_web::test] +async fn should_partially_refund_manually_captured_payment() { + let response = CONNECTOR + .capture_payment_and_refund( + payment_method_details(), + None, + Some(types::RefundsData { + refund_amount: 50, + ..utils::PaymentRefundType::default().0 + }), + get_default_payment_info(), + ) + .await + .unwrap(); + assert_eq!( + response.response.unwrap().refund_status, + enums::RefundStatus::Success, + ); +} + +// Synchronizes a refund using the manual capture flow (Non 3DS). +#[actix_web::test] +async fn should_sync_manually_captured_refund() { + let refund_response = CONNECTOR + .capture_payment_and_refund( + payment_method_details(), + None, + None, + get_default_payment_info(), + ) + .await + .unwrap(); + let response = CONNECTOR + .rsync_retry_till_status_matches( + enums::RefundStatus::Success, + refund_response.response.unwrap().connector_refund_id, + None, + get_default_payment_info(), + ) + .await + .unwrap(); + assert_eq!( + response.response.unwrap().refund_status, + enums::RefundStatus::Success, + ); +} + +// Creates a payment using the automatic capture flow (Non 3DS). +#[actix_web::test] +async fn should_make_payment() { + let authorize_response = CONNECTOR + .make_payment(payment_method_details(), get_default_payment_info()) + .await + .unwrap(); + assert_eq!(authorize_response.status, enums::AttemptStatus::Charged); +} + +// Synchronizes a payment using the automatic capture flow (Non 3DS). +#[actix_web::test] +async fn should_sync_auto_captured_payment() { + let authorize_response = CONNECTOR + .make_payment(payment_method_details(), get_default_payment_info()) + .await + .unwrap(); + assert_eq!(authorize_response.status, enums::AttemptStatus::Charged); + let txn_id = utils::get_connector_transaction_id(authorize_response.response); + assert_ne!(txn_id, None, "Empty connector transaction id"); + let response = CONNECTOR + .psync_retry_till_status_matches( + enums::AttemptStatus::Charged, + Some(types::PaymentsSyncData { + connector_transaction_id: types::ResponseId::ConnectorTransactionId( + txn_id.unwrap(), + ), + capture_method: Some(enums::CaptureMethod::Automatic), + ..Default::default() + }), + get_default_payment_info(), + ) + .await + .unwrap(); + assert_eq!(response.status, enums::AttemptStatus::Charged,); +} + +// Refunds a payment using the automatic capture flow (Non 3DS). +#[actix_web::test] +async fn should_refund_auto_captured_payment() { + let response = CONNECTOR + .make_payment_and_refund(payment_method_details(), None, get_default_payment_info()) + .await + .unwrap(); + assert_eq!( + response.response.unwrap().refund_status, + enums::RefundStatus::Success, + ); +} + +// Partially refunds a payment using the automatic capture flow (Non 3DS). +#[actix_web::test] +async fn should_partially_refund_succeeded_payment() { + let refund_response = CONNECTOR + .make_payment_and_refund( + payment_method_details(), + Some(types::RefundsData { + refund_amount: 50, + ..utils::PaymentRefundType::default().0 + }), + get_default_payment_info(), + ) + .await + .unwrap(); + assert_eq!( + refund_response.response.unwrap().refund_status, + enums::RefundStatus::Success, + ); +} + +// Creates multiple refunds against a payment using the automatic capture flow (Non 3DS). +#[actix_web::test] +async fn should_refund_succeeded_payment_multiple_times() { + CONNECTOR + .make_payment_and_multiple_refund( + payment_method_details(), + Some(types::RefundsData { + refund_amount: 50, + ..utils::PaymentRefundType::default().0 + }), + get_default_payment_info(), + ) + .await; +} + +// Synchronizes a refund using the automatic capture flow (Non 3DS). +#[actix_web::test] +async fn should_sync_refund() { + let refund_response = CONNECTOR + .make_payment_and_refund(payment_method_details(), None, get_default_payment_info()) + .await + .unwrap(); + let response = CONNECTOR + .rsync_retry_till_status_matches( + enums::RefundStatus::Success, + refund_response.response.unwrap().connector_refund_id, + None, + get_default_payment_info(), + ) + .await + .unwrap(); + assert_eq!( + response.response.unwrap().refund_status, + enums::RefundStatus::Success, + ); +} + +// Cards Negative scenarios +// Creates a payment with incorrect CVC. +#[actix_web::test] +async fn should_fail_payment_for_incorrect_cvc() { + let response = CONNECTOR + .make_payment( + Some(types::PaymentsAuthorizeData { + payment_method_data: PaymentMethodData::Card(Card { + card_cvc: Secret::new("12345".to_string()), + ..utils::CCardType::default().0 + }), + ..utils::PaymentAuthorizeType::default().0 + }), + get_default_payment_info(), + ) + .await + .unwrap(); + assert_eq!( + response.response.unwrap_err().message, + "Your card's security code is invalid.".to_string(), + ); +} + +// Creates a payment with incorrect expiry month. +#[actix_web::test] +async fn should_fail_payment_for_invalid_exp_month() { + let response = CONNECTOR + .make_payment( + Some(types::PaymentsAuthorizeData { + payment_method_data: PaymentMethodData::Card(Card { + card_exp_month: Secret::new("20".to_string()), + ..utils::CCardType::default().0 + }), + ..utils::PaymentAuthorizeType::default().0 + }), + get_default_payment_info(), + ) + .await + .unwrap(); + assert_eq!( + response.response.unwrap_err().message, + "Your card's expiration month is invalid.".to_string(), + ); +} + +// Creates a payment with incorrect expiry year. +#[actix_web::test] +async fn should_fail_payment_for_incorrect_expiry_year() { + let response = CONNECTOR + .make_payment( + Some(types::PaymentsAuthorizeData { + payment_method_data: PaymentMethodData::Card(Card { + card_exp_year: Secret::new("2000".to_string()), + ..utils::CCardType::default().0 + }), + ..utils::PaymentAuthorizeType::default().0 + }), + get_default_payment_info(), + ) + .await + .unwrap(); + assert_eq!( + response.response.unwrap_err().message, + "Your card's expiration year is invalid.".to_string(), + ); +} + +// Voids a payment using automatic capture flow (Non 3DS). +#[actix_web::test] +async fn should_fail_void_payment_for_auto_capture() { + let authorize_response = CONNECTOR + .make_payment(payment_method_details(), get_default_payment_info()) + .await + .unwrap(); + assert_eq!(authorize_response.status, enums::AttemptStatus::Charged); + let txn_id = utils::get_connector_transaction_id(authorize_response.response); + assert_ne!(txn_id, None, "Empty connector transaction id"); + let void_response = CONNECTOR + .void_payment(txn_id.unwrap(), None, get_default_payment_info()) + .await + .unwrap(); + assert_eq!( + void_response.response.unwrap_err().message, + "You cannot cancel this PaymentIntent because it has a status of succeeded." + ); +} + +// Captures a payment using invalid connector payment id. +#[actix_web::test] +async fn should_fail_capture_for_invalid_payment() { + let capture_response = CONNECTOR + .capture_payment("123456789".to_string(), None, get_default_payment_info()) + .await + .unwrap(); + assert_eq!( + capture_response.response.unwrap_err().message, + String::from("No such payment_intent: '123456789'") + ); +} + +// Refunds a payment with refund amount higher than payment amount. +#[actix_web::test] +async fn should_fail_for_refund_amount_higher_than_payment_amount() { + let response = CONNECTOR + .make_payment_and_refund( + payment_method_details(), + Some(types::RefundsData { + refund_amount: 150, + ..utils::PaymentRefundType::default().0 + }), + get_default_payment_info(), + ) + .await + .unwrap(); + assert_eq!( + response.response.unwrap_err().message, + "Refund amount (₹1.50) is greater than charge amount (₹1.00)", + ); +} + +// Connector dependent test cases goes here + +// [#478]: add unit tests for non 3DS, wallets & webhooks in connector tests diff --git a/crates/router/tests/connectors/main.rs b/crates/router/tests/connectors/main.rs index c2a0d75411be..19e3971cbb9e 100644 --- a/crates/router/tests/connectors/main.rs +++ b/crates/router/tests/connectors/main.rs @@ -10,6 +10,7 @@ mod aci; mod adyen; mod adyenplatform; mod airwallex; +mod amazonpay; mod authorizedotnet; mod bambora; mod bamboraapac; diff --git a/crates/router/tests/connectors/sample_auth.toml b/crates/router/tests/connectors/sample_auth.toml index caaafcdb6cf5..f36436b6ccb3 100644 --- a/crates/router/tests/connectors/sample_auth.toml +++ b/crates/router/tests/connectors/sample_auth.toml @@ -10,6 +10,9 @@ api_key = "Bearer MyApiKey" key1 = "MerchantId" api_secret = "Secondary key" +[amazonpay] +api_key="API Key" + [authorizedotnet] api_key = "MyMerchantName" key1 = "MyTransactionKey" diff --git a/crates/test_utils/src/connector_auth.rs b/crates/test_utils/src/connector_auth.rs index 7508c76048c8..d23560300f34 100644 --- a/crates/test_utils/src/connector_auth.rs +++ b/crates/test_utils/src/connector_auth.rs @@ -17,6 +17,7 @@ pub struct ConnectorAuthentication { #[cfg(feature = "payouts")] pub adyen_uk: Option, pub airwallex: Option, + pub amazonpay: Option, pub authorizedotnet: Option, pub bambora: Option, pub bamboraapac: Option, diff --git a/loadtest/config/development.toml b/loadtest/config/development.toml index 723498f7e7a1..6ef82db0f211 100644 --- a/loadtest/config/development.toml +++ b/loadtest/config/development.toml @@ -79,6 +79,7 @@ adyenplatform.base_url = "https://balanceplatform-api-test.adyen.com/" adyen.payout_base_url = "https://pal-test.adyen.com/" adyen.dispute_base_url = "https://ca-test.adyen.com/" airwallex.base_url = "https://api-demo.airwallex.com/" +amazonpay.base_url = "https://pay-api.amazon.com/v2" applepay.base_url = "https://apple-pay-gateway.apple.com/" authorizedotnet.base_url = "https://apitest.authorize.net/xml/v1/request.api" bambora.base_url = "https://api.na.bambora.com" @@ -181,6 +182,7 @@ cards = [ "adyen", "adyenplatform", "airwallex", + "amazonpay", "authorizedotnet", "bambora", "bamboraapac", diff --git a/scripts/add_connector.sh b/scripts/add_connector.sh index 7e19844ad4d2..1915cd65cdd6 100755 --- a/scripts/add_connector.sh +++ b/scripts/add_connector.sh @@ -6,7 +6,7 @@ function find_prev_connector() { git checkout $self cp $self $self.tmp # Add new connector to existing list and sort it - connectors=(aci adyen adyenplatform airwallex applepay authorizedotnet bambora bamboraapac bankofamerica billwerk bitpay bluesnap boku braintree cashtocode checkout coinbase cryptopay cybersource datatrans deutschebank digitalvirgo dlocal dummyconnector ebanx elavon fiserv fiservemea fiuu forte globalpay globepay gocardless gpayments helcim iatapay itaubank jpmorgan klarna mifinity mollie multisafepay netcetera nexinets nexixpay noon novalnet nuvei opayo opennode paybox payeezy payme payone paypal payu placetopay plaid powertranz prophetpay rapyd razorpay shift4 square stax stripe taxjar threedsecureio thunes trustpay tsys volt wellsfargo wellsfargopayout wise worldline worldpay zsl "$1") + connectors=(aci adyen adyenplatform airwallex amazonpay applepay authorizedotnet bambora bamboraapac bankofamerica billwerk bitpay bluesnap boku braintree cashtocode checkout coinbase cryptopay cybersource datatrans deutschebank digitalvirgo dlocal dummyconnector ebanx elavon fiserv fiservemea fiuu forte globalpay globepay gocardless gpayments helcim iatapay itaubank jpmorgan klarna mifinity mollie multisafepay netcetera nexinets nexixpay noon novalnet nuvei opayo opennode paybox payeezy payme payone paypal payu placetopay plaid powertranz prophetpay rapyd razorpay shift4 square stax stripe taxjar threedsecureio thunes trustpay tsys volt wellsfargo wellsfargopayout wise worldline worldpay zsl "$1") IFS=$'\n' sorted=($(sort <<<"${connectors[*]}")); unset IFS res="$(echo ${sorted[@]})" sed -i'' -e "s/^ connectors=.*/ connectors=($res \"\$1\")/" $self.tmp From 51b6cdfad76027f96df1e9f72b4b40ca6f2194c0 Mon Sep 17 00:00:00 2001 From: Shankar Singh C <83439957+ShankarSinghC@users.noreply.github.com> Date: Fri, 8 Nov 2024 16:58:46 +0530 Subject: [PATCH 12/25] fix(router): get apple pay certificates only from metadata during the session call (#6514) Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com> --- .../src/core/payments/flows/session_flow.rs | 19 +++-------- crates/router/src/core/payments/helpers.rs | 32 ++++--------------- 2 files changed, 11 insertions(+), 40 deletions(-) diff --git a/crates/router/src/core/payments/flows/session_flow.rs b/crates/router/src/core/payments/flows/session_flow.rs index ba8054696a11..545d05e776fa 100644 --- a/crates/router/src/core/payments/flows/session_flow.rs +++ b/crates/router/src/core/payments/flows/session_flow.rs @@ -189,20 +189,11 @@ async fn create_applepay_session_token( ) } else { // Get the apple pay metadata - let connector_apple_pay_wallet_details = - helpers::get_applepay_metadata(router_data.connector_wallets_details.clone()) - .map_err(|error| { - logger::debug!( - "Apple pay connector wallets details parsing failed in create_applepay_session_token {:?}", - error - ) - }) - .ok(); - - let apple_pay_metadata = match connector_apple_pay_wallet_details { - Some(apple_pay_wallet_details) => apple_pay_wallet_details, - None => helpers::get_applepay_metadata(router_data.connector_meta_data.clone())?, - }; + let apple_pay_metadata = + helpers::get_applepay_metadata(router_data.connector_meta_data.clone()) + .attach_printable( + "Failed to to fetch apple pay certificates during session call", + )?; // Get payment request data , apple pay session request and merchant keys let ( diff --git a/crates/router/src/core/payments/helpers.rs b/crates/router/src/core/payments/helpers.rs index 5a8bc5cd12bd..acaccccbb17b 100644 --- a/crates/router/src/core/payments/helpers.rs +++ b/crates/router/src/core/payments/helpers.rs @@ -4788,32 +4788,17 @@ pub fn validate_customer_access( pub fn is_apple_pay_simplified_flow( connector_metadata: Option, - connector_wallets_details: Option, connector_name: Option<&String>, ) -> CustomResult { - let connector_apple_pay_wallet_details = - get_applepay_metadata(connector_wallets_details) - .map_err(|error| { - logger::debug!( - "Apple pay connector wallets details parsing failed for {:?} in is_apple_pay_simplified_flow {:?}", - connector_name, - error - ) - }) - .ok(); - - let option_apple_pay_metadata = match connector_apple_pay_wallet_details { - Some(apple_pay_wallet_details) => Some(apple_pay_wallet_details), - None => get_applepay_metadata(connector_metadata) - .map_err(|error| { - logger::debug!( - "Apple pay metadata parsing failed for {:?} in is_apple_pay_simplified_flow {:?}", + let option_apple_pay_metadata = get_applepay_metadata(connector_metadata) + .map_err(|error| { + logger::info!( + "Apple pay metadata parsing for {:?} in is_apple_pay_simplified_flow {:?}", connector_name, error ) - }) - .ok(), - }; + }) + .ok(); // return true only if the apple flow type is simplified Ok(matches!( @@ -4999,7 +4984,6 @@ where let connector_data_list = if is_apple_pay_simplified_flow( merchant_connector_account_type.get_metadata(), - merchant_connector_account_type.get_connector_wallets_details(), merchant_connector_account_type .get_connector_name() .as_ref(), @@ -5027,10 +5011,6 @@ where for merchant_connector_account in profile_specific_merchant_connector_account_list { if is_apple_pay_simplified_flow( merchant_connector_account.metadata.clone(), - merchant_connector_account - .connector_wallets_details - .as_deref() - .cloned(), Some(&merchant_connector_account.connector_name), )? { let connector_data = api::ConnectorData::get_connector_by_name( From 860a57ad9a679056ac66423edfc16973f497e184 Mon Sep 17 00:00:00 2001 From: Debarati Ghatak <88573135+cookieg13@users.noreply.github.com> Date: Fri, 8 Nov 2024 17:23:28 +0530 Subject: [PATCH 13/25] fix(connector): [Novalnet] Send decoded wallet token to applepay (#6503) --- .../src/connectors/novalnet/transformers.rs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/crates/hyperswitch_connectors/src/connectors/novalnet/transformers.rs b/crates/hyperswitch_connectors/src/connectors/novalnet/transformers.rs index 26364d73f4ff..313e35eeb0e6 100644 --- a/crates/hyperswitch_connectors/src/connectors/novalnet/transformers.rs +++ b/crates/hyperswitch_connectors/src/connectors/novalnet/transformers.rs @@ -32,8 +32,9 @@ use strum::Display; use crate::{ types::{RefundsResponseRouterData, ResponseRouterData}, utils::{ - self, BrowserInformationData, PaymentsAuthorizeRequestData, PaymentsCancelRequestData, - PaymentsCaptureRequestData, PaymentsSyncRequestData, RefundsRequestData, RouterData as _, + self, ApplePay, BrowserInformationData, PaymentsAuthorizeRequestData, + PaymentsCancelRequestData, PaymentsCaptureRequestData, PaymentsSyncRequestData, + RefundsRequestData, RouterData as _, }, }; @@ -298,7 +299,8 @@ impl TryFrom<&NovalnetRouterData<&PaymentsAuthorizeRouterData>> for NovalnetPaym return_url: None, error_return_url: None, payment_data: Some(NovalNetPaymentData::ApplePay(NovalnetApplePay { - wallet_data: Secret::new(payment_method_data.payment_data.clone()), + wallet_data: payment_method_data + .get_applepay_decoded_payment_data()?, })), enforce_3d: None, create_token, From 21d3071f317e153f9ff83446c29b0f88c4bbd973 Mon Sep 17 00:00:00 2001 From: Anurag Thakur Date: Fri, 8 Nov 2024 17:28:42 +0530 Subject: [PATCH 14/25] fix(docs): Fix broken pages in API reference (#6507) --- api-reference/api-reference/api-key/api-key--create.mdx | 2 +- api-reference/api-reference/api-key/api-key--revoke.mdx | 2 +- .../api-reference/routing/routing--activate-config.mdx | 2 +- api-reference/api-reference/routing/routing--retrieve.mdx | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/api-reference/api-reference/api-key/api-key--create.mdx b/api-reference/api-reference/api-key/api-key--create.mdx index 977d4b928518..663ec4e66458 100644 --- a/api-reference/api-reference/api-key/api-key--create.mdx +++ b/api-reference/api-reference/api-key/api-key--create.mdx @@ -1,3 +1,3 @@ --- -openapi: post /api_keys/{merchant_id) +openapi: post /api_keys/{merchant_id} --- \ No newline at end of file diff --git a/api-reference/api-reference/api-key/api-key--revoke.mdx b/api-reference/api-reference/api-key/api-key--revoke.mdx index d95f088533e5..62bbf0203219 100644 --- a/api-reference/api-reference/api-key/api-key--revoke.mdx +++ b/api-reference/api-reference/api-key/api-key--revoke.mdx @@ -1,3 +1,3 @@ --- -openapi: delete /api_keys/{merchant_id)/{key_id} +openapi: delete /api_keys/{merchant_id}/{key_id} --- \ No newline at end of file diff --git a/api-reference/api-reference/routing/routing--activate-config.mdx b/api-reference/api-reference/routing/routing--activate-config.mdx index 990723a90c71..12e39e519e90 100644 --- a/api-reference/api-reference/routing/routing--activate-config.mdx +++ b/api-reference/api-reference/routing/routing--activate-config.mdx @@ -1,3 +1,3 @@ --- -openapi: post /routing/{algorithm_id}/activate +openapi: post /routing/{routing_algorithm_id}/activate --- \ No newline at end of file diff --git a/api-reference/api-reference/routing/routing--retrieve.mdx b/api-reference/api-reference/routing/routing--retrieve.mdx index f39e44c824c3..3c5d31cb2fad 100644 --- a/api-reference/api-reference/routing/routing--retrieve.mdx +++ b/api-reference/api-reference/routing/routing--retrieve.mdx @@ -1,3 +1,3 @@ --- -openapi: get /routing/{algorithm_id} +openapi: get /routing/{routing_algorithm_id} --- \ No newline at end of file From d9ce42fd0cecb1eda196071da925f4f0e75a834f Mon Sep 17 00:00:00 2001 From: Prajjwal Kumar Date: Fri, 8 Nov 2024 17:29:44 +0530 Subject: [PATCH 15/25] Refactor(core): interpolate success_based_routing config params with their specific values (#6448) Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com> --- crates/api_models/src/routing.rs | 5 +- crates/external_services/src/grpc_client.rs | 3 - .../src/grpc_client/dynamic_routing.rs | 38 ++------ crates/router/src/core/errors.rs | 2 + crates/router/src/core/payments.rs | 46 ++++++++- .../payments/operations/payment_response.rs | 36 ++++++- crates/router/src/core/payments/routing.rs | 10 ++ crates/router/src/core/routing/helpers.rs | 94 ++++++++++++++++++- 8 files changed, 188 insertions(+), 46 deletions(-) diff --git a/crates/api_models/src/routing.rs b/crates/api_models/src/routing.rs index 47d75b2e8357..389af3dab7bf 100644 --- a/crates/api_models/src/routing.rs +++ b/crates/api_models/src/routing.rs @@ -615,8 +615,11 @@ impl Default for SuccessBasedRoutingConfig { pub enum SuccessBasedRoutingConfigParams { PaymentMethod, PaymentMethodType, - Currency, AuthenticationType, + Currency, + Country, + CardNetwork, + CardBin, } #[derive(serde::Serialize, serde::Deserialize, Debug, Clone, ToSchema)] diff --git a/crates/external_services/src/grpc_client.rs b/crates/external_services/src/grpc_client.rs index 5afd30245519..e7b229a80708 100644 --- a/crates/external_services/src/grpc_client.rs +++ b/crates/external_services/src/grpc_client.rs @@ -5,7 +5,6 @@ use std::{fmt::Debug, sync::Arc}; #[cfg(feature = "dynamic_routing")] use dynamic_routing::{DynamicRoutingClientConfig, RoutingStrategy}; -use router_env::logger; use serde; /// Struct contains all the gRPC Clients @@ -38,8 +37,6 @@ impl GrpcClientSettings { .await .expect("Failed to establish a connection with the Dynamic Routing Server"); - logger::info!("Connection established with gRPC Server"); - Arc::new(GrpcClients { #[cfg(feature = "dynamic_routing")] dynamic_routing: dynamic_routing_connection, diff --git a/crates/external_services/src/grpc_client/dynamic_routing.rs b/crates/external_services/src/grpc_client/dynamic_routing.rs index 343dd80e9516..0546d05ba7c9 100644 --- a/crates/external_services/src/grpc_client/dynamic_routing.rs +++ b/crates/external_services/src/grpc_client/dynamic_routing.rs @@ -9,6 +9,7 @@ use error_stack::ResultExt; use http_body_util::combinators::UnsyncBoxBody; use hyper::body::Bytes; use hyper_util::client::legacy::connect::HttpConnector; +use router_env::logger; use serde; use success_rate::{ success_rate_calculator_client::SuccessRateCalculatorClient, CalSuccessRateConfig, @@ -80,6 +81,7 @@ impl DynamicRoutingClientConfig { let success_rate_client = match self { Self::Enabled { host, port } => { let uri = format!("http://{}:{}", host, port).parse::()?; + logger::info!("Connection established with dynamic routing gRPC Server"); Some(SuccessRateCalculatorClient::with_origin(client, uri)) } Self::Disabled => None, @@ -98,6 +100,7 @@ pub trait SuccessBasedDynamicRouting: dyn_clone::DynClone + Send + Sync { &self, id: String, success_rate_based_config: SuccessBasedRoutingConfig, + params: String, label_input: Vec, ) -> DynamicRoutingResult; /// To update the success rate with the given label @@ -105,6 +108,7 @@ pub trait SuccessBasedDynamicRouting: dyn_clone::DynClone + Send + Sync { &self, id: String, success_rate_based_config: SuccessBasedRoutingConfig, + params: String, response: Vec, ) -> DynamicRoutingResult; } @@ -115,24 +119,9 @@ impl SuccessBasedDynamicRouting for SuccessRateCalculatorClient { &self, id: String, success_rate_based_config: SuccessBasedRoutingConfig, + params: String, label_input: Vec, ) -> DynamicRoutingResult { - let params = success_rate_based_config - .params - .map(|vec| { - vec.into_iter().fold(String::new(), |mut acc_str, params| { - if !acc_str.is_empty() { - acc_str.push(':') - } - acc_str.push_str(params.to_string().as_str()); - acc_str - }) - }) - .get_required_value("params") - .change_context(DynamicRoutingError::MissingRequiredField { - field: "params".to_string(), - })?; - let labels = label_input .into_iter() .map(|conn_choice| conn_choice.to_string()) @@ -167,6 +156,7 @@ impl SuccessBasedDynamicRouting for SuccessRateCalculatorClient { &self, id: String, success_rate_based_config: SuccessBasedRoutingConfig, + params: String, label_input: Vec, ) -> DynamicRoutingResult { let config = success_rate_based_config @@ -182,22 +172,6 @@ impl SuccessBasedDynamicRouting for SuccessRateCalculatorClient { }) .collect(); - let params = success_rate_based_config - .params - .map(|vec| { - vec.into_iter().fold(String::new(), |mut acc_str, params| { - if !acc_str.is_empty() { - acc_str.push(':') - } - acc_str.push_str(params.to_string().as_str()); - acc_str - }) - }) - .get_required_value("params") - .change_context(DynamicRoutingError::MissingRequiredField { - field: "params".to_string(), - })?; - let request = tonic::Request::new(UpdateSuccessRateWindowRequest { id, params, diff --git a/crates/router/src/core/errors.rs b/crates/router/src/core/errors.rs index d095b471e2dd..96321d097946 100644 --- a/crates/router/src/core/errors.rs +++ b/crates/router/src/core/errors.rs @@ -330,6 +330,8 @@ pub enum RoutingError { MetadataParsingError, #[error("Unable to retrieve success based routing config")] SuccessBasedRoutingConfigError, + #[error("Params not found in success based routing config")] + SuccessBasedRoutingParamsNotFoundError, #[error("Unable to calculate success based routing config from dynamic routing service")] SuccessRateCalculationError, #[error("Success rate client from dynamic routing gRPC service not initialized")] diff --git a/crates/router/src/core/payments.rs b/crates/router/src/core/payments.rs index 5d75001b118a..4ad00df9c247 100644 --- a/crates/router/src/core/payments.rs +++ b/crates/router/src/core/payments.rs @@ -72,6 +72,8 @@ use super::{ #[cfg(feature = "frm")] use crate::core::fraud_check as frm_core; #[cfg(all(feature = "v1", feature = "dynamic_routing"))] +use crate::core::routing::helpers as routing_helpers; +#[cfg(all(feature = "v1", feature = "dynamic_routing"))] use crate::types::api::convert_connector_data_to_routable_connectors; use crate::{ configs::settings::{ApplePayPreDecryptFlow, PaymentMethodTypeTokenFilter}, @@ -5580,10 +5582,46 @@ where #[cfg(all(feature = "v1", feature = "dynamic_routing"))] let connectors = { if business_profile.dynamic_routing_algorithm.is_some() { - routing::perform_success_based_routing(state, connectors.clone(), business_profile) - .await - .map_err(|e| logger::error!(success_rate_routing_error=?e)) - .unwrap_or(connectors) + let success_based_routing_config_params_interpolator = + routing_helpers::SuccessBasedRoutingConfigParamsInterpolator::new( + payment_data.get_payment_attempt().payment_method, + payment_data.get_payment_attempt().payment_method_type, + payment_data.get_payment_attempt().authentication_type, + payment_data.get_payment_attempt().currency, + payment_data + .get_billing_address() + .and_then(|address| address.address) + .and_then(|address| address.country), + payment_data + .get_payment_attempt() + .payment_method_data + .as_ref() + .and_then(|data| data.as_object()) + .and_then(|card| card.get("card")) + .and_then(|data| data.as_object()) + .and_then(|card| card.get("card_network")) + .and_then(|network| network.as_str()) + .map(|network| network.to_string()), + payment_data + .get_payment_attempt() + .payment_method_data + .as_ref() + .and_then(|data| data.as_object()) + .and_then(|card| card.get("card")) + .and_then(|data| data.as_object()) + .and_then(|card| card.get("card_isin")) + .and_then(|card_isin| card_isin.as_str()) + .map(|card_isin| card_isin.to_string()), + ); + routing::perform_success_based_routing( + state, + connectors.clone(), + business_profile, + success_based_routing_config_params_interpolator, + ) + .await + .map_err(|e| logger::error!(success_rate_routing_error=?e)) + .unwrap_or(connectors) } else { connectors } diff --git a/crates/router/src/core/payments/operations/payment_response.rs b/crates/router/src/core/payments/operations/payment_response.rs index 0234b97032c7..f0381dbf8220 100644 --- a/crates/router/src/core/payments/operations/payment_response.rs +++ b/crates/router/src/core/payments/operations/payment_response.rs @@ -21,7 +21,7 @@ use tracing_futures::Instrument; use super::{Operation, OperationSessionSetters, PostUpdateTracker}; #[cfg(all(feature = "v1", feature = "dynamic_routing"))] -use crate::core::routing::helpers::push_metrics_for_success_based_routing; +use crate::core::routing::helpers as routing_helpers; use crate::{ connector::utils::PaymentResponseRouterData, consts, @@ -1968,13 +1968,44 @@ async fn payment_response_update_tracker( let state = state.clone(); let business_profile = business_profile.clone(); let payment_attempt = payment_attempt.clone(); + let success_based_routing_config_params_interpolator = + routing_helpers::SuccessBasedRoutingConfigParamsInterpolator::new( + payment_attempt.payment_method, + payment_attempt.payment_method_type, + payment_attempt.authentication_type, + payment_attempt.currency, + payment_data + .address + .get_payment_billing() + .and_then(|address| address.clone().address) + .and_then(|address| address.country), + payment_attempt + .payment_method_data + .as_ref() + .and_then(|data| data.as_object()) + .and_then(|card| card.get("card")) + .and_then(|data| data.as_object()) + .and_then(|card| card.get("card_network")) + .and_then(|network| network.as_str()) + .map(|network| network.to_string()), + payment_attempt + .payment_method_data + .as_ref() + .and_then(|data| data.as_object()) + .and_then(|card| card.get("card")) + .and_then(|data| data.as_object()) + .and_then(|card| card.get("card_isin")) + .and_then(|card_isin| card_isin.as_str()) + .map(|card_isin| card_isin.to_string()), + ); tokio::spawn( async move { - push_metrics_for_success_based_routing( + routing_helpers::push_metrics_with_update_window_for_success_based_routing( &state, &payment_attempt, routable_connectors, &business_profile, + success_based_routing_config_params_interpolator, ) .await .map_err(|e| logger::error!(dynamic_routing_metrics_error=?e)) @@ -1984,6 +2015,7 @@ async fn payment_response_update_tracker( ); } } + payment_data.payment_intent = payment_intent; payment_data.payment_attempt = payment_attempt; router_data.payment_method_status.and_then(|status| { diff --git a/crates/router/src/core/payments/routing.rs b/crates/router/src/core/payments/routing.rs index 40721e9b4c31..28321529ef2d 100644 --- a/crates/router/src/core/payments/routing.rs +++ b/crates/router/src/core/payments/routing.rs @@ -1240,6 +1240,7 @@ pub async fn perform_success_based_routing( state: &SessionState, routable_connectors: Vec, business_profile: &domain::Profile, + success_based_routing_config_params_interpolator: routing::helpers::SuccessBasedRoutingConfigParamsInterpolator, ) -> RoutingResult> { let success_based_dynamic_routing_algo_ref: api_routing::DynamicRoutingAlgorithmRef = business_profile @@ -1293,6 +1294,14 @@ pub async fn perform_success_based_routing( .change_context(errors::RoutingError::SuccessBasedRoutingConfigError) .attach_printable("unable to fetch success_rate based dynamic routing configs")?; + let success_based_routing_config_params = success_based_routing_config_params_interpolator + .get_string_val( + success_based_routing_configs + .params + .as_ref() + .ok_or(errors::RoutingError::SuccessBasedRoutingParamsNotFoundError)?, + ); + let tenant_business_profile_id = routing::helpers::generate_tenant_business_profile_id( &state.tenant.redis_key_prefix, business_profile.get_id().get_string_repr(), @@ -1302,6 +1311,7 @@ pub async fn perform_success_based_routing( .calculate_success_rate( tenant_business_profile_id, success_based_routing_configs, + success_based_routing_config_params, routable_connectors, ) .await diff --git a/crates/router/src/core/routing/helpers.rs b/crates/router/src/core/routing/helpers.rs index 31cd4234714e..0250d00d1bbc 100644 --- a/crates/router/src/core/routing/helpers.rs +++ b/crates/router/src/core/routing/helpers.rs @@ -640,11 +640,12 @@ pub async fn fetch_success_based_routing_configs( /// metrics for success based dynamic routing #[cfg(all(feature = "v1", feature = "dynamic_routing"))] #[instrument(skip_all)] -pub async fn push_metrics_for_success_based_routing( +pub async fn push_metrics_with_update_window_for_success_based_routing( state: &SessionState, payment_attempt: &storage::PaymentAttempt, routable_connectors: Vec, business_profile: &domain::Profile, + success_based_routing_config_params_interpolator: SuccessBasedRoutingConfigParamsInterpolator, ) -> RouterResult<()> { let success_based_dynamic_routing_algo_ref: routing_types::DynamicRoutingAlgorithmRef = business_profile @@ -697,10 +698,20 @@ pub async fn push_metrics_for_success_based_routing( business_profile.get_id().get_string_repr(), ); + let success_based_routing_config_params = success_based_routing_config_params_interpolator + .get_string_val( + success_based_routing_configs + .params + .as_ref() + .ok_or(errors::RoutingError::SuccessBasedRoutingParamsNotFoundError) + .change_context(errors::ApiErrorResponse::InternalServerError)?, + ); + let success_based_connectors = client .calculate_success_rate( tenant_business_profile_id.clone(), success_based_routing_configs.clone(), + success_based_routing_config_params.clone(), routable_connectors.clone(), ) .await @@ -725,9 +736,10 @@ pub async fn push_metrics_for_success_based_routing( let (first_success_based_connector, merchant_connector_id) = first_success_based_connector_label .split_once(':') .ok_or(errors::ApiErrorResponse::InternalServerError) - .attach_printable( - "unable to split connector_name and mca_id from the first connector obtained from dynamic routing service", - )?; + .attach_printable(format!( + "unable to split connector_name and mca_id from the first connector {:?} obtained from dynamic routing service", + first_success_based_connector_label + ))?; let outcome = get_success_based_metrics_outcome_for_payment( &payment_status_attribute, @@ -802,6 +814,7 @@ pub async fn push_metrics_for_success_based_routing( .update_success_rate( tenant_business_profile_id, success_based_routing_configs, + success_based_routing_config_params, vec![routing_types::RoutableConnectorChoiceWithStatus::new( routing_types::RoutableConnectorChoice { choice_kind: api_models::routing::RoutableChoiceKind::FullStruct, @@ -956,3 +969,76 @@ pub async fn default_success_based_routing_setup( ); Ok(ApplicationResponse::Json(new_record)) } + +pub struct SuccessBasedRoutingConfigParamsInterpolator { + pub payment_method: Option, + pub payment_method_type: Option, + pub authentication_type: Option, + pub currency: Option, + pub country: Option, + pub card_network: Option, + pub card_bin: Option, +} + +impl SuccessBasedRoutingConfigParamsInterpolator { + pub fn new( + payment_method: Option, + payment_method_type: Option, + authentication_type: Option, + currency: Option, + country: Option, + card_network: Option, + card_bin: Option, + ) -> Self { + Self { + payment_method, + payment_method_type, + authentication_type, + currency, + country, + card_network, + card_bin, + } + } + + pub fn get_string_val( + &self, + params: &Vec, + ) -> String { + let mut parts: Vec = Vec::new(); + for param in params { + let val = match param { + routing_types::SuccessBasedRoutingConfigParams::PaymentMethod => self + .payment_method + .as_ref() + .map_or(String::new(), |pm| pm.to_string()), + routing_types::SuccessBasedRoutingConfigParams::PaymentMethodType => self + .payment_method_type + .as_ref() + .map_or(String::new(), |pmt| pmt.to_string()), + routing_types::SuccessBasedRoutingConfigParams::AuthenticationType => self + .authentication_type + .as_ref() + .map_or(String::new(), |at| at.to_string()), + routing_types::SuccessBasedRoutingConfigParams::Currency => self + .currency + .as_ref() + .map_or(String::new(), |cur| cur.to_string()), + routing_types::SuccessBasedRoutingConfigParams::Country => self + .country + .as_ref() + .map_or(String::new(), |cn| cn.to_string()), + routing_types::SuccessBasedRoutingConfigParams::CardNetwork => { + self.card_network.clone().unwrap_or_default() + } + routing_types::SuccessBasedRoutingConfigParams::CardBin => { + self.card_bin.clone().unwrap_or_default() + } + }; + if !val.is_empty() { + parts.push(val); + } + } + parts.join(":") + } +} From 28e3c366930d29bd59f1a0f06cf3b140387dbb84 Mon Sep 17 00:00:00 2001 From: Pa1NarK <69745008+pixincreate@users.noreply.github.com> Date: Fri, 8 Nov 2024 17:42:30 +0530 Subject: [PATCH 16/25] ci(cypressV2): update cypress v2 framework to accommodate hyperswitch v2 changes (#6493) --- .../cypress/e2e/configs/Payment/Utils.js | 12 - .../cypress/e2e/configs/Payment/_Reusable.js | 15 +- .../spec/Payment/0001-[No3DS]Payments.cy.js | 132 ++++++++++ .../e2e/spec/Payment/0002-[3DS]Payments.cy.js | 8 + .../cypress/fixtures/organization.json | 2 +- cypress-tests-v2/cypress/support/commands.js | 233 +++++++++++------- .../cypress/utils/RequestBodyUtils.js | 48 ++++ cypress-tests-v2/package-lock.json | 153 +++++------- cypress-tests-v2/package.json | 5 +- 9 files changed, 412 insertions(+), 196 deletions(-) create mode 100644 cypress-tests-v2/cypress/e2e/spec/Payment/0001-[No3DS]Payments.cy.js create mode 100644 cypress-tests-v2/cypress/e2e/spec/Payment/0002-[3DS]Payments.cy.js create mode 100644 cypress-tests-v2/cypress/utils/RequestBodyUtils.js diff --git a/cypress-tests-v2/cypress/e2e/configs/Payment/Utils.js b/cypress-tests-v2/cypress/e2e/configs/Payment/Utils.js index 6faea73d4ef9..9785839f4221 100644 --- a/cypress-tests-v2/cypress/e2e/configs/Payment/Utils.js +++ b/cypress-tests-v2/cypress/e2e/configs/Payment/Utils.js @@ -94,15 +94,3 @@ export function defaultErrorHandler(response, response_data) { expect(response.body.error).to.include(response_data.body.error); } } - -export function isoTimeTomorrow() { - const now = new Date(); - - // Create a new date object for tomorrow - const tomorrow = new Date(now); - tomorrow.setDate(now.getDate() + 1); - - // Convert to ISO string format - const isoStringTomorrow = tomorrow.toISOString(); - return isoStringTomorrow; -} diff --git a/cypress-tests-v2/cypress/e2e/configs/Payment/_Reusable.js b/cypress-tests-v2/cypress/e2e/configs/Payment/_Reusable.js index b69506ff0755..353d96260b55 100644 --- a/cypress-tests-v2/cypress/e2e/configs/Payment/_Reusable.js +++ b/cypress-tests-v2/cypress/e2e/configs/Payment/_Reusable.js @@ -15,12 +15,23 @@ function normalise(input) { paybox: "Paybox", paypal: "Paypal", wellsfargo: "Wellsfargo", + fiuu: "Fiuu", // Add more known exceptions here }; if (typeof input !== "string") { - const spec_name = Cypress.spec.name.split("-")[1].split(".")[0]; - return `${spec_name}`; + const specName = Cypress.spec.name; + + if (specName.includes("-")) { + const parts = specName.split("-"); + + if (parts.length > 1 && parts[1].includes(".")) { + return parts[1].split(".")[0]; + } + } + + // Fallback + return `${specName}`; } const lowerCaseInput = input.toLowerCase(); diff --git a/cypress-tests-v2/cypress/e2e/spec/Payment/0001-[No3DS]Payments.cy.js b/cypress-tests-v2/cypress/e2e/spec/Payment/0001-[No3DS]Payments.cy.js new file mode 100644 index 000000000000..21766617c09a --- /dev/null +++ b/cypress-tests-v2/cypress/e2e/spec/Payment/0001-[No3DS]Payments.cy.js @@ -0,0 +1,132 @@ +/* +No 3DS Auto capture with Confirm True +No 3DS Auto capture with Confirm False +No 3DS Manual capture with Confirm True +No 3DS Manual capture with Confirm False +No 3DS Manual multiple capture with Confirm True +No 3DS Manual multiple capture with Confirm False +*/ + +import * as fixtures from "../../../fixtures/imports"; +import State from "../../../utils/State"; +import getConnectorDetails from "../../configs/Payment/Utils"; + +let globalState; + +// Below is an example of a test that is skipped just because it is not implemented yet +describe("[Payment] [No 3DS] [Payment Method: Card]", () => { + context("[Payment] [No 3DS] [Capture: Automatic] [Confirm: True]", () => { + let should_continue = true; + + before("seed global state", () => { + cy.task("getGlobalState").then((state) => { + globalState = new State(state); + }); + }); + beforeEach(function () { + if (!should_continue) { + this.skip(); + } + }); + + after("flush global state", () => { + cy.task("setGlobalState", globalState.data); + }); + + it.skip("Create payment intent", () => { + let data = getConnectorDetails(globalState.get("connectorId"))["card_pm"][ + "PaymentIntent" + ]; + let req_data = data["Request"]; + let res_data = data["Response"]; + cy.paymentIntentCreateCall( + fixtures.createPaymentBody, + req_data, + res_data, + "no_three_ds", + "automatic", + globalState + ); + }); + + it.skip("List payment methods", () => { + cy.paymentMethodsListCall(globalState); + }); + + it.skip("Confirm payment intent", () => { + let data = getConnectorDetails(globalState.get("connectorId"))["card_pm"][ + "No3DSAutoCapture" + ]; + let req_data = data["Request"]; + let res_data = data["Response"]; + cy.paymentIntentConfirmCall( + fixtures.confirmBody, + req_data, + res_data, + true, + globalState + ); + }); + + it.skip("Retrieve payment intent", () => { + cy.paymentIntentRetrieveCall(globalState); + }); + }); + context("[Payment] [No 3DS] [Capture: Automatic] [Confirm: False]", () => { + let should_continue = true; + + before("seed global state", () => { + cy.task("getGlobalState").then((state) => { + globalState = new State(state); + }); + }); + beforeEach(function () { + if (!should_continue) { + this.skip(); + } + }); + + after("flush global state", () => { + cy.task("setGlobalState", globalState.data); + }); + + it.skip("Create Payment Intent", () => { + let data = getConnectorDetails(globalState.get("connectorId"))["card_pm"][ + "PaymentIntent" + ]; + let req_data = data["Request"]; + let res_data = data["Response"]; + cy.paymentIntentCreateCall( + fixtures.createPaymentBody, + req_data, + res_data, + "no_three_ds", + "automatic", + globalState + ); + }); + + it.skip("Payment Methods", () => { + cy.paymentMethodsCallTest(globalState); + }); + + it.skip("Confirm No 3DS", () => { + let data = getConnectorDetails(globalState.get("connectorId"))["card_pm"][ + "No3DSAutoCapture" + ]; + let req_data = data["Request"]; + let res_data = data["Response"]; + cy.paymentIntentConfirmCall( + fixtures.confirmBody, + req_data, + res_data, + true, + globalState + ); + }); + + it.skip("Retrieve payment intent", () => { + cy.paymentIntentRetrieveCall(globalState); + }); + }); +}); diff --git a/cypress-tests-v2/cypress/e2e/spec/Payment/0002-[3DS]Payments.cy.js b/cypress-tests-v2/cypress/e2e/spec/Payment/0002-[3DS]Payments.cy.js new file mode 100644 index 000000000000..2a1aa7d51528 --- /dev/null +++ b/cypress-tests-v2/cypress/e2e/spec/Payment/0002-[3DS]Payments.cy.js @@ -0,0 +1,8 @@ +/* +3DS Auto capture with Confirm True +3DS Auto capture with Confirm False +3DS Manual capture with Confirm True +3DS Manual capture with Confirm False +3DS Manual multiple capture with Confirm True +3DS Manual multiple capture with Confirm False +*/ diff --git a/cypress-tests-v2/cypress/fixtures/organization.json b/cypress-tests-v2/cypress/fixtures/organization.json index 24d084ab606f..f0577db88765 100644 --- a/cypress-tests-v2/cypress/fixtures/organization.json +++ b/cypress-tests-v2/cypress/fixtures/organization.json @@ -1,6 +1,6 @@ { "org_create": { - "organization_name": "Hyperswitch Organization" + "organization_name": "Hyperswitch" }, "org_update": { "organization_name": "Hyperswitch", diff --git a/cypress-tests-v2/cypress/support/commands.js b/cypress-tests-v2/cypress/support/commands.js index abdf95194bb6..eb4ca3423eb3 100644 --- a/cypress-tests-v2/cypress/support/commands.js +++ b/cypress-tests-v2/cypress/support/commands.js @@ -26,10 +26,9 @@ // cy.task can only be used in support files (spec files or commands file) -import { - getValueByKey, - isoTimeTomorrow, -} from "../e2e/configs/Payment/Utils.js"; +import { nanoid } from "nanoid"; +import { getValueByKey } from "../e2e/configs/Payment/Utils.js"; +import { isoTimeTomorrow, validateEnv } from "../utils/RequestBodyUtils.js"; function logRequestId(xRequestId) { if (xRequestId) { @@ -48,6 +47,9 @@ Cypress.Commands.add( const base_url = globalState.get("baseUrl"); const url = `${base_url}/v2/organization`; + // Update request body + organizationCreateBody.organization_name += " " + nanoid(); + cy.request({ method: "POST", url: url, @@ -71,7 +73,7 @@ Cypress.Commands.add( } else { // to be updated throw new Error( - `Organization create call failed with status ${response.status} and message ${response.body.message}` + `Organization create call failed with status ${response.status} and message: "${response.body.error.message}"` ); } }); @@ -111,7 +113,7 @@ Cypress.Commands.add("organizationRetrieveCall", (globalState) => { } else { // to be updated throw new Error( - `Organization retrieve call failed with status ${response.status} and message ${response.body.message}` + `Organization retrieve call failed with status ${response.status} and message: "${response.body.error.message}"` ); } }); @@ -125,6 +127,9 @@ Cypress.Commands.add( const organization_id = globalState.get("organizationId"); const url = `${base_url}/v2/organization/${organization_id}`; + // Update request body + organizationUpdateBody.organization_name += " " + nanoid(); + cy.request({ method: "PUT", url: url, @@ -152,7 +157,7 @@ Cypress.Commands.add( } else { // to be updated throw new Error( - `Organization update call failed with status ${response.status} and message ${response.body.message}` + `Organization update call failed with status ${response.status} and message: "${response.body.error.message}"` ); } }); @@ -166,6 +171,8 @@ Cypress.Commands.add( // Define the necessary variables and constants const api_key = globalState.get("adminApiKey"); const base_url = globalState.get("baseUrl"); + const key_id_type = "publishable_key"; + const key_id = validateEnv(base_url, key_id_type); const organization_id = globalState.get("organizationId"); const url = `${base_url}/v2/merchant_accounts`; @@ -192,14 +199,9 @@ Cypress.Commands.add( .and.to.include(`${merchant_name}_`) .and.to.be.a("string").and.not.be.empty; - if (base_url.includes("sandbox") || base_url.includes("integ")) - expect(response.body) - .to.have.property("publishable_key") - .and.to.include("pk_snd").and.to.not.be.empty; - else if (base_url.includes("localhost")) - expect(response.body) - .to.have.property("publishable_key") - .and.to.include("pk_dev").and.to.not.be.empty; + expect(response.body) + .to.have.property(key_id_type) + .and.to.include(key_id).and.to.not.be.empty; globalState.set("merchantId", response.body.id); globalState.set("publishableKey", response.body.publishable_key); @@ -208,7 +210,7 @@ Cypress.Commands.add( } else { // to be updated throw new Error( - `Merchant create call failed with status ${response.status} and message ${response.body.message}` + `Merchant create call failed with status ${response.status} and message: "${response.body.error.message}"` ); } }); @@ -218,6 +220,8 @@ Cypress.Commands.add("merchantAccountRetrieveCall", (globalState) => { // Define the necessary variables and constants const api_key = globalState.get("adminApiKey"); const base_url = globalState.get("baseUrl"); + const key_id_type = "publishable_key"; + const key_id = validateEnv(base_url, key_id_type); const merchant_id = globalState.get("merchantId"); const url = `${base_url}/v2/merchant_accounts/${merchant_id}`; @@ -236,14 +240,8 @@ Cypress.Commands.add("merchantAccountRetrieveCall", (globalState) => { expect(response.body).to.have.property("id").and.to.be.a("string").and.not .be.empty; - if (base_url.includes("sandbox") || base_url.includes("integ")) - expect(response.body) - .to.have.property("publishable_key") - .and.to.include("pk_snd").and.to.not.be.empty; - else - expect(response.body) - .to.have.property("publishable_key") - .and.to.include("pk_dev").and.to.not.be.empty; + expect(response.body).to.have.property(key_id_type).and.to.include(key_id) + .and.to.not.be.empty; if (merchant_id === undefined || merchant_id === null) { globalState.set("merchantId", response.body.id); @@ -253,7 +251,7 @@ Cypress.Commands.add("merchantAccountRetrieveCall", (globalState) => { } else { // to be updated throw new Error( - `Merchant account retrieve call failed with status ${response.status} and message ${response.body.message}` + `Merchant account retrieve call failed with status ${response.status} and message: "${response.body.error.message}"` ); } }); @@ -264,6 +262,8 @@ Cypress.Commands.add( // Define the necessary variables and constants const api_key = globalState.get("adminApiKey"); const base_url = globalState.get("baseUrl"); + const key_id_type = "publishable_key"; + const key_id = validateEnv(base_url, key_id_type); const merchant_id = globalState.get("merchantId"); const url = `${base_url}/v2/merchant_accounts/${merchant_id}`; @@ -284,14 +284,10 @@ Cypress.Commands.add( if (response.status === 200) { expect(response.body.id).to.equal(merchant_id); - if (base_url.includes("sandbox") || base_url.includes("integ")) - expect(response.body) - .to.have.property("publishable_key") - .and.to.include("pk_snd").and.to.not.be.empty; - else - expect(response.body) - .to.have.property("publishable_key") - .and.to.include("pk_dev").and.to.not.be.empty; + expect(response.body) + .to.have.property(key_id_type) + .and.to.include(key_id).and.to.not.be.empty; + expect(response.body.merchant_name).to.equal(merchant_name); if (merchant_id === undefined || merchant_id === null) { @@ -302,7 +298,7 @@ Cypress.Commands.add( } else { // to be updated throw new Error( - `Merchant account update call failed with status ${response.status} and message ${response.body.message}` + `Merchant account update call failed with status ${response.status} and message: "${response.body.error.message}"` ); } }); @@ -349,7 +345,7 @@ Cypress.Commands.add( } else { // to be updated throw new Error( - `Business profile create call failed with status ${response.status} and message ${response.body.message}` + `Business profile create call failed with status ${response.status} and message: "${response.body.error.message}"` ); } }); @@ -390,7 +386,7 @@ Cypress.Commands.add("businessProfileRetrieveCall", (globalState) => { } else { // to be updated throw new Error( - `Business profile retrieve call failed with status ${response.status} and message ${response.body.message}` + `Business profile retrieve call failed with status ${response.status} and message: "${response.body.error.message}"` ); } }); @@ -436,7 +432,7 @@ Cypress.Commands.add( } else { // to be updated throw new Error( - `Business profile update call failed with status ${response.status} and message ${response.body.message}` + `Business profile update call failed with status ${response.status} and message: "${response.body.error.message}"` ); } }); @@ -494,8 +490,8 @@ Cypress.Commands.add( authDetails.connector_account_details; if (authDetails && authDetails.metadata) { - createConnectorBody.metadata = { - ...createConnectorBody.metadata, // Preserve existing metadata fields + mcaCreateBody.metadata = { + ...mcaCreateBody.metadata, // Preserve existing metadata fields ...authDetails.metadata, // Merge with authDetails.metadata }; } @@ -525,7 +521,7 @@ Cypress.Commands.add( } else { // to be updated throw new Error( - `Merchant connector account create call failed with status ${response.status} and message ${response.body.message}` + `Merchant connector account create call failed with status ${response.status} and message: "${response.body.error.message}"` ); } }); @@ -573,7 +569,7 @@ Cypress.Commands.add("mcaRetrieveCall", (globalState) => { } else { // to be updated throw new Error( - `Merchant connector account retrieve call failed with status ${response.status} and message ${response.body.message}` + `Merchant connector account retrieve call failed with status ${response.status} and message: "${response.body.error.message}"` ); } }); @@ -638,7 +634,7 @@ Cypress.Commands.add( } else { // to be updated throw new Error( - `Merchant connector account update call failed with status ${response.status} and message ${response.body.message}` + `Merchant connector account update call failed with status ${response.status} and message: "${response.body.error.message}"` ); } }); @@ -654,6 +650,8 @@ Cypress.Commands.add("apiKeyCreateCall", (apiKeyCreateBody, globalState) => { // We do not want to keep API Key forever, // so we set the expiry to tomorrow as new merchant accounts are created with every run const expiry = isoTimeTomorrow(); + const key_id_type = "key_id"; + const key_id = validateEnv(base_url, key_id_type); const merchant_id = globalState.get("merchantId"); const url = `${base_url}/v2/api_keys`; @@ -682,13 +680,8 @@ Cypress.Commands.add("apiKeyCreateCall", (apiKeyCreateBody, globalState) => { expect(response.body.description).to.equal(apiKeyCreateBody.description); // API Key assertions are intentionally excluded to avoid being exposed in the logs - if (base_url.includes("sandbox") || base_url.includes("integ")) { - expect(response.body).to.have.property("key_id").and.to.include("snd_") - .and.to.not.be.empty; - } else if (base_url.includes("localhost")) { - expect(response.body).to.have.property("key_id").and.to.include("dev_") - .and.to.not.be.empty; - } + expect(response.body).to.have.property(key_id_type).and.to.include(key_id) + .and.to.not.be.empty; globalState.set("apiKeyId", response.body.key_id); globalState.set("apiKey", response.body.api_key); @@ -697,7 +690,7 @@ Cypress.Commands.add("apiKeyCreateCall", (apiKeyCreateBody, globalState) => { } else { // to be updated throw new Error( - `API Key create call failed with status ${response.status} and message ${response.body.message}` + `API Key create call failed with status ${response.status} and message: "${response.body.error.message}"` ); } }); @@ -706,6 +699,8 @@ Cypress.Commands.add("apiKeyRetrieveCall", (globalState) => { // Define the necessary variables and constant const api_key = globalState.get("adminApiKey"); const base_url = globalState.get("baseUrl"); + const key_id_type = "key_id"; + const key_id = validateEnv(base_url, key_id_type); const merchant_id = globalState.get("merchantId"); const api_key_id = globalState.get("apiKeyId"); const url = `${base_url}/v2/api_keys/${api_key_id}`; @@ -728,15 +723,9 @@ Cypress.Commands.add("apiKeyRetrieveCall", (globalState) => { if (response.status === 200) { expect(response.body.merchant_id).to.equal(merchant_id); - // API Key assertions are intentionally excluded to avoid being exposed in the logs - if (base_url.includes("sandbox") || base_url.includes("integ")) { - expect(response.body).to.have.property("key_id").and.to.include("snd_") - .and.to.not.be.empty; - } else if (base_url.includes("localhost")) { - expect(response.body).to.have.property("key_id").and.to.include("dev_") - .and.to.not.be.empty; - } + expect(response.body).to.have.property(key_id_type).and.to.include(key_id) + .and.to.not.be.empty; if (api_key === undefined || api_key === null) { globalState.set("apiKey", response.body.api_key); @@ -745,7 +734,7 @@ Cypress.Commands.add("apiKeyRetrieveCall", (globalState) => { } else { // to be updated throw new Error( - `API Key retrieve call failed with status ${response.status} and message ${response.body.message}` + `API Key retrieve call failed with status ${response.status} and message: "${response.body.error.message}"` ); } }); @@ -758,6 +747,8 @@ Cypress.Commands.add("apiKeyUpdateCall", (apiKeyUpdateBody, globalState) => { // We do not want to keep API Key forever, // so we set the expiry to tomorrow as new merchant accounts are created with every run const expiry = isoTimeTomorrow(); + const key_id_type = "key_id"; + const key_id = validateEnv(base_url, key_id_type); const merchant_id = globalState.get("merchantId"); const url = `${base_url}/v2/api_keys/${api_key_id}`; @@ -786,13 +777,8 @@ Cypress.Commands.add("apiKeyUpdateCall", (apiKeyUpdateBody, globalState) => { expect(response.body.description).to.equal(apiKeyUpdateBody.description); // API Key assertions are intentionally excluded to avoid being exposed in the logs - if (base_url.includes("sandbox") || base_url.includes("integ")) { - expect(response.body).to.have.property("key_id").and.to.include("snd_") - .and.to.not.be.empty; - } else if (base_url.includes("localhost")) { - expect(response.body).to.have.property("key_id").and.to.include("dev_") - .and.to.not.be.empty; - } + expect(response.body).to.have.property(key_id_type).and.to.include(key_id) + .and.to.not.be.empty; if (api_key === undefined || api_key === null) { globalState.set("apiKey", response.body.api_key); @@ -801,7 +787,7 @@ Cypress.Commands.add("apiKeyUpdateCall", (apiKeyUpdateBody, globalState) => { } else { // to be updated throw new Error( - `API Key update call failed with status ${response.status} and message ${response.body.message}` + `API Key update call failed with status ${response.status} and message: "${response.body.error.message}"` ); } }); @@ -847,7 +833,7 @@ Cypress.Commands.add( } else { // to be updated throw new Error( - `Routing algorithm setup call failed with status ${response.status} and message ${response.body.message}` + `Routing algorithm setup call failed with status ${response.status} and message: "${response.body.error.message}"` ); } }); @@ -886,7 +872,7 @@ Cypress.Commands.add( } else { // to be updated throw new Error( - `Routing algorithm activation call failed with status ${response.status} and message ${response.body.message}` + `Routing algorithm activation call failed with status ${response.status} and message: "${response.body.error.message}"` ); } }); @@ -925,7 +911,7 @@ Cypress.Commands.add("routingActivationRetrieveCall", (globalState) => { } else { // to be updated throw new Error( - `Routing algorithm activation retrieve call failed with status ${response.status} and message ${response.body.message}` + `Routing algorithm activation retrieve call failed with status ${response.status} and message: "${response.body.error.message}"` ); } }); @@ -960,7 +946,7 @@ Cypress.Commands.add("routingDeactivateCall", (globalState) => { } else { // to be updated throw new Error( - `Routing algorithm deactivation call failed with status ${response.status} and message ${response.body.message}` + `Routing algorithm deactivation call failed with status ${response.status} and message: "${response.body.error.message}"` ); } }); @@ -997,7 +983,7 @@ Cypress.Commands.add("routingRetrieveCall", (globalState) => { } else { // to be updated throw new Error( - `Routing algorithm activation retrieve call failed with status ${response.status} and message ${response.body.message}` + `Routing algorithm activation retrieve call failed with status ${response.status} and message: "${response.body.error.message}"` ); } }); @@ -1016,7 +1002,7 @@ Cypress.Commands.add( routingDefaultFallbackBody = payload; cy.request({ - method: "POST", + method: "PATCH", url: url, headers: { Authorization: `Bearer ${api_key}`, @@ -1032,7 +1018,7 @@ Cypress.Commands.add( } else { // to be updated throw new Error( - `Routing algorithm activation retrieve call failed with status ${response.status} and message ${response.body.message}` + `Routing algorithm activation retrieve call failed with status ${response.status} and message: "${response.body.error.message}"` ); } }); @@ -1061,7 +1047,7 @@ Cypress.Commands.add("routingFallbackRetrieveCall", (globalState) => { } else { // to be updated throw new Error( - `Routing algorithm activation retrieve call failed with status ${response.status} and message ${response.body.message}` + `Routing algorithm activation retrieve call failed with status ${response.status} and message: "${response.body.error.message}"` ); } }); @@ -1100,7 +1086,7 @@ Cypress.Commands.add("userLogin", (globalState) => { } else { // to be updated throw new Error( - `User login call failed to get totp token with status ${response.status} and message ${response.body.message}` + `User login call failed to get totp token with status ${response.status} and message: "${response.body.error.message}"` ); } }); @@ -1133,7 +1119,7 @@ Cypress.Commands.add("terminate2Fa", (globalState) => { } else { // to be updated throw new Error( - `2FA terminate call failed with status ${response.status} and message ${response.body.message}` + `2FA terminate call failed with status ${response.status} and message: "${response.body.error.message}"` ); } }); @@ -1166,7 +1152,7 @@ Cypress.Commands.add("userInfo", (globalState) => { } else { // to be updated throw new Error( - `User login call failed to fetch user info with status ${response.status} and message ${response.body.message}` + `User login call failed to fetch user info with status ${response.status} and message: "${response.body.error.message}"` ); } }); @@ -1177,6 +1163,8 @@ Cypress.Commands.add("merchantAccountsListCall", (globalState) => { // Define the necessary variables and constants const api_key = globalState.get("adminApiKey"); const base_url = globalState.get("baseUrl"); + const key_id_type = "publishable_key"; + const key_id = validateEnv(base_url, key_id_type); const organization_id = globalState.get("organizationId"); const url = `${base_url}/v2/organization/${organization_id}/merchant_accounts`; @@ -1198,21 +1186,15 @@ Cypress.Commands.add("merchantAccountsListCall", (globalState) => { expect(response.body[key]) .to.have.property("organization_id") .and.to.equal(organization_id); - if (base_url.includes("integ") || base_url.includes("sandbox")) { - expect(response.body[key]) - .to.have.property("publishable_key") - .and.include("pk_snd_").and.to.not.be.empty; - } else if (base_url.includes("localhost")) { - expect(response.body[key]) - .to.have.property("publishable_key") - .and.include("pk_dev_").and.to.not.be.empty; - } + expect(response.body[key]) + .to.have.property(key_id_type) + .and.include(key_id).and.to.not.be.empty; expect(response.body[key]).to.have.property("id").and.to.not.be.empty; } } else { // to be updated throw new Error( - `Merchant accounts list call failed with status ${response.status} and message ${response.body.message}` + `Merchant accounts list call failed with status ${response.status} and message: "${response.body.error.message}"` ); } }); @@ -1253,7 +1235,7 @@ Cypress.Commands.add("businessProfilesListCall", (globalState) => { } else { // to be updated throw new Error( - `Business profiles list call failed with status ${response.status} and message ${response.body.message}` + `Business profiles list call failed with status ${response.status} and message: "${response.body.error.message}"` ); } }); @@ -1314,7 +1296,7 @@ Cypress.Commands.add("mcaListCall", (globalState, service_type) => { } else { // to be updated throw new Error( - `Merchant connector account list call failed with status ${response.status} and message ${response.body.message}` + `Merchant connector account list call failed with status ${response.status} and message: "${response.body.error.message}"` ); } }); @@ -1323,6 +1305,8 @@ Cypress.Commands.add("apiKeysListCall", (globalState) => { // Define the necessary variables and constants const api_key = globalState.get("adminApiKey"); const base_url = globalState.get("baseUrl"); + const key_id_type = "key_id"; + const key_id = validateEnv(base_url, key_id_type); const merchant_id = globalState.get("merchantId"); const url = `${base_url}/v2/api_keys/list`; @@ -1347,8 +1331,8 @@ Cypress.Commands.add("apiKeysListCall", (globalState) => { expect(response.body).to.be.an("array").and.to.not.be.empty; for (const key in response.body) { expect(response.body[key]) - .to.have.property("key_id") - .and.to.include("dev_").and.to.not.be.empty; + .to.have.property(key_id_type) + .and.to.include(key_id).and.to.not.be.empty; expect(response.body[key]) .to.have.property("merchant_id") .and.to.equal(merchant_id).and.to.not.be.empty; @@ -1356,13 +1340,74 @@ Cypress.Commands.add("apiKeysListCall", (globalState) => { } else { // to be updated throw new Error( - `API Keys list call failed with status ${response.status} and message ${response.body.message}` + `API Keys list call failed with status ${response.status} and message: "${response.body.error.message}"` ); } }); }); -// templates +// Payment API calls +// Update the below commands while following the conventions +// Below is an example of how the payment intent create call should look like (update the below command as per the need) +Cypress.Commands.add( + "paymentIntentCreateCall", + ( + globalState, + paymentRequestBody, + paymentResponseBody + /* Add more variables based on the need*/ + ) => { + // Define the necessary variables and constants at the top + // Also construct the URL here + const api_key = globalState.get("apiKey"); + const base_url = globalState.get("baseUrl"); + const profile_id = globalState.get("profileId"); + const url = `${base_url}/v2/payments/create-intent`; + + // Update request body if needed + paymentRequestBody = {}; + + // Pass Custom Headers + const customHeaders = { + "x-profile-id": profile_id, + }; + + cy.request({ + method: "POST", + url: url, + headers: { + "api-key": api_key, + "Content-Type": "application/json", + ...customHeaders, + }, + body: paymentRequestBody, + failOnStatusCode: false, + }).then((response) => { + // Logging x-request-id is mandatory + logRequestId(response.headers["x-request-id"]); + + if (response.status === 200) { + // Update the assertions based on the need + expect(response.body).to.deep.equal(paymentResponseBody); + } else if (response.status === 400) { + // Add 4xx validations here + expect(response.body).to.deep.equal(paymentResponseBody); + } else if (response.status === 500) { + // Add 5xx validations here + expect(response.body).to.deep.equal(paymentResponseBody); + } else { + // If status code is other than the ones mentioned above, default should be thrown + throw new Error( + `Payment intent create call failed with status ${response.status} and message: "${response.body.error.message}"` + ); + } + }); + } +); +Cypress.Commands.add("paymentIntentConfirmCall", (globalState) => {}); +Cypress.Commands.add("paymentIntentRetrieveCall", (globalState) => {}); + +// templates for future use Cypress.Commands.add("", () => { cy.request({}).then((response) => {}); }); diff --git a/cypress-tests-v2/cypress/utils/RequestBodyUtils.js b/cypress-tests-v2/cypress/utils/RequestBodyUtils.js new file mode 100644 index 000000000000..0926cbd97bfb --- /dev/null +++ b/cypress-tests-v2/cypress/utils/RequestBodyUtils.js @@ -0,0 +1,48 @@ +const keyPrefixes = { + localhost: { + publishable_key: "pk_dev_", + key_id: "dev_", + }, + integ: { + publishable_key: "pk_snd_", + key_id: "snd_", + }, + sandbox: { + publishable_key: "pk_snd_", + key_id: "snd_", + }, +}; + +export function isoTimeTomorrow() { + const now = new Date(); + + // Create a new date object for tomorrow + const tomorrow = new Date(now); + tomorrow.setDate(now.getDate() + 1); + + // Convert to ISO string format + const isoStringTomorrow = tomorrow.toISOString(); + return isoStringTomorrow; +} + +export function validateEnv(baseUrl, keyIdType) { + if (!baseUrl) { + throw new Error("Please provide a baseUrl"); + } + + const environment = Object.keys(keyPrefixes).find((env) => + baseUrl.includes(env) + ); + + if (!environment) { + throw new Error("Unsupported baseUrl"); + } + + const prefix = keyPrefixes[environment][keyIdType]; + + if (!prefix) { + throw new Error(`Unsupported keyIdType: ${keyIdType}`); + } + + return prefix; +} diff --git a/cypress-tests-v2/package-lock.json b/cypress-tests-v2/package-lock.json index d9282b8d3ce9..36f801468a9f 100644 --- a/cypress-tests-v2/package-lock.json +++ b/cypress-tests-v2/package-lock.json @@ -10,9 +10,10 @@ "license": "ISC", "devDependencies": { "@types/fs-extra": "^11.0.4", - "cypress": "^13.14.2", + "cypress": "^13.15.2", "cypress-mochawesome-reporter": "^3.8.2", "jsqr": "^1.4.0", + "nanoid": "^5.0.8", "prettier": "^3.3.2" } }, @@ -28,9 +29,9 @@ } }, "node_modules/@cypress/request": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/@cypress/request/-/request-3.0.5.tgz", - "integrity": "sha512-v+XHd9XmWbufxF1/bTaVm2yhbxY+TB4YtWRqF2zaXBlDNMkls34KiATz0AVDLavL3iB6bQk9/7n3oY1EoLSWGA==", + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@cypress/request/-/request-3.0.6.tgz", + "integrity": "sha512-fi0eVdCOtKu5Ed6+E8mYxUF6ZTFJDZvHogCBelM0xVXmrDEkyM22gRArQzq1YcHPm1V47Vf/iAD+WgVdUlJCGg==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -49,7 +50,7 @@ "performance-now": "^2.1.0", "qs": "6.13.0", "safe-buffer": "^5.1.2", - "tough-cookie": "^4.1.3", + "tough-cookie": "^5.0.0", "tunnel-agent": "^0.6.0", "uuid": "^8.3.2" }, @@ -567,9 +568,9 @@ } }, "node_modules/ci-info": { - "version": "3.9.0", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", - "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.0.0.tgz", + "integrity": "sha512-TdHqgGf9odd8SXNuxtUBVx8Nv+qZOejE6qyqiy5NtbYYQOeFa6zmHkxlPzmaLxWWHsU6nJmB7AETdVPi+2NBUg==", "dev": true, "funding": [ { @@ -741,14 +742,14 @@ } }, "node_modules/cypress": { - "version": "13.14.2", - "resolved": "https://registry.npmjs.org/cypress/-/cypress-13.14.2.tgz", - "integrity": "sha512-lsiQrN17vHMB2fnvxIrKLAjOr9bPwsNbPZNrWf99s4u+DVmCY6U+w7O3GGG9FvP4EUVYaDu+guWeNLiUzBrqvA==", + "version": "13.15.2", + "resolved": "https://registry.npmjs.org/cypress/-/cypress-13.15.2.tgz", + "integrity": "sha512-ARbnUorjcCM3XiPwgHKuqsyr5W9Qn+pIIBPaoilnoBkLdSC2oLQjV1BUpnmc7KR+b7Avah3Ly2RMFnfxr96E/A==", "dev": true, "hasInstallScript": true, "license": "MIT", "dependencies": { - "@cypress/request": "^3.0.1", + "@cypress/request": "^3.0.6", "@cypress/xvfb": "^1.2.4", "@types/sinonjs__fake-timers": "8.1.1", "@types/sizzle": "^2.3.2", @@ -759,6 +760,7 @@ "cachedir": "^2.3.0", "chalk": "^4.1.0", "check-more-types": "^2.24.0", + "ci-info": "^4.0.0", "cli-cursor": "^3.1.0", "cli-table3": "~0.6.1", "commander": "^6.2.1", @@ -773,7 +775,6 @@ "figures": "^3.2.0", "fs-extra": "^9.1.0", "getos": "^3.2.1", - "is-ci": "^3.0.1", "is-installed-globally": "~0.4.0", "lazy-ass": "^1.6.0", "listr2": "^3.8.3", @@ -788,6 +789,7 @@ "semver": "^7.5.3", "supports-color": "^8.1.1", "tmp": "~0.2.3", + "tree-kill": "1.2.2", "untildify": "^4.0.0", "yauzl": "^2.10.0" }, @@ -1203,9 +1205,9 @@ } }, "node_modules/form-data": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", - "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.1.tgz", + "integrity": "sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==", "dev": true, "license": "MIT", "dependencies": { @@ -1583,19 +1585,6 @@ "node": ">=8" } }, - "node_modules/is-ci": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-3.0.1.tgz", - "integrity": "sha512-ZYvCgrefwqoQ6yTyYUbQu64HsITZ3NfKX1lzaEYdkTDcfKzzCI/wthRRYKkdjHKFVgNiXKAKm65Zo1pk2as/QQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ci-info": "^3.2.0" - }, - "bin": { - "is-ci": "bin.js" - } - }, "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -2457,6 +2446,25 @@ "dev": true, "license": "MIT" }, + "node_modules/nanoid": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.0.8.tgz", + "integrity": "sha512-TcJPw+9RV9dibz1hHUzlLVy8N4X9TnwirAjrU08Juo6BNKggzVfP2ZJ/3ZUSq15Xl5i85i+Z89XBO90pB2PghQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.js" + }, + "engines": { + "node": "^18 || >=20" + } + }, "node_modules/normalize-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", @@ -2733,13 +2741,6 @@ "dev": true, "license": "MIT" }, - "node_modules/psl": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz", - "integrity": "sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==", - "dev": true, - "license": "MIT" - }, "node_modules/pump": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.2.tgz", @@ -2751,16 +2752,6 @@ "once": "^1.3.1" } }, - "node_modules/punycode": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", - "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, "node_modules/qs": { "version": "6.13.0", "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", @@ -2777,13 +2768,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/querystringify": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", - "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==", - "dev": true, - "license": "MIT" - }, "node_modules/randombytes": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", @@ -2843,13 +2827,6 @@ "dev": true, "license": "ISC" }, - "node_modules/requires-port": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", - "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", - "dev": true, - "license": "MIT" - }, "node_modules/restore-cursor": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", @@ -3150,6 +3127,26 @@ "dev": true, "license": "MIT" }, + "node_modules/tldts": { + "version": "6.1.59", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.59.tgz", + "integrity": "sha512-472ilPxsRuqBBpn+KuRBHJvZhk6tTo4yTVsmODrLBNLwRYJPkDfMEHivgNwp5iEl+cbrZzzRtLKRxZs7+QKkRg==", + "dev": true, + "license": "MIT", + "dependencies": { + "tldts-core": "^6.1.59" + }, + "bin": { + "tldts": "bin/cli.js" + } + }, + "node_modules/tldts-core": { + "version": "6.1.59", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.59.tgz", + "integrity": "sha512-EiYgNf275AQyVORl8HQYYe7rTVnmLb4hkWK7wAk/12Ksy5EiHpmUmTICa4GojookBPC8qkLMBKKwCmzNA47ZPQ==", + "dev": true, + "license": "MIT" + }, "node_modules/tmp": { "version": "0.2.3", "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.3.tgz", @@ -3175,29 +3172,26 @@ } }, "node_modules/tough-cookie": { - "version": "4.1.4", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.4.tgz", - "integrity": "sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-5.0.0.tgz", + "integrity": "sha512-FRKsF7cz96xIIeMZ82ehjC3xW2E+O2+v11udrDYewUbszngYhsGa8z6YUMMzO9QJZzzyd0nGGXnML/TReX6W8Q==", "dev": true, "license": "BSD-3-Clause", "dependencies": { - "psl": "^1.1.33", - "punycode": "^2.1.1", - "universalify": "^0.2.0", - "url-parse": "^1.5.3" + "tldts": "^6.1.32" }, "engines": { - "node": ">=6" + "node": ">=16" } }, - "node_modules/tough-cookie/node_modules/universalify": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz", - "integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==", + "node_modules/tree-kill": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", + "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", "dev": true, "license": "MIT", - "engines": { - "node": ">= 4.0.0" + "bin": { + "tree-kill": "cli.js" } }, "node_modules/tslib": { @@ -3267,17 +3261,6 @@ "node": ">=8" } }, - "node_modules/url-parse": { - "version": "1.5.10", - "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", - "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "querystringify": "^2.1.1", - "requires-port": "^1.0.0" - } - }, "node_modules/uuid": { "version": "8.3.2", "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", diff --git a/cypress-tests-v2/package.json b/cypress-tests-v2/package.json index e06c1a63be20..20403f9e7774 100644 --- a/cypress-tests-v2/package.json +++ b/cypress-tests-v2/package.json @@ -15,9 +15,10 @@ "license": "ISC", "devDependencies": { "@types/fs-extra": "^11.0.4", - "cypress": "^13.14.2", + "cypress": "^13.15.2", "cypress-mochawesome-reporter": "^3.8.2", "jsqr": "^1.4.0", - "prettier": "^3.3.2" + "prettier": "^3.3.2", + "nanoid": "^5.0.8" } } From 1dfcaabff8a42c0ceb52215eca558fa1b297a929 Mon Sep 17 00:00:00 2001 From: Prasunna Soppa <70575890+prasunna09@users.noreply.github.com> Date: Fri, 8 Nov 2024 17:43:03 +0530 Subject: [PATCH 17/25] refactor(router): Remove card exp validation for migration api (#6460) Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com> --- .../router/src/core/payment_methods/cards.rs | 356 +++++++++++++++++- .../src/core/payment_methods/migration.rs | 47 +++ 2 files changed, 400 insertions(+), 3 deletions(-) diff --git a/crates/router/src/core/payment_methods/cards.rs b/crates/router/src/core/payment_methods/cards.rs index 990a0f0e9ad5..bcf0f9348627 100644 --- a/crates/router/src/core/payment_methods/cards.rs +++ b/crates/router/src/core/payment_methods/cards.rs @@ -76,7 +76,9 @@ use crate::{ }, core::{ errors::{self, StorageErrorExt}, - payment_methods::{network_tokenization, transformers as payment_methods, vault}, + payment_methods::{ + migration, network_tokenization, transformers as payment_methods, vault, + }, payments::{ helpers, routing::{self, SessionFlowRoutingInput}, @@ -410,7 +412,7 @@ pub async fn migrate_payment_method( card_number, &req, ); - get_client_secret_or_add_payment_method( + get_client_secret_or_add_payment_method_for_migration( &state, payment_method_create_request, merchant_account, @@ -448,7 +450,7 @@ pub async fn populate_bin_details_for_masked_card( card_details: &api_models::payment_methods::MigrateCardDetail, db: &dyn db::StorageInterface, ) -> errors::CustomResult { - helpers::validate_card_expiry(&card_details.card_exp_month, &card_details.card_exp_year)?; + migration::validate_card_expiry(&card_details.card_exp_month, &card_details.card_exp_year)?; let card_number = card_details.card_number.clone(); let (card_isin, _last4_digits) = get_card_bin_and_last4_digits_for_masked_card( @@ -887,6 +889,96 @@ pub async fn get_client_secret_or_add_payment_method( } } +#[cfg(all( + any(feature = "v1", feature = "v2"), + not(feature = "payment_methods_v2") +))] +#[instrument(skip_all)] +pub async fn get_client_secret_or_add_payment_method_for_migration( + state: &routes::SessionState, + req: api::PaymentMethodCreate, + merchant_account: &domain::MerchantAccount, + key_store: &domain::MerchantKeyStore, +) -> errors::RouterResponse { + let merchant_id = merchant_account.get_id(); + let customer_id = req.customer_id.clone().get_required_value("customer_id")?; + + #[cfg(not(feature = "payouts"))] + let condition = req.card.is_some(); + #[cfg(feature = "payouts")] + let condition = req.card.is_some() || req.bank_transfer.is_some() || req.wallet.is_some(); + let key_manager_state = state.into(); + let payment_method_billing_address: Option>> = req + .billing + .clone() + .async_map(|billing| create_encrypted_data(&key_manager_state, key_store, billing)) + .await + .transpose() + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Unable to encrypt Payment method billing address")?; + + let connector_mandate_details = req + .connector_mandate_details + .clone() + .map(serde_json::to_value) + .transpose() + .change_context(errors::ApiErrorResponse::InternalServerError)?; + + if condition { + Box::pin(save_migration_payment_method( + state, + req, + merchant_account, + key_store, + )) + .await + } else { + let payment_method_id = generate_id(consts::ID_LENGTH, "pm"); + + let res = create_payment_method( + state, + &req, + &customer_id, + payment_method_id.as_str(), + None, + merchant_id, + None, + None, + None, + key_store, + connector_mandate_details, + Some(enums::PaymentMethodStatus::AwaitingData), + None, + merchant_account.storage_scheme, + payment_method_billing_address.map(Into::into), + None, + None, + None, + None, + ) + .await?; + + if res.status == enums::PaymentMethodStatus::AwaitingData { + add_payment_method_status_update_task( + &*state.store, + &res, + enums::PaymentMethodStatus::AwaitingData, + enums::PaymentMethodStatus::Inactive, + merchant_id, + ) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable( + "Failed to add payment method status update task in process tracker", + )?; + } + + Ok(services::api::ApplicationResponse::Json( + api::PaymentMethodResponse::foreign_from((None, res)), + )) + } +} + #[instrument(skip_all)] pub fn authenticate_pm_client_secret_and_check_expiry( req_client_secret: &String, @@ -1380,6 +1472,264 @@ pub async fn add_payment_method( Ok(services::ApplicationResponse::Json(resp)) } +#[cfg(all( + any(feature = "v1", feature = "v2"), + not(feature = "payment_methods_v2") +))] +#[instrument(skip_all)] +pub async fn save_migration_payment_method( + state: &routes::SessionState, + req: api::PaymentMethodCreate, + merchant_account: &domain::MerchantAccount, + key_store: &domain::MerchantKeyStore, +) -> errors::RouterResponse { + req.validate()?; + let db = &*state.store; + let merchant_id = merchant_account.get_id(); + let customer_id = req.customer_id.clone().get_required_value("customer_id")?; + let payment_method = req.payment_method.get_required_value("payment_method")?; + let key_manager_state = state.into(); + let payment_method_billing_address: Option>> = req + .billing + .clone() + .async_map(|billing| create_encrypted_data(&key_manager_state, key_store, billing)) + .await + .transpose() + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Unable to encrypt Payment method billing address")?; + + let connector_mandate_details = req + .connector_mandate_details + .clone() + .map(serde_json::to_value) + .transpose() + .change_context(errors::ApiErrorResponse::InternalServerError)?; + + let response = match payment_method { + #[cfg(feature = "payouts")] + api_enums::PaymentMethod::BankTransfer => match req.bank_transfer.clone() { + Some(bank) => add_bank_to_locker( + state, + req.clone(), + merchant_account, + key_store, + &bank, + &customer_id, + ) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Add PaymentMethod Failed"), + _ => Ok(store_default_payment_method( + &req, + &customer_id, + merchant_id, + )), + }, + api_enums::PaymentMethod::Card => match req.card.clone() { + Some(card) => { + let mut card_details = card; + card_details = helpers::populate_bin_details_for_payment_method_create( + card_details.clone(), + db, + ) + .await; + migration::validate_card_expiry( + &card_details.card_exp_month, + &card_details.card_exp_year, + )?; + Box::pin(add_card_to_locker( + state, + req.clone(), + &card_details, + &customer_id, + merchant_account, + None, + )) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Add Card Failed") + } + _ => Ok(store_default_payment_method( + &req, + &customer_id, + merchant_id, + )), + }, + _ => Ok(store_default_payment_method( + &req, + &customer_id, + merchant_id, + )), + }; + + let (mut resp, duplication_check) = response?; + + match duplication_check { + Some(duplication_check) => match duplication_check { + payment_methods::DataDuplicationCheck::Duplicated => { + let existing_pm = get_or_insert_payment_method( + state, + req.clone(), + &mut resp, + merchant_account, + &customer_id, + key_store, + ) + .await?; + + resp.client_secret = existing_pm.client_secret; + } + payment_methods::DataDuplicationCheck::MetaDataChanged => { + if let Some(card) = req.card.clone() { + let existing_pm = get_or_insert_payment_method( + state, + req.clone(), + &mut resp, + merchant_account, + &customer_id, + key_store, + ) + .await?; + + let client_secret = existing_pm.client_secret.clone(); + + delete_card_from_locker( + state, + &customer_id, + merchant_id, + existing_pm + .locker_id + .as_ref() + .unwrap_or(&existing_pm.payment_method_id), + ) + .await?; + + let add_card_resp = add_card_hs( + state, + req.clone(), + &card, + &customer_id, + merchant_account, + api::enums::LockerChoice::HyperswitchCardVault, + Some( + existing_pm + .locker_id + .as_ref() + .unwrap_or(&existing_pm.payment_method_id), + ), + ) + .await; + + if let Err(err) = add_card_resp { + logger::error!(vault_err=?err); + db.delete_payment_method_by_merchant_id_payment_method_id( + &(state.into()), + key_store, + merchant_id, + &resp.payment_method_id, + ) + .await + .to_not_found_response(errors::ApiErrorResponse::PaymentMethodNotFound)?; + + Err(report!(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed while updating card metadata changes"))? + }; + + let existing_pm_data = + get_card_details_without_locker_fallback(&existing_pm, state).await?; + + let updated_card = Some(api::CardDetailFromLocker { + scheme: existing_pm.scheme.clone(), + last4_digits: Some(card.card_number.get_last4()), + issuer_country: card + .card_issuing_country + .or(existing_pm_data.issuer_country), + card_isin: Some(card.card_number.get_card_isin()), + card_number: Some(card.card_number), + expiry_month: Some(card.card_exp_month), + expiry_year: Some(card.card_exp_year), + card_token: None, + card_fingerprint: None, + card_holder_name: card + .card_holder_name + .or(existing_pm_data.card_holder_name), + nick_name: card.nick_name.or(existing_pm_data.nick_name), + card_network: card.card_network.or(existing_pm_data.card_network), + card_issuer: card.card_issuer.or(existing_pm_data.card_issuer), + card_type: card.card_type.or(existing_pm_data.card_type), + saved_to_locker: true, + }); + + let updated_pmd = updated_card.as_ref().map(|card| { + PaymentMethodsData::Card(CardDetailsPaymentMethod::from(card.clone())) + }); + let pm_data_encrypted: Option>> = + updated_pmd + .async_map(|updated_pmd| { + create_encrypted_data(&key_manager_state, key_store, updated_pmd) + }) + .await + .transpose() + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Unable to encrypt payment method data")?; + + let pm_update = storage::PaymentMethodUpdate::PaymentMethodDataUpdate { + payment_method_data: pm_data_encrypted.map(Into::into), + }; + + db.update_payment_method( + &(state.into()), + key_store, + existing_pm, + pm_update, + merchant_account.storage_scheme, + ) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed to add payment method in db")?; + + resp.client_secret = client_secret; + } + } + }, + None => { + let pm_metadata = resp.metadata.as_ref().map(|data| data.peek()); + + let locker_id = if resp.payment_method == Some(api_enums::PaymentMethod::Card) + || resp.payment_method == Some(api_enums::PaymentMethod::BankTransfer) + { + Some(resp.payment_method_id) + } else { + None + }; + resp.payment_method_id = generate_id(consts::ID_LENGTH, "pm"); + let pm = insert_payment_method( + state, + &resp, + &req, + key_store, + merchant_id, + &customer_id, + pm_metadata.cloned(), + None, + locker_id, + connector_mandate_details, + req.network_transaction_id.clone(), + merchant_account.storage_scheme, + payment_method_billing_address.map(Into::into), + None, + None, + None, + ) + .await?; + + resp.client_secret = pm.client_secret; + } + } + + Ok(services::ApplicationResponse::Json(resp)) +} + #[cfg(all( any(feature = "v1", feature = "v2"), not(feature = "payment_methods_v2") diff --git a/crates/router/src/core/payment_methods/migration.rs b/crates/router/src/core/payment_methods/migration.rs index af9e01d253c8..b0aaa767a4e1 100644 --- a/crates/router/src/core/payment_methods/migration.rs +++ b/crates/router/src/core/payment_methods/migration.rs @@ -2,7 +2,9 @@ use actix_multipart::form::{bytes::Bytes, text::Text, MultipartForm}; use api_models::payment_methods::{PaymentMethodMigrationResponse, PaymentMethodRecord}; use csv::Reader; use error_stack::ResultExt; +use masking::PeekInterface; use rdkafka::message::ToBytes; +use router_env::{instrument, tracing}; use crate::{ core::{errors, payment_methods::cards::migrate_payment_method}, @@ -102,3 +104,48 @@ pub fn get_payment_method_records( }), } } + +#[instrument(skip_all)] +pub fn validate_card_expiry( + card_exp_month: &masking::Secret, + card_exp_year: &masking::Secret, +) -> errors::CustomResult<(), errors::ApiErrorResponse> { + let exp_month = card_exp_month + .peek() + .to_string() + .parse::() + .change_context(errors::ApiErrorResponse::InvalidDataValue { + field_name: "card_exp_month", + })?; + ::cards::CardExpirationMonth::try_from(exp_month).change_context( + errors::ApiErrorResponse::PreconditionFailed { + message: "Invalid Expiry Month".to_string(), + }, + )?; + + let year_str = card_exp_year.peek().to_string(); + + validate_card_exp_year(year_str).change_context( + errors::ApiErrorResponse::PreconditionFailed { + message: "Invalid Expiry Year".to_string(), + }, + )?; + + Ok(()) +} + +fn validate_card_exp_year(year: String) -> Result<(), errors::ValidationError> { + let year_str = year.to_string(); + if year_str.len() == 2 || year_str.len() == 4 { + year_str + .parse::() + .map_err(|_| errors::ValidationError::InvalidValue { + message: "card_exp_year".to_string(), + })?; + Ok(()) + } else { + Err(errors::ValidationError::InvalidValue { + message: "invalid card expiration year".to_string(), + }) + } +} From 3d9f4432bcef8a5326d1bdabbc2be5bd0df9fd73 Mon Sep 17 00:00:00 2001 From: Debarati Ghatak <88573135+cookieg13@users.noreply.github.com> Date: Fri, 8 Nov 2024 17:56:08 +0530 Subject: [PATCH 18/25] fix(connector): [Novalnet] Add mandatory fields for wallets and card in config (#6463) --- .../payment_connector_required_fields.rs | 349 +++++++++++++++++- 1 file changed, 347 insertions(+), 2 deletions(-) diff --git a/crates/router/src/configs/defaults/payment_connector_required_fields.rs b/crates/router/src/configs/defaults/payment_connector_required_fields.rs index 28ddeec1fcd3..1c02619a8e97 100644 --- a/crates/router/src/configs/defaults/payment_connector_required_fields.rs +++ b/crates/router/src/configs/defaults/payment_connector_required_fields.rs @@ -33,6 +33,7 @@ impl Default for Mandates { enums::Connector::Globalpay, enums::Connector::Multisafepay, enums::Connector::Bankofamerica, + enums::Connector::Novalnet, enums::Connector::Noon, enums::Connector::Cybersource, enums::Connector::Wellsfargo, @@ -47,6 +48,7 @@ impl Default for Mandates { enums::Connector::Adyen, enums::Connector::Bankofamerica, enums::Connector::Cybersource, + enums::Connector::Novalnet, enums::Connector::Wellsfargo, ]), }, @@ -68,6 +70,7 @@ impl Default for Mandates { enums::Connector::Multisafepay, enums::Connector::Nexinets, enums::Connector::Noon, + enums::Connector::Novalnet, enums::Connector::Payme, enums::Connector::Stripe, enums::Connector::Bankofamerica, @@ -88,6 +91,7 @@ impl Default for Mandates { enums::Connector::Multisafepay, enums::Connector::Nexinets, enums::Connector::Noon, + enums::Connector::Novalnet, enums::Connector::Payme, enums::Connector::Stripe, ]), @@ -2067,7 +2071,7 @@ impl Default for settings::RequiredFields { "billing.address.line2".to_string(), RequiredFieldInfo { required_field: "payment_method_data.billing.address.line2".to_string(), - display_name: "line1".to_string(), + display_name: "line2".to_string(), field_type: enums::FieldType::UserAddressLine2, value: None, } @@ -2117,6 +2121,19 @@ impl Default for settings::RequiredFields { value: None, } ), + ( + "billing.address.country".to_string(), + RequiredFieldInfo { + required_field: "payment_method_data.billing.address.country".to_string(), + display_name: "country".to_string(), + field_type: enums::FieldType::UserAddressCountry{ + options: vec![ + "ALL".to_string(), + ] + }, + value: None, + } + ), ] ), } @@ -5256,7 +5273,7 @@ impl Default for settings::RequiredFields { "billing.address.line2".to_string(), RequiredFieldInfo { required_field: "payment_method_data.billing.address.line2".to_string(), - display_name: "line1".to_string(), + display_name: "line2".to_string(), field_type: enums::FieldType::UserAddressLine2, value: None, } @@ -5306,6 +5323,19 @@ impl Default for settings::RequiredFields { value: None, } ), + ( + "billing.address.country".to_string(), + RequiredFieldInfo { + required_field: "payment_method_data.billing.address.country".to_string(), + display_name: "country".to_string(), + field_type: enums::FieldType::UserAddressCountry{ + options: vec![ + "ALL".to_string(), + ] + }, + value: None, + } + ), ] ), } @@ -8200,6 +8230,111 @@ impl Default for settings::RequiredFields { common: HashMap::new(), } ), + ( + enums::Connector::Novalnet, + RequiredFieldFinal { + mandate: HashMap::new(), + non_mandate: HashMap::new(), + common: HashMap::from( + [ + ( + "browser_info.language".to_string(), + RequiredFieldInfo { + required_field: "browser_info.language".to_string(), + display_name: "browser_info_language".to_string(), + field_type: enums::FieldType::BrowserLanguage, + value: None, + } + ), + ( + "browser_info.ip_address".to_string(), + RequiredFieldInfo { + required_field: "browser_info.ip_address".to_string(), + display_name: "browser_info_ip_address".to_string(), + field_type: enums::FieldType::BrowserIp, + value: None, + } + ), + ( + "billing.address.line1".to_string(), + RequiredFieldInfo { + required_field: "payment_method_data.billing.address.line1".to_string(), + display_name: "line1".to_string(), + field_type: enums::FieldType::UserAddressLine1, + value: None, + } + ), + ( + "billing.address.line2".to_string(), + RequiredFieldInfo { + required_field: "payment_method_data.billing.address.line2".to_string(), + display_name: "line2".to_string(), + field_type: enums::FieldType::UserAddressLine2, + value: None, + } + ), + ( + "billing.address.city".to_string(), + RequiredFieldInfo { + required_field: "payment_method_data.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: "payment_method_data.billing.address.zip".to_string(), + display_name: "zip".to_string(), + field_type: enums::FieldType::UserAddressPincode, + value: None, + } + ), + ( + "billing.address.first_name".to_string(), + RequiredFieldInfo { + required_field: "payment_method_data.billing.address.first_name".to_string(), + display_name: "first_name".to_string(), + field_type: enums::FieldType::UserFullName, + value: None, + } + ), + ( + "billing.address.last_name".to_string(), + RequiredFieldInfo { + required_field: "payment_method_data.billing.address.last_name".to_string(), + display_name: "last_name".to_string(), + field_type: enums::FieldType::UserFullName, + value: None, + } + ), + ( + "billing.email".to_string(), + RequiredFieldInfo { + required_field: "payment_method_data.billing.email".to_string(), + display_name: "email_address".to_string(), + field_type: enums::FieldType::UserEmailAddress, + value: None, + } + ), + ( + "billing.address.country".to_string(), + RequiredFieldInfo { + required_field: "payment_method_data.billing.address.country".to_string(), + display_name: "country".to_string(), + field_type: enums::FieldType::UserAddressCountry{ + options: vec![ + "ALL".to_string(), + ] + }, + value: None, + } + ), + ] + ), + } + ), ( enums::Connector::Wellsfargo, RequiredFieldFinal { @@ -8473,6 +8608,111 @@ impl Default for settings::RequiredFields { common: HashMap::new(), } ), + ( + enums::Connector::Novalnet, + RequiredFieldFinal { + mandate: HashMap::new(), + non_mandate: HashMap::new(), + common: HashMap::from( + [ + ( + "browser_info.language".to_string(), + RequiredFieldInfo { + required_field: "browser_info.language".to_string(), + display_name: "browser_info_language".to_string(), + field_type: enums::FieldType::BrowserLanguage, + value: None, + } + ), + ( + "browser_info.ip_address".to_string(), + RequiredFieldInfo { + required_field: "browser_info.ip_address".to_string(), + display_name: "browser_info_ip_address".to_string(), + field_type: enums::FieldType::BrowserIp, + value: None, + } + ), + ( + "billing.address.line1".to_string(), + RequiredFieldInfo { + required_field: "payment_method_data.billing.address.line1".to_string(), + display_name: "line1".to_string(), + field_type: enums::FieldType::UserAddressLine1, + value: None, + } + ), + ( + "billing.address.line2".to_string(), + RequiredFieldInfo { + required_field: "payment_method_data.billing.address.line2".to_string(), + display_name: "line2".to_string(), + field_type: enums::FieldType::UserAddressLine2, + value: None, + } + ), + ( + "billing.address.city".to_string(), + RequiredFieldInfo { + required_field: "payment_method_data.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: "payment_method_data.billing.address.zip".to_string(), + display_name: "zip".to_string(), + field_type: enums::FieldType::UserAddressPincode, + value: None, + } + ), + ( + "billing.address.first_name".to_string(), + RequiredFieldInfo { + required_field: "payment_method_data.billing.address.first_name".to_string(), + display_name: "first_name".to_string(), + field_type: enums::FieldType::UserFullName, + value: None, + } + ), + ( + "billing.address.last_name".to_string(), + RequiredFieldInfo { + required_field: "payment_method_data.billing.address.last_name".to_string(), + display_name: "last_name".to_string(), + field_type: enums::FieldType::UserFullName, + value: None, + } + ), + ( + "billing.email".to_string(), + RequiredFieldInfo { + required_field: "payment_method_data.billing.email".to_string(), + display_name: "email_address".to_string(), + field_type: enums::FieldType::UserEmailAddress, + value: None, + } + ), + ( + "billing.address.country".to_string(), + RequiredFieldInfo { + required_field: "payment_method_data.billing.address.country".to_string(), + display_name: "country".to_string(), + field_type: enums::FieldType::UserAddressCountry{ + options: vec![ + "ALL".to_string(), + ] + }, + value: None, + } + ), + ] + ), + } + ), ( enums::Connector::Nuvei, RequiredFieldFinal { @@ -9136,6 +9376,111 @@ impl Default for settings::RequiredFields { common: HashMap::new(), } ), + ( + enums::Connector::Novalnet, + RequiredFieldFinal { + mandate: HashMap::new(), + non_mandate: HashMap::new(), + common: HashMap::from( + [ + ( + "browser_info.language".to_string(), + RequiredFieldInfo { + required_field: "browser_info.language".to_string(), + display_name: "browser_info_language".to_string(), + field_type: enums::FieldType::BrowserLanguage, + value: None, + } + ), + ( + "browser_info.ip_address".to_string(), + RequiredFieldInfo { + required_field: "browser_info.ip_address".to_string(), + display_name: "browser_info_ip_address".to_string(), + field_type: enums::FieldType::BrowserIp, + value: None, + } + ), + ( + "billing.address.line1".to_string(), + RequiredFieldInfo { + required_field: "payment_method_data.billing.address.line1".to_string(), + display_name: "line1".to_string(), + field_type: enums::FieldType::UserAddressLine1, + value: None, + } + ), + ( + "billing.address.line2".to_string(), + RequiredFieldInfo { + required_field: "payment_method_data.billing.address.line2".to_string(), + display_name: "line2".to_string(), + field_type: enums::FieldType::UserAddressLine2, + value: None, + } + ), + ( + "billing.address.city".to_string(), + RequiredFieldInfo { + required_field: "payment_method_data.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: "payment_method_data.billing.address.zip".to_string(), + display_name: "zip".to_string(), + field_type: enums::FieldType::UserAddressPincode, + value: None, + } + ), + ( + "billing.address.first_name".to_string(), + RequiredFieldInfo { + required_field: "payment_method_data.billing.address.first_name".to_string(), + display_name: "first_name".to_string(), + field_type: enums::FieldType::UserFullName, + value: None, + } + ), + ( + "billing.address.last_name".to_string(), + RequiredFieldInfo { + required_field: "payment_method_data.billing.address.last_name".to_string(), + display_name: "last_name".to_string(), + field_type: enums::FieldType::UserFullName, + value: None, + } + ), + ( + "billing.email".to_string(), + RequiredFieldInfo { + required_field: "payment_method_data.billing.email".to_string(), + display_name: "email_address".to_string(), + field_type: enums::FieldType::UserEmailAddress, + value: None, + } + ), + ( + "billing.address.country".to_string(), + RequiredFieldInfo { + required_field: "payment_method_data.billing.address.country".to_string(), + display_name: "country".to_string(), + field_type: enums::FieldType::UserAddressCountry{ + options: vec![ + "ALL".to_string(), + ] + }, + value: None, + } + ), + ] + ), + } + ), ( enums::Connector::Paypal, RequiredFieldFinal { From 1be2654b4fd61a9d6a9e3b3772d9bffd8f1333dc Mon Sep 17 00:00:00 2001 From: Rutam Prita Mishra Date: Fri, 8 Nov 2024 17:59:57 +0530 Subject: [PATCH 19/25] feat(payments): Add audit events for PaymentUpdate update (#6426) --- .../src/core/payments/operations/payment_update.rs | 11 +++++++++-- crates/router/src/events/audit_events.rs | 5 +++++ 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/crates/router/src/core/payments/operations/payment_update.rs b/crates/router/src/core/payments/operations/payment_update.rs index 9005e94c9629..2f1a0c333fa6 100644 --- a/crates/router/src/core/payments/operations/payment_update.rs +++ b/crates/router/src/core/payments/operations/payment_update.rs @@ -25,6 +25,7 @@ use crate::{ payments::{self, helpers, operations, CustomerDetails, PaymentAddress, PaymentData}, utils as core_utils, }, + events::audit_events::{AuditEvent, AuditEventType}, routes::{app::ReqState, SessionState}, services, types::{ @@ -694,7 +695,7 @@ impl UpdateTracker, api::PaymentsRequest> for Paymen async fn update_trackers<'b>( &'b self, _state: &'b SessionState, - _req_state: ReqState, + req_state: ReqState, mut _payment_data: PaymentData, _customer: Option, _storage_scheme: storage_enums::MerchantStorageScheme, @@ -714,7 +715,7 @@ impl UpdateTracker, api::PaymentsRequest> for Paymen async fn update_trackers<'b>( &'b self, state: &'b SessionState, - _req_state: ReqState, + req_state: ReqState, mut payment_data: PaymentData, customer: Option, storage_scheme: storage_enums::MerchantStorageScheme, @@ -925,6 +926,12 @@ impl UpdateTracker, api::PaymentsRequest> for Paymen ) .await .to_not_found_response(errors::ApiErrorResponse::PaymentNotFound)?; + let amount = payment_data.amount; + req_state + .event_context + .event(AuditEvent::new(AuditEventType::PaymentUpdate { amount })) + .with(payment_data.to_event()) + .emit(); Ok(( payments::is_confirm(self, payment_data.confirm), diff --git a/crates/router/src/events/audit_events.rs b/crates/router/src/events/audit_events.rs index 9fbd754b43c0..a6b0884d21de 100644 --- a/crates/router/src/events/audit_events.rs +++ b/crates/router/src/events/audit_events.rs @@ -1,3 +1,4 @@ +use api_models::payments::Amount; use common_utils::types::MinorUnit; use diesel_models::fraud_check::FraudCheck; use events::{Event, EventInfo}; @@ -27,6 +28,9 @@ pub enum AuditEventType { capture_amount: Option, multiple_capture_count: Option, }, + PaymentUpdate { + amount: Amount, + }, PaymentApprove, PaymentCreate, } @@ -67,6 +71,7 @@ impl Event for AuditEvent { AuditEventType::RefundSuccess => "refund_success", AuditEventType::RefundFail => "refund_fail", AuditEventType::PaymentCancelled { .. } => "payment_cancelled", + AuditEventType::PaymentUpdate { .. } => "payment_update", AuditEventType::PaymentApprove { .. } => "payment_approve", AuditEventType::PaymentCreate { .. } => "payment_create", }; From 378ec44db9752020083d61a538592d5383a06b40 Mon Sep 17 00:00:00 2001 From: Kashif Date: Fri, 8 Nov 2024 18:00:39 +0530 Subject: [PATCH 20/25] feat(connector): [worldpay] add support for mandates (#6479) Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com> --- .../src/connectors/worldpay.rs | 179 +++++++- .../src/connectors/worldpay/requests.rs | 49 +++ .../src/connectors/worldpay/response.rs | 13 + .../src/connectors/worldpay/transformers.rs | 413 ++++++++++++++---- crates/hyperswitch_connectors/src/utils.rs | 6 + .../src/errors/api_error_response.rs | 2 +- crates/router/src/consts.rs | 3 - .../payments/operations/payment_response.rs | 2 +- crates/router/src/lib.rs | 1 - 9 files changed, 549 insertions(+), 119 deletions(-) diff --git a/crates/hyperswitch_connectors/src/connectors/worldpay.rs b/crates/hyperswitch_connectors/src/connectors/worldpay.rs index aaddae176667..fd67bb60128a 100644 --- a/crates/hyperswitch_connectors/src/connectors/worldpay.rs +++ b/crates/hyperswitch_connectors/src/connectors/worldpay.rs @@ -13,6 +13,7 @@ use common_utils::{ }; use error_stack::ResultExt; use hyperswitch_domain_models::{ + payment_method_data::PaymentMethodData, router_data::{AccessToken, ConnectorAuthType, ErrorResponse, RouterData}, router_flow_types::{ access_token_auth::AccessTokenAuth, @@ -29,7 +30,7 @@ use hyperswitch_domain_models::{ types::{ PaymentsAuthorizeRouterData, PaymentsCancelRouterData, PaymentsCaptureRouterData, PaymentsCompleteAuthorizeRouterData, PaymentsSyncRouterData, RefundExecuteRouterData, - RefundSyncRouterData, RefundsRouterData, + RefundSyncRouterData, RefundsRouterData, SetupMandateRouterData, }, }; use hyperswitch_interfaces::{ @@ -50,15 +51,17 @@ use requests::{ use response::{ EventType, ResponseIdStr, WorldpayErrorResponse, WorldpayEventResponse, WorldpayPaymentsResponse, WorldpayWebhookEventType, WorldpayWebhookTransactionId, + WP_CORRELATION_ID, }; -use transformers::{self as worldpay, WP_CORRELATION_ID}; +use ring::hmac; +use transformers::{self as worldpay}; use crate::{ constants::headers, types::ResponseRouterData, utils::{ construct_not_implemented_error_report, convert_amount, get_header_key_value, - ForeignTryFrom, RefundsRequestData, + is_mandate_supported, ForeignTryFrom, PaymentMethodDataType, RefundsRequestData, }, }; @@ -171,6 +174,19 @@ impl ConnectorValidation for Worldpay { ), } } + + fn validate_mandate_payment( + &self, + pm_type: Option, + pm_data: PaymentMethodData, + ) -> CustomResult<(), errors::ConnectorError> { + let mandate_supported_pmd = std::collections::HashSet::from([PaymentMethodDataType::Card]); + is_mandate_supported(pm_data.clone(), pm_type, mandate_supported_pmd, self.id()) + } + + fn is_webhook_source_verification_mandatory(&self) -> bool { + true + } } impl api::Payment for Worldpay {} @@ -179,15 +195,108 @@ impl api::MandateSetup for Worldpay {} impl ConnectorIntegration for Worldpay { - fn build_request( + fn get_headers( + &self, + req: &SetupMandateRouterData, + connectors: &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: &SetupMandateRouterData, + connectors: &Connectors, + ) -> CustomResult { + Ok(format!("{}api/payments", self.base_url(connectors))) + } + + fn get_request_body( &self, - _req: &RouterData, + req: &SetupMandateRouterData, _connectors: &Connectors, + ) -> CustomResult { + let auth = worldpay::WorldpayAuthType::try_from(&req.connector_auth_type) + .change_context(errors::ConnectorError::FailedToObtainAuthType)?; + let connector_router_data = worldpay::WorldpayRouterData::try_from(( + &self.get_currency_unit(), + req.request.currency, + req.request.minor_amount.unwrap_or_default(), + req, + ))?; + let connector_req = + WorldpayPaymentsRequest::try_from((&connector_router_data, &auth.entity_id))?; + + Ok(RequestContent::Json(Box::new(connector_req))) + } + + fn build_request( + &self, + req: &SetupMandateRouterData, + connectors: &Connectors, ) -> CustomResult, errors::ConnectorError> { - Err( - errors::ConnectorError::NotImplemented("Setup Mandate flow for Worldpay".to_string()) - .into(), - ) + Ok(Some( + RequestBuilder::new() + .method(Method::Post) + .url(&types::SetupMandateType::get_url(self, req, connectors)?) + .attach_default_headers() + .headers(types::SetupMandateType::get_headers(self, req, connectors)?) + .set_body(types::SetupMandateType::get_request_body( + self, req, connectors, + )?) + .build(), + )) + } + + fn handle_response( + &self, + data: &SetupMandateRouterData, + event_builder: Option<&mut ConnectorEvent>, + res: Response, + ) -> CustomResult { + let response: WorldpayPaymentsResponse = res + .response + .parse_struct("Worldpay PaymentsResponse") + .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + + event_builder.map(|i| i.set_response_body(&response)); + router_env::logger::info!(connector_response=?response); + let optional_correlation_id = res.headers.and_then(|headers| { + headers + .get(WP_CORRELATION_ID) + .and_then(|header_value| header_value.to_str().ok()) + .map(|id| id.to_string()) + }); + + RouterData::foreign_try_from(( + ResponseRouterData { + response, + data: data.clone(), + http_code: res.status_code, + }, + optional_correlation_id, + )) + .change_context(errors::ConnectorError::ResponseHandlingFailed) + } + + fn get_error_response( + &self, + res: Response, + event_builder: Option<&mut ConnectorEvent>, + ) -> CustomResult { + self.build_error_response(res, event_builder) + } + + fn get_5xx_error_response( + &self, + res: Response, + event_builder: Option<&mut ConnectorEvent>, + ) -> CustomResult { + self.build_error_response(res, event_builder) } } @@ -401,6 +510,7 @@ impl ConnectorIntegration for Wor enums::AttemptStatus::Authorizing | enums::AttemptStatus::Authorized | enums::AttemptStatus::CaptureInitiated + | enums::AttemptStatus::Charged | enums::AttemptStatus::Pending | enums::AttemptStatus::VoidInitiated, EventType::Authorized, @@ -587,6 +697,7 @@ impl ConnectorIntegration, _merchant_id: &common_utils::id_type::MerchantId, - connector_webhook_secrets: &api_models::webhooks::ConnectorWebhookSecrets, + _connector_webhook_secrets: &api_models::webhooks::ConnectorWebhookSecrets, ) -> CustomResult, errors::ConnectorError> { - let secret_str = std::str::from_utf8(&connector_webhook_secrets.secret) - .change_context(errors::ConnectorError::WebhookBodyDecodingFailed)?; - let to_sign = format!( - "{}{}", - secret_str, - std::str::from_utf8(request.body) - .change_context(errors::ConnectorError::WebhookBodyDecodingFailed)? - ); - Ok(to_sign.into_bytes()) + Ok(request.body.to_vec()) + } + + async fn verify_webhook_source( + &self, + request: &IncomingWebhookRequestDetails<'_>, + merchant_id: &common_utils::id_type::MerchantId, + connector_webhook_details: Option, + _connector_account_details: crypto::Encryptable>, + connector_label: &str, + ) -> CustomResult { + let connector_webhook_secrets = self + .get_webhook_source_verification_merchant_secret( + merchant_id, + connector_label, + connector_webhook_details, + ) + .await + .change_context(errors::ConnectorError::WebhookSourceVerificationFailed)?; + let signature = self + .get_webhook_source_verification_signature(request, &connector_webhook_secrets) + .change_context(errors::ConnectorError::WebhookSourceVerificationFailed)?; + let message = self + .get_webhook_source_verification_message( + request, + merchant_id, + &connector_webhook_secrets, + ) + .change_context(errors::ConnectorError::WebhookSourceVerificationFailed)?; + let secret_key = hex::decode(connector_webhook_secrets.secret) + .change_context(errors::ConnectorError::WebhookVerificationSecretInvalid)?; + + let signing_key = hmac::Key::new(hmac::HMAC_SHA256, &secret_key); + let signed_message = hmac::sign(&signing_key, &message); + let computed_signature = hex::encode(signed_message.as_ref()); + + Ok(computed_signature.as_bytes() == hex::encode(signature).as_bytes()) } fn get_webhook_object_reference_id( diff --git a/crates/hyperswitch_connectors/src/connectors/worldpay/requests.rs b/crates/hyperswitch_connectors/src/connectors/worldpay/requests.rs index b0fa85a64c36..884caa9e840b 100644 --- a/crates/hyperswitch_connectors/src/connectors/worldpay/requests.rs +++ b/crates/hyperswitch_connectors/src/connectors/worldpay/requests.rs @@ -24,6 +24,7 @@ pub struct Merchant { #[derive(Clone, Debug, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] pub struct Instruction { + #[serde(skip_serializing_if = "Option::is_none")] pub settlement: Option, pub method: PaymentMethod, pub payment_instrument: PaymentInstrument, @@ -33,6 +34,43 @@ pub struct Instruction { pub debt_repayment: Option, #[serde(rename = "threeDS")] pub three_ds: Option, + /// For setting up mandates + pub token_creation: Option, + /// For specifying CIT vs MIT + pub customer_agreement: Option, +} + +#[derive(Clone, Debug, PartialEq, Serialize)] +pub struct TokenCreation { + #[serde(rename = "type")] + pub token_type: TokenCreationType, +} + +#[derive(Clone, Debug, PartialEq, Serialize)] +#[serde(rename_all = "lowercase")] +pub enum TokenCreationType { + Worldpay, +} + +#[derive(Clone, Debug, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct CustomerAgreement { + #[serde(rename = "type")] + pub agreement_type: CustomerAgreementType, + pub stored_card_usage: StoredCardUsageType, +} + +#[derive(Clone, Debug, PartialEq, Serialize)] +#[serde(rename_all = "lowercase")] +pub enum CustomerAgreementType { + Subscription, +} + +#[derive(Clone, Debug, PartialEq, Serialize)] +#[serde(rename_all = "lowercase")] +pub enum StoredCardUsageType { + First, + Subsequent, } #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] @@ -225,6 +263,14 @@ pub enum ThreeDSRequestChannel { #[serde(rename_all = "camelCase")] pub struct ThreeDSRequestChallenge { pub return_url: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub preference: Option, +} + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub enum ThreeDsPreference { + ChallengeMandated, } #[derive(Clone, Debug, PartialEq, Default, Serialize, Deserialize)] @@ -284,3 +330,6 @@ pub struct WorldpayCompleteAuthorizationRequest { #[serde(skip_serializing_if = "Option::is_none")] pub collection_reference: Option, } + +pub(super) const THREE_DS_MODE: &str = "always"; +pub(super) const THREE_DS_TYPE: &str = "integrated"; diff --git a/crates/hyperswitch_connectors/src/connectors/worldpay/response.rs b/crates/hyperswitch_connectors/src/connectors/worldpay/response.rs index 2b090bf02ac3..5e9fb0304243 100644 --- a/crates/hyperswitch_connectors/src/connectors/worldpay/response.rs +++ b/crates/hyperswitch_connectors/src/connectors/worldpay/response.rs @@ -41,6 +41,16 @@ pub struct AuthorizedResponse { pub description: Option, pub risk_factors: Option>, pub fraud: Option, + /// Mandate's token + pub token: Option, +} + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct MandateToken { + pub href: Secret, + pub token_id: String, + pub token_expiry_date_time: String, } #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] @@ -445,3 +455,6 @@ pub enum WorldpayWebhookStatus { SentForRefund, RefundFailed, } + +/// Worldpay's unique reference ID for a request +pub(super) const WP_CORRELATION_ID: &str = "WP-CorrelationId"; diff --git a/crates/hyperswitch_connectors/src/connectors/worldpay/transformers.rs b/crates/hyperswitch_connectors/src/connectors/worldpay/transformers.rs index daaa095c3149..b69e4bcd97b6 100644 --- a/crates/hyperswitch_connectors/src/connectors/worldpay/transformers.rs +++ b/crates/hyperswitch_connectors/src/connectors/worldpay/transformers.rs @@ -1,6 +1,6 @@ use std::collections::HashMap; -use api_models::payments::Address; +use api_models::payments::{Address, MandateIds, MandateReferenceId}; use base64::Engine; use common_enums::enums; use common_utils::{ @@ -10,9 +10,11 @@ use error_stack::ResultExt; use hyperswitch_domain_models::{ payment_method_data::{PaymentMethodData, WalletData}, router_data::{ConnectorAuthType, ErrorResponse, RouterData}, - router_flow_types::Authorize, - router_request_types::{PaymentsAuthorizeData, ResponseId}, - router_response_types::{PaymentsResponseData, RedirectForm}, + router_flow_types::{Authorize, SetupMandate}, + router_request_types::{ + BrowserInformation, PaymentsAuthorizeData, ResponseId, SetupMandateRequestData, + }, + router_response_types::{MandateReference, PaymentsResponseData, RedirectForm}, types, }; use hyperswitch_interfaces::{api, errors}; @@ -22,7 +24,10 @@ use serde::{Deserialize, Serialize}; use super::{requests::*, response::*}; use crate::{ types::ResponseRouterData, - utils::{self, AddressData, ForeignTryFrom, PaymentsAuthorizeRequestData, RouterData as _}, + utils::{ + self, AddressData, ForeignTryFrom, PaymentsAuthorizeRequestData, + PaymentsSetupMandateRequestData, RouterData as RouterDataTrait, + }, }; #[derive(Debug, Serialize)] @@ -47,18 +52,15 @@ impl TryFrom<(&api::CurrencyUnit, enums::Currency, MinorUnit, T)> for Worldpa } } -/// Worldpay's unique reference ID for a request -pub const WP_CORRELATION_ID: &str = "WP-CorrelationId"; - #[derive(Debug, Default, Serialize, Deserialize)] pub struct WorldpayConnectorMetadataObject { pub merchant_name: Option>, } -impl TryFrom<&Option> for WorldpayConnectorMetadataObject { +impl TryFrom> for WorldpayConnectorMetadataObject { type Error = error_stack::Report; - fn try_from(meta_data: &Option) -> Result { - let metadata: Self = utils::to_connector_meta_from_secret::(meta_data.clone()) + fn try_from(meta_data: Option<&pii::SecretSerdeValue>) -> Result { + let metadata: Self = utils::to_connector_meta_from_secret::(meta_data.cloned()) .change_context(errors::ConnectorError::InvalidConnectorConfig { config: "metadata", })?; @@ -69,6 +71,7 @@ impl TryFrom<&Option> for WorldpayConnectorMetadataObject fn fetch_payment_instrument( payment_method: PaymentMethodData, billing_address: Option<&Address>, + mandate_ids: Option, ) -> CustomResult { match payment_method { PaymentMethodData::Card(card) => Ok(PaymentInstrument::Card(CardPayment { @@ -103,6 +106,29 @@ fn fetch_payment_instrument( None }, })), + PaymentMethodData::MandatePayment => mandate_ids + .and_then(|mandate_ids| { + mandate_ids + .mandate_reference_id + .and_then(|mandate_id| match mandate_id { + MandateReferenceId::ConnectorMandateId(connector_mandate_id) => { + connector_mandate_id.get_connector_mandate_id().map(|href| { + PaymentInstrument::CardToken(CardToken { + payment_type: PaymentType::Token, + href, + cvc: None, + }) + }) + } + _ => None, + }) + }) + .ok_or( + errors::ConnectorError::MissingRequiredField { + field_name: "connector_mandate_id", + } + .into(), + ), PaymentMethodData::Wallet(wallet) => match wallet { WalletData::GooglePay(data) => Ok(PaymentInstrument::Googlepay(WalletPayment { payment_type: PaymentType::Encrypted, @@ -149,7 +175,6 @@ fn fetch_payment_instrument( | PaymentMethodData::BankDebit(_) | PaymentMethodData::BankTransfer(_) | PaymentMethodData::Crypto(_) - | PaymentMethodData::MandatePayment | PaymentMethodData::Reward | PaymentMethodData::RealTimePayment(_) | PaymentMethodData::Upi(_) @@ -196,109 +221,300 @@ impl TryFrom<(enums::PaymentMethod, Option)> for Payme } } -impl - TryFrom<( - &WorldpayRouterData<&RouterData>, - &Secret, - )> for WorldpayPaymentsRequest +// Trait to abstract common functionality between Authorize and SetupMandate +trait WorldpayPaymentsRequestData { + fn get_return_url(&self) -> Result>; + fn get_auth_type(&self) -> &enums::AuthenticationType; + fn get_browser_info(&self) -> Option<&BrowserInformation>; + fn get_payment_method_data(&self) -> &PaymentMethodData; + fn get_setup_future_usage(&self) -> Option; + fn get_off_session(&self) -> Option; + fn get_mandate_id(&self) -> Option; + fn get_currency(&self) -> enums::Currency; + fn get_optional_billing_address(&self) -> Option<&Address>; + fn get_connector_meta_data(&self) -> Option<&pii::SecretSerdeValue>; + fn get_payment_method(&self) -> enums::PaymentMethod; + fn get_payment_method_type(&self) -> Option; + fn get_connector_request_reference_id(&self) -> String; + fn get_is_mandate_payment(&self) -> bool; + fn get_settlement_info(&self, _amount: i64) -> Option { + None + } +} + +impl WorldpayPaymentsRequestData + for RouterData { - type Error = error_stack::Report; + fn get_return_url(&self) -> Result> { + self.request.get_router_return_url() + } - fn try_from( - req: ( - &WorldpayRouterData< - &RouterData, - >, - &Secret, + fn get_auth_type(&self) -> &enums::AuthenticationType { + &self.auth_type + } + + fn get_browser_info(&self) -> Option<&BrowserInformation> { + self.request.browser_info.as_ref() + } + + fn get_payment_method_data(&self) -> &PaymentMethodData { + &self.request.payment_method_data + } + + fn get_setup_future_usage(&self) -> Option { + self.request.setup_future_usage + } + + fn get_off_session(&self) -> Option { + self.request.off_session + } + + fn get_mandate_id(&self) -> Option { + self.request.mandate_id.clone() + } + + fn get_currency(&self) -> enums::Currency { + self.request.currency + } + + fn get_optional_billing_address(&self) -> Option<&Address> { + self.get_optional_billing() + } + + fn get_connector_meta_data(&self) -> Option<&pii::SecretSerdeValue> { + self.connector_meta_data.as_ref() + } + + fn get_payment_method(&self) -> enums::PaymentMethod { + self.payment_method + } + + fn get_payment_method_type(&self) -> Option { + self.request.payment_method_type + } + + fn get_connector_request_reference_id(&self) -> String { + self.connector_request_reference_id.clone() + } + + fn get_is_mandate_payment(&self) -> bool { + true + } +} + +impl WorldpayPaymentsRequestData + for RouterData +{ + fn get_return_url(&self) -> Result> { + self.request.get_complete_authorize_url() + } + + fn get_auth_type(&self) -> &enums::AuthenticationType { + &self.auth_type + } + + fn get_browser_info(&self) -> Option<&BrowserInformation> { + self.request.browser_info.as_ref() + } + + fn get_payment_method_data(&self) -> &PaymentMethodData { + &self.request.payment_method_data + } + + fn get_setup_future_usage(&self) -> Option { + self.request.setup_future_usage + } + + fn get_off_session(&self) -> Option { + self.request.off_session + } + + fn get_mandate_id(&self) -> Option { + self.request.mandate_id.clone() + } + + fn get_currency(&self) -> enums::Currency { + self.request.currency + } + + fn get_optional_billing_address(&self) -> Option<&Address> { + self.get_optional_billing() + } + + fn get_connector_meta_data(&self) -> Option<&pii::SecretSerdeValue> { + self.connector_meta_data.as_ref() + } + + fn get_payment_method(&self) -> enums::PaymentMethod { + self.payment_method + } + + fn get_payment_method_type(&self) -> Option { + self.request.payment_method_type + } + + fn get_connector_request_reference_id(&self) -> String { + self.connector_request_reference_id.clone() + } + + fn get_is_mandate_payment(&self) -> bool { + self.request.is_mandate_payment() + } + + fn get_settlement_info(&self, amount: i64) -> Option { + match (self.request.capture_method.unwrap_or_default(), amount) { + (_, 0) => None, + (enums::CaptureMethod::Automatic, _) => Some(AutoSettlement { auto: true }), + (enums::CaptureMethod::Manual, _) | (enums::CaptureMethod::ManualMultiple, _) => { + Some(AutoSettlement { auto: false }) + } + _ => None, + } + } +} + +// Dangling helper function to create ThreeDS request +fn create_three_ds_request( + router_data: &T, + is_mandate_payment: bool, +) -> Result, error_stack::Report> { + match router_data.get_auth_type() { + enums::AuthenticationType::ThreeDs => { + let browser_info = router_data.get_browser_info().ok_or( + errors::ConnectorError::MissingRequiredField { + field_name: "browser_info", + }, + )?; + + let accept_header = browser_info + .accept_header + .clone() + .get_required_value("accept_header") + .change_context(errors::ConnectorError::MissingRequiredField { + field_name: "accept_header", + })?; + + let user_agent_header = browser_info + .user_agent + .clone() + .get_required_value("user_agent") + .change_context(errors::ConnectorError::MissingRequiredField { + field_name: "user_agent", + })?; + + Ok(Some(ThreeDSRequest { + three_ds_type: THREE_DS_TYPE.to_string(), + mode: THREE_DS_MODE.to_string(), + device_data: ThreeDSRequestDeviceData { + accept_header, + user_agent_header, + browser_language: browser_info.language.clone(), + browser_screen_width: browser_info.screen_width, + browser_screen_height: browser_info.screen_height, + browser_color_depth: browser_info.color_depth.map(|depth| depth.to_string()), + time_zone: browser_info.time_zone.map(|tz| tz.to_string()), + browser_java_enabled: browser_info.java_enabled, + browser_javascript_enabled: browser_info.java_script_enabled, + channel: Some(ThreeDSRequestChannel::Browser), + }, + challenge: ThreeDSRequestChallenge { + return_url: router_data.get_return_url()?, + preference: if is_mandate_payment { + Some(ThreeDsPreference::ChallengeMandated) + } else { + None + }, + }, + })) + } + _ => Ok(None), + } +} + +// Dangling helper function to determine token and agreement settings +fn get_token_and_agreement( + payment_method_data: &PaymentMethodData, + setup_future_usage: Option, + off_session: Option, +) -> (Option, Option) { + match (payment_method_data, setup_future_usage, off_session) { + // CIT + (PaymentMethodData::Card(_), Some(enums::FutureUsage::OffSession), _) => ( + Some(TokenCreation { + token_type: TokenCreationType::Worldpay, + }), + Some(CustomerAgreement { + agreement_type: CustomerAgreementType::Subscription, + stored_card_usage: StoredCardUsageType::First, + }), ), - ) -> Result { + // MIT + (PaymentMethodData::Card(_), _, Some(true)) => ( + None, + Some(CustomerAgreement { + agreement_type: CustomerAgreementType::Subscription, + stored_card_usage: StoredCardUsageType::Subsequent, + }), + ), + _ => (None, None), + } +} + +// Implementation for WorldpayPaymentsRequest using abstracted request +impl TryFrom<(&WorldpayRouterData<&T>, &Secret)> + for WorldpayPaymentsRequest +{ + type Error = error_stack::Report; + + fn try_from(req: (&WorldpayRouterData<&T>, &Secret)) -> Result { let (item, entity_id) = req; let worldpay_connector_metadata_object: WorldpayConnectorMetadataObject = - WorldpayConnectorMetadataObject::try_from(&item.router_data.connector_meta_data)?; + WorldpayConnectorMetadataObject::try_from(item.router_data.get_connector_meta_data())?; + let merchant_name = worldpay_connector_metadata_object.merchant_name.ok_or( errors::ConnectorError::InvalidConnectorConfig { config: "metadata.merchant_name", }, )?; - let three_ds = match item.router_data.auth_type { - enums::AuthenticationType::ThreeDs => { - let browser_info = item - .router_data - .request - .browser_info - .clone() - .get_required_value("browser_info") - .change_context(errors::ConnectorError::MissingRequiredField { - field_name: "browser_info", - })?; - let accept_header = browser_info - .accept_header - .get_required_value("accept_header") - .change_context(errors::ConnectorError::MissingRequiredField { - field_name: "accept_header", - })?; - let user_agent_header = browser_info - .user_agent - .get_required_value("user_agent") - .change_context(errors::ConnectorError::MissingRequiredField { - field_name: "user_agent", - })?; - Some(ThreeDSRequest { - three_ds_type: "integrated".to_string(), - mode: "always".to_string(), - device_data: ThreeDSRequestDeviceData { - accept_header, - user_agent_header, - browser_language: browser_info.language.clone(), - browser_screen_width: browser_info.screen_width, - browser_screen_height: browser_info.screen_height, - browser_color_depth: browser_info - .color_depth - .map(|depth| depth.to_string()), - time_zone: browser_info.time_zone.map(|tz| tz.to_string()), - browser_java_enabled: browser_info.java_enabled, - browser_javascript_enabled: browser_info.java_script_enabled, - channel: Some(ThreeDSRequestChannel::Browser), - }, - challenge: ThreeDSRequestChallenge { - return_url: item.router_data.request.get_complete_authorize_url()?, - }, - }) - } - _ => None, - }; + + let is_mandate_payment = item.router_data.get_is_mandate_payment(); + let three_ds = create_three_ds_request(item.router_data, is_mandate_payment)?; + + let (token_creation, customer_agreement) = get_token_and_agreement( + item.router_data.get_payment_method_data(), + item.router_data.get_setup_future_usage(), + item.router_data.get_off_session(), + ); + Ok(Self { instruction: Instruction { - settlement: item - .router_data - .request - .capture_method - .map(|capture_method| AutoSettlement { - auto: capture_method == enums::CaptureMethod::Automatic, - }), + settlement: item.router_data.get_settlement_info(item.amount), method: PaymentMethod::try_from(( - item.router_data.payment_method, - item.router_data.request.payment_method_type, + item.router_data.get_payment_method(), + item.router_data.get_payment_method_type(), ))?, payment_instrument: fetch_payment_instrument( - item.router_data.request.payment_method_data.clone(), - item.router_data.get_optional_billing(), + item.router_data.get_payment_method_data().clone(), + item.router_data.get_optional_billing_address(), + item.router_data.get_mandate_id(), )?, narrative: InstructionNarrative { line1: merchant_name.expose(), }, value: PaymentValue { amount: item.amount, - currency: item.router_data.request.currency, + currency: item.router_data.get_currency(), }, debt_repayment: None, three_ds, + token_creation, + customer_agreement, }, merchant: Merchant { entity: entity_id.clone(), ..Default::default() }, - transaction_reference: item.router_data.connector_request_reference_id.clone(), + transaction_reference: item.router_data.get_connector_request_reference_id(), customer: None, }) } @@ -409,14 +625,22 @@ impl ), ) -> Result { let (router_data, optional_correlation_id) = item; - let (description, redirection_data, error) = router_data + let (description, redirection_data, mandate_reference, error) = router_data .response .other_fields .as_ref() .map(|other_fields| match other_fields { - WorldpayPaymentResponseFields::AuthorizedResponse(res) => { - (res.description.clone(), None, None) - } + WorldpayPaymentResponseFields::AuthorizedResponse(res) => ( + res.description.clone(), + None, + res.token.as_ref().map(|mandate_token| MandateReference { + connector_mandate_id: Some(mandate_token.href.clone().expose()), + payment_method_id: Some(mandate_token.token_id.clone()), + mandate_metadata: None, + connector_mandate_request_reference_id: None, + }), + None, + ), WorldpayPaymentResponseFields::DDCResponse(res) => ( None, Some(RedirectForm::WorldpayDDCForm { @@ -435,6 +659,7 @@ impl ]), }), None, + None, ), WorldpayPaymentResponseFields::ThreeDsChallenged(res) => ( None, @@ -447,15 +672,17 @@ impl )]), }), None, + None, ), WorldpayPaymentResponseFields::RefusedResponse(res) => ( + None, None, None, Some((res.refusal_code.clone(), res.refusal_description.clone())), ), - WorldpayPaymentResponseFields::FraudHighRisk(_) => (None, None, None), + WorldpayPaymentResponseFields::FraudHighRisk(_) => (None, None, None, None), }) - .unwrap_or((None, None, None)); + .unwrap_or((None, None, None, None)); let worldpay_status = router_data.response.outcome.clone(); let optional_error_message = match worldpay_status { PaymentOutcome::ThreeDsAuthenticationFailed => { @@ -475,7 +702,7 @@ impl optional_correlation_id.clone(), ))?, redirection_data: Box::new(redirection_data), - mandate_reference: Box::new(None), + mandate_reference: Box::new(mandate_reference), connector_metadata: None, network_txn_id: None, connector_response_reference_id: optional_correlation_id.clone(), diff --git a/crates/hyperswitch_connectors/src/utils.rs b/crates/hyperswitch_connectors/src/utils.rs index 110917226fe7..bbabcefc02d4 100644 --- a/crates/hyperswitch_connectors/src/utils.rs +++ b/crates/hyperswitch_connectors/src/utils.rs @@ -1434,6 +1434,7 @@ impl RefundsRequestData for RefundsData { pub trait PaymentsSetupMandateRequestData { fn get_browser_info(&self) -> Result; fn get_email(&self) -> Result; + fn get_router_return_url(&self) -> Result; fn is_card(&self) -> bool; } @@ -1446,6 +1447,11 @@ impl PaymentsSetupMandateRequestData for SetupMandateRequestData { fn get_email(&self) -> Result { self.email.clone().ok_or_else(missing_field_err("email")) } + fn get_router_return_url(&self) -> Result { + self.router_return_url + .clone() + .ok_or_else(missing_field_err("router_return_url")) + } fn is_card(&self) -> bool { matches!(self.payment_method_data, PaymentMethodData::Card(_)) } diff --git a/crates/hyperswitch_domain_models/src/errors/api_error_response.rs b/crates/hyperswitch_domain_models/src/errors/api_error_response.rs index 75bd7740026f..b02da3960756 100644 --- a/crates/hyperswitch_domain_models/src/errors/api_error_response.rs +++ b/crates/hyperswitch_domain_models/src/errors/api_error_response.rs @@ -644,7 +644,7 @@ impl ErrorSwitch for ApiErrorRespon AER::Unprocessable(ApiError::new("WE", 5, "There was an issue processing the webhook body", None)) }, Self::WebhookInvalidMerchantSecret => { - AER::BadRequest(ApiError::new("WE", 6, "Merchant Secret set for webhook source verificartion is invalid", None)) + AER::BadRequest(ApiError::new("WE", 6, "Merchant Secret set for webhook source verification is invalid", None)) } Self::IntegrityCheckFailed { reason, diff --git a/crates/router/src/consts.rs b/crates/router/src/consts.rs index 090ddca961ba..3f4faedff81b 100644 --- a/crates/router/src/consts.rs +++ b/crates/router/src/consts.rs @@ -180,6 +180,3 @@ pub const VAULT_DELETE_FLOW_TYPE: &str = "delete_from_vault"; /// Vault Fingerprint fetch flow type #[cfg(all(feature = "v2", feature = "payment_methods_v2"))] pub const VAULT_GET_FINGERPRINT_FLOW_TYPE: &str = "get_fingerprint_vault"; - -/// Worldpay's unique reference ID for a request TODO: Move to hyperswitch_connectors/constants once Worldpay is moved to connectors crate -pub const WP_CORRELATION_ID: &str = "WP-CorrelationId"; diff --git a/crates/router/src/core/payments/operations/payment_response.rs b/crates/router/src/core/payments/operations/payment_response.rs index f0381dbf8220..f4ec0ef9eb55 100644 --- a/crates/router/src/core/payments/operations/payment_response.rs +++ b/crates/router/src/core/payments/operations/payment_response.rs @@ -2396,7 +2396,7 @@ fn update_connector_mandate_details_for_the_flow( )) } } else { - None + original_connector_mandate_reference_id }; payment_data.payment_attempt.connector_mandate_detail = connector_mandate_reference_id diff --git a/crates/router/src/lib.rs b/crates/router/src/lib.rs index 06ab971925bc..215a8b209cf0 100644 --- a/crates/router/src/lib.rs +++ b/crates/router/src/lib.rs @@ -89,7 +89,6 @@ pub mod headers { pub const X_REDIRECT_URI: &str = "x-redirect-uri"; pub const X_TENANT_ID: &str = "x-tenant-id"; pub const X_CLIENT_SECRET: &str = "X-Client-Secret"; - pub const X_WP_API_VERSION: &str = "WP-Api-Version"; } pub mod pii { From bc92a2e9d9bb1ec914670ea1c2e399c9c6b8839a Mon Sep 17 00:00:00 2001 From: chikke srujan <121822803+srujanchikke@users.noreply.github.com> Date: Fri, 8 Nov 2024 18:23:58 +0530 Subject: [PATCH 21/25] fix(connector): [fiuu]fix mandates for fiuu (#6487) Co-authored-by: Chikke Srujan Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com> --- .../src/connectors/fiuu/transformers.rs | 20 ++- crates/hyperswitch_connectors/src/utils.rs | 20 +++ .../payment_connector_required_fields.rs | 104 ++++++++------ .../router/src/core/payment_methods/cards.rs | 18 +-- .../cypress/e2e/PaymentUtils/Fiuu.js | 134 +++++++++++++++++- cypress-tests/cypress/support/commands.js | 18 ++- .../cypress/support/redirectionHandler.js | 18 ++- 7 files changed, 259 insertions(+), 73 deletions(-) diff --git a/crates/hyperswitch_connectors/src/connectors/fiuu/transformers.rs b/crates/hyperswitch_connectors/src/connectors/fiuu/transformers.rs index 3639a7db4c8d..c0fcd77d8f00 100644 --- a/crates/hyperswitch_connectors/src/connectors/fiuu/transformers.rs +++ b/crates/hyperswitch_connectors/src/connectors/fiuu/transformers.rs @@ -205,8 +205,11 @@ impl TryFrom<&FiuuRouterData<&PaymentsAuthorizeRouterData>> for FiuuMandateReque let order_id = item.router_data.connector_request_reference_id.clone(); let currency = item.router_data.request.currency; let amount = item.amount.clone(); - let billing_name = item.router_data.get_billing_full_name()?; - let email = item.router_data.request.get_email()?; + let billing_name = item + .router_data + .request + .get_card_holder_name_from_additional_payment_method_data()?; + let email = item.router_data.get_billing_email()?; let token = Secret::new(item.router_data.request.get_connector_mandate_id()?); let verify_key = auth.verify_key; let recurring_request = FiuuRecurringRequest { @@ -313,8 +316,6 @@ pub struct FiuuCardData { cc_year: Secret, #[serde(rename = "mpstokenstatus")] mps_token_status: Option, - #[serde(rename = "CustName")] - customer_name: Option>, #[serde(rename = "CustEmail")] customer_email: Option, } @@ -548,15 +549,11 @@ impl TryFrom<(&Card, &PaymentsAuthorizeRouterData)> for FiuuPaymentMethodData { fn try_from( (req_card, item): (&Card, &PaymentsAuthorizeRouterData), ) -> Result { - let (mps_token_status, customer_name, customer_email) = + let (mps_token_status, customer_email) = if item.request.is_customer_initiated_mandate_payment() { - ( - Some(1), - Some(item.request.get_customer_name()?), - Some(item.request.get_email()?), - ) + (Some(1), Some(item.get_billing_email()?)) } else { - (None, None, None) + (None, None) }; let non_3ds = match item.is_three_ds() { false => 1, @@ -570,7 +567,6 @@ impl TryFrom<(&Card, &PaymentsAuthorizeRouterData)> for FiuuPaymentMethodData { cc_month: req_card.card_exp_month.clone(), cc_year: req_card.card_exp_year.clone(), mps_token_status, - customer_name, customer_email, }))) } diff --git a/crates/hyperswitch_connectors/src/utils.rs b/crates/hyperswitch_connectors/src/utils.rs index bbabcefc02d4..81c9aa6084db 100644 --- a/crates/hyperswitch_connectors/src/utils.rs +++ b/crates/hyperswitch_connectors/src/utils.rs @@ -1155,6 +1155,9 @@ pub trait PaymentsAuthorizeRequestData { fn get_metadata_as_object(&self) -> Option; fn get_authentication_data(&self) -> Result; fn get_customer_name(&self) -> Result, Error>; + fn get_card_holder_name_from_additional_payment_method_data( + &self, + ) -> Result, Error>; } impl PaymentsAuthorizeRequestData for PaymentsAuthorizeData { @@ -1316,6 +1319,23 @@ impl PaymentsAuthorizeRequestData for PaymentsAuthorizeData { .clone() .ok_or_else(missing_field_err("customer_name")) } + + fn get_card_holder_name_from_additional_payment_method_data( + &self, + ) -> Result, Error> { + match &self.additional_payment_method_data { + Some(payments::AdditionalPaymentData::Card(card_data)) => Ok(card_data + .card_holder_name + .clone() + .ok_or_else(|| errors::ConnectorError::MissingRequiredField { + field_name: "card_holder_name", + })?), + _ => Err(errors::ConnectorError::MissingRequiredFields { + field_names: vec!["card_holder_name"], + } + .into()), + } + } } pub trait PaymentsCaptureRequestData { diff --git a/crates/router/src/configs/defaults/payment_connector_required_fields.rs b/crates/router/src/configs/defaults/payment_connector_required_fields.rs index 1c02619a8e97..fa538cc3192f 100644 --- a/crates/router/src/configs/defaults/payment_connector_required_fields.rs +++ b/crates/router/src/configs/defaults/payment_connector_required_fields.rs @@ -292,24 +292,6 @@ impl Default for settings::RequiredFields { common: HashMap::new(), } ), - ( - enums::Connector::Fiuu, - RequiredFieldFinal { - mandate: HashMap::from([ - ( - "billing.email".to_string(), - RequiredFieldInfo { - required_field: "payment_method_data.billing.email".to_string(), - display_name: "email".to_string(), - field_type: enums::FieldType::UserEmailAddress, - value: None, - } - ) - ]), - non_mandate: HashMap::new(), - common: HashMap::new(), - } - ), ( enums::Connector::Authorizedotnet, RequiredFieldFinal { @@ -1423,8 +1405,37 @@ impl Default for settings::RequiredFields { ( enums::Connector::Fiuu, RequiredFieldFinal { - mandate: HashMap::new(), - non_mandate: HashMap::from( + mandate: HashMap::from([ + ( + "billing.email".to_string(), + RequiredFieldInfo { + required_field: "payment_method_data.billing.email".to_string(), + display_name: "email".to_string(), + field_type: enums::FieldType::UserEmailAddress, + value: None, + } + ), + ( + "billing.address.first_name".to_string(), + RequiredFieldInfo { + required_field: "payment_method_data.billing.address.first_name".to_string(), + display_name: "card_holder_name".to_string(), + field_type: enums::FieldType::UserFullName, + value: None, + } + ), + ( + "billing.address.last_name".to_string(), + RequiredFieldInfo { + required_field: "payment_method_data.billing.address.last_name".to_string(), + display_name: "card_holder_name".to_string(), + field_type: enums::FieldType::UserFullName, + value: None, + } + ), + ]), + non_mandate: HashMap::new(), + common: HashMap::from( [ ( "payment_method_data.card.card_number".to_string(), @@ -1464,7 +1475,6 @@ impl Default for settings::RequiredFields { ) ] ), - common: HashMap::new(), } ), ( @@ -3402,24 +3412,6 @@ impl Default for settings::RequiredFields { common: HashMap::new(), } ), - ( - enums::Connector::Fiuu, - RequiredFieldFinal { - mandate: HashMap::from([ - ( - "billing.email".to_string(), - RequiredFieldInfo { - required_field: "payment_method_data.billing.email".to_string(), - display_name: "email".to_string(), - field_type: enums::FieldType::UserEmailAddress, - value: None, - } - ) - ]), - non_mandate: HashMap::new(), - common: HashMap::new(), - } - ), ( enums::Connector::Authorizedotnet, RequiredFieldFinal { @@ -4533,8 +4525,37 @@ impl Default for settings::RequiredFields { ( enums::Connector::Fiuu, RequiredFieldFinal { - mandate: HashMap::new(), - non_mandate: HashMap::from( + mandate: HashMap::from([ + ( + "billing.email".to_string(), + RequiredFieldInfo { + required_field: "payment_method_data.billing.email".to_string(), + display_name: "email".to_string(), + field_type: enums::FieldType::UserEmailAddress, + value: None, + } + ), + ( + "billing.address.first_name".to_string(), + RequiredFieldInfo { + required_field: "payment_method_data.billing.address.first_name".to_string(), + display_name: "card_holder_name".to_string(), + field_type: enums::FieldType::UserFullName, + value: None, + } + ), + ( + "billing.address.last_name".to_string(), + RequiredFieldInfo { + required_field: "payment_method_data.billing.address.last_name".to_string(), + display_name: "card_holder_name".to_string(), + field_type: enums::FieldType::UserFullName, + value: None, + } + ), + ]), + non_mandate: HashMap::new(), + common: HashMap::from( [ ( "payment_method_data.card.card_number".to_string(), @@ -4574,7 +4595,6 @@ impl Default for settings::RequiredFields { ) ] ), - common: HashMap::new(), } ), ( diff --git a/crates/router/src/core/payment_methods/cards.rs b/crates/router/src/core/payment_methods/cards.rs index bcf0f9348627..cbd535903ae2 100644 --- a/crates/router/src/core/payment_methods/cards.rs +++ b/crates/router/src/core/payment_methods/cards.rs @@ -3103,15 +3103,18 @@ pub async fn list_payment_methods( .await .transpose()?; let setup_future_usage = payment_intent.as_ref().and_then(|pi| pi.setup_future_usage); + let is_cit_transaction = payment_attempt + .as_ref() + .map(|pa| pa.mandate_details.is_some()) + .unwrap_or(false) + || setup_future_usage + .map(|future_usage| future_usage == common_enums::FutureUsage::OffSession) + .unwrap_or(false); let payment_type = payment_attempt.as_ref().map(|pa| { let amount = api::Amount::from(pa.net_amount.get_order_amount()); let mandate_type = if pa.mandate_id.is_some() { Some(api::MandateTransactionType::RecurringMandateTransaction) - } else if pa.mandate_details.is_some() - || setup_future_usage - .map(|future_usage| future_usage == common_enums::enums::FutureUsage::OffSession) - .unwrap_or(false) - { + } else if is_cit_transaction { Some(api::MandateTransactionType::NewMandateTransaction) } else { None @@ -3619,16 +3622,13 @@ pub async fn list_payment_methods( .get(&connector_variant) .map(|required_fields_final| { let mut required_fields_hs = required_fields_final.common.clone(); - if let Some(pa) = payment_attempt.as_ref() { - if let Some(_mandate) = &pa.mandate_details { + if is_cit_transaction { required_fields_hs .extend(required_fields_final.mandate.clone()); } else { required_fields_hs .extend(required_fields_final.non_mandate.clone()); } - } - required_fields_hs = should_collect_shipping_or_billing_details_from_wallet_connector( &payment_method, element.payment_experience.as_ref(), diff --git a/cypress-tests/cypress/e2e/PaymentUtils/Fiuu.js b/cypress-tests/cypress/e2e/PaymentUtils/Fiuu.js index 24a445dd3c25..4a910c7897c1 100644 --- a/cypress-tests/cypress/e2e/PaymentUtils/Fiuu.js +++ b/cypress-tests/cypress/e2e/PaymentUtils/Fiuu.js @@ -9,9 +9,9 @@ const successfulNo3DSCardDetails = { const successfulThreeDSTestCardDetails = { card_number: "5105105105105100", card_exp_month: "12", - card_exp_year: "2030", + card_exp_year: "2031", card_holder_name: "joseph Doe", - card_cvc: "123", + card_cvc: "444", }; const singleUseMandateData = { @@ -75,7 +75,7 @@ export const connectorDetails = { Response: { status: 200, body: { - status: "requires_capture", + status: "requires_customer_action", }, }, }, @@ -287,6 +287,20 @@ export const connectorDetails = { payment_method: "card", payment_method_data: { card: successfulNo3DSCardDetails, + billing: { + address: { + line1: "1467", + line2: "Harrison Street", + line3: "Harrison Street", + city: "San Fransico", + state: "California", + zip: "94122", + country: "NL", + first_name: "joseph", + last_name: "Doe", + }, + email: "johndoe@gmail.com" + }, }, currency: "USD", mandate_data: multiUseMandateData, @@ -351,7 +365,21 @@ export const connectorDetails = { payment_method: "card", payment_method_data: { card: successfulNo3DSCardDetails, - }, + billing: { + address: { + line1: "1467", + line2: "Harrison Street", + line3: "Harrison Street", + city: "San Fransico", + state: "California", + zip: "94122", + country: "NL", + first_name: "joseph", + last_name: "Doe", + }, + email: "johndoe@gmail.com" + }, + }, currency: "USD", mandate_data: null, customer_acceptance: { @@ -375,6 +403,20 @@ export const connectorDetails = { payment_method: "card", payment_method_data: { card: successfulNo3DSCardDetails, + billing: { + address: { + line1: "1467", + line2: "Harrison Street", + line3: "Harrison Street", + city: "San Fransico", + state: "California", + zip: "94122", + country: "NL", + first_name: "joseph", + last_name: "Doe", + }, + email: "johndoe@gmail.com" + }, }, currency: "USD", setup_future_usage: "on_session", @@ -399,6 +441,20 @@ export const connectorDetails = { payment_method: "card", payment_method_data: { card: successfulNo3DSCardDetails, + billing: { + address: { + line1: "1467", + line2: "Harrison Street", + line3: "Harrison Street", + city: "San Fransico", + state: "California", + zip: "94122", + country: "NL", + first_name: "joseph", + last_name: "Doe", + }, + email: "johndoe@gmail.com" + }, }, currency: "USD", setup_future_usage: "on_session", @@ -423,6 +479,20 @@ export const connectorDetails = { payment_method: "card", payment_method_data: { card: successfulNo3DSCardDetails, + billing: { + address: { + line1: "1467", + line2: "Harrison Street", + line3: "Harrison Street", + city: "San Fransico", + state: "California", + zip: "94122", + country: "NL", + first_name: "joseph", + last_name: "Doe", + }, + email: "johndoe@gmail.com" + }, }, setup_future_usage: "off_session", customer_acceptance: { @@ -446,6 +516,20 @@ export const connectorDetails = { payment_method: "card", payment_method_data: { card: successfulNo3DSCardDetails, + billing: { + address: { + line1: "1467", + line2: "Harrison Street", + line3: "Harrison Street", + city: "San Fransico", + state: "California", + zip: "94122", + country: "NL", + first_name: "joseph", + last_name: "Doe", + }, + email: "johndoe@gmail.com" + }, }, setup_future_usage: "off_session", customer_acceptance: { @@ -491,6 +575,20 @@ export const connectorDetails = { payment_method: "card", payment_method_data: { card: successfulNo3DSCardDetails, + billing: { + address: { + line1: "1467", + line2: "Harrison Street", + line3: "Harrison Street", + city: "San Fransico", + state: "California", + zip: "94122", + country: "NL", + first_name: "joseph", + last_name: "Doe", + }, + email: "johndoe@gmail.com" + }, }, currency: "USD", mandate_data: null, @@ -515,6 +613,20 @@ export const connectorDetails = { payment_method: "card", payment_method_data: { card: successfulThreeDSTestCardDetails, + billing: { + address: { + line1: "1467", + line2: "Harrison Street", + line3: "Harrison Street", + city: "San Fransico", + state: "California", + zip: "94122", + country: "NL", + first_name: "joseph", + last_name: "Doe", + }, + email: "johndoe@gmail.com" + }, }, currency: "USD", mandate_data: null, @@ -540,6 +652,20 @@ export const connectorDetails = { payment_method: "card", payment_method_data: { card: successfulThreeDSTestCardDetails, + billing: { + address: { + line1: "1467", + line2: "Harrison Street", + line3: "Harrison Street", + city: "San Fransico", + state: "California", + zip: "94122", + country: "NL", + first_name: "joseph", + last_name: "Doe", + }, + email: "johndoe@gmail.com" + }, }, mandate_data: null, authentication_type: "three_ds", diff --git a/cypress-tests/cypress/support/commands.js b/cypress-tests/cypress/support/commands.js index 9b938e673d77..5b2ae9bb8ad7 100644 --- a/cypress-tests/cypress/support/commands.js +++ b/cypress-tests/cypress/support/commands.js @@ -1769,7 +1769,11 @@ Cypress.Commands.add( const nextActionUrl = response.body.next_action.redirect_to_url; cy.log(nextActionUrl); } else if (response.body.authentication_type === "no_three_ds") { - expect(response.body.status).to.equal("succeeded"); + if (response.body.connector === "fiuu") { + expect(response.body.status).to.equal("failed"); + } else { + expect(response.body.status).to.equal("succeeded"); + } } else { throw new Error( `Invalid authentication type ${response.body.authentication_type}` @@ -1783,7 +1787,11 @@ Cypress.Commands.add( const nextActionUrl = response.body.next_action.redirect_to_url; cy.log(nextActionUrl); } else if (response.body.authentication_type === "no_three_ds") { - expect(response.body.status).to.equal("requires_capture"); + if (response.body.connector === "fiuu") { + expect(response.body.status).to.equal("failed"); + } else { + expect(response.body.status).to.equal("requires_capture"); + } } else { throw new Error( `Invalid authentication type ${response.body.authentication_type}` @@ -1861,7 +1869,11 @@ Cypress.Commands.add( const nextActionUrl = response.body.next_action.redirect_to_url; cy.log(nextActionUrl); } else if (response.body.authentication_type === "no_three_ds") { - expect(response.body.status).to.equal("requires_capture"); + if (response.body.connector === "fiuu") { + expect(response.body.status).to.equal("failed"); + } else { + expect(response.body.status).to.equal("requires_capture"); + } } else { throw new Error( `Invalid authentication type ${response.body.authentication_type}` diff --git a/cypress-tests/cypress/support/redirectionHandler.js b/cypress-tests/cypress/support/redirectionHandler.js index aa6db7d50747..b3eb89868f05 100644 --- a/cypress-tests/cypress/support/redirectionHandler.js +++ b/cypress-tests/cypress/support/redirectionHandler.js @@ -352,10 +352,22 @@ function threeDsRedirection(redirection_url, expected_url, connectorId) { .then(() => { cy.get('input[name="challengeDataEntry"]').click().type("1234"); cy.get('input[value="SUBMIT"]').click(); - }) + }); }); - } - else { + } else if (connectorId === "fiuu") { + cy.get('form[id="cc_form"]', { timeout: WAIT_TIME_IATAPAY }) + .should("exist") + .then((form) => { + cy.get('button.pay-btn[name="pay"]').click(); + cy.get("div.otp") + .invoke("text") + .then((otpText) => { + const otp = otpText.match(/\d+/)[0]; // Extract the numeric OTP + cy.get("input#otp-input").should("not.be.disabled").type(otp); + cy.get('button.pay-btn').click(); + }); + }); + } else { // If connectorId is neither of adyen, trustpay, nmi, stripe, bankofamerica or cybersource, wait for 10 seconds cy.wait(WAIT_TIME); } From 0389ae74e112dedd9d98314906820f78e4b89380 Mon Sep 17 00:00:00 2001 From: Sanchith Hegde <22217505+SanchithHegde@users.noreply.github.com> Date: Fri, 8 Nov 2024 19:20:14 +0530 Subject: [PATCH 22/25] refactor(payment_methods): refactor customer payment methods list v2 code to follow better code practices (#6433) --- Cargo.lock | 4 +- api-reference-v2/openapi_spec.json | 11 +- api-reference/openapi_spec.json | 4 +- crates/api_models/src/admin.rs | 19 +- crates/api_models/src/payment_methods.rs | 8 - crates/api_models/src/payments.rs | 8 +- crates/diesel_models/src/business_profile.rs | 6 + crates/diesel_models/src/payment_attempt.rs | 2 +- crates/diesel_models/src/payment_method.rs | 190 +++++++++++------- crates/diesel_models/src/schema_v2.rs | 2 +- .../connectors/deutschebank/transformers.rs | 6 +- .../src/business_profile.rs | 78 +++++-- .../src/merchant_connector_account.rs | 29 +++ .../src/payment_methods.rs | 6 +- .../src/router_data.rs | 2 +- .../src/router_response_types.rs | 2 +- crates/router/src/core/admin.rs | 5 +- crates/router/src/core/payment_methods.rs | 155 +++++--------- .../router/src/core/payment_methods/cards.rs | 79 ++++---- .../src/core/payment_methods/transformers.rs | 1 - crates/router/src/core/payments.rs | 3 +- .../payments/operations/payment_response.rs | 4 +- .../router/src/core/payments/tokenization.rs | 18 +- crates/router/src/db/payment_method.rs | 86 ++++---- crates/router/src/routes/payment_methods.rs | 12 +- crates/router/src/services/authentication.rs | 47 +++++ crates/router/src/types/api/admin.rs | 1 + crates/router/src/types/payment_methods.rs | 4 +- .../src/types/storage/payment_method.rs | 36 ---- .../down.sql | 33 ++- .../up.sql | 39 ++-- .../2024-08-28-081721_add_v2_columns/down.sql | 3 +- .../2024-08-28-081721_add_v2_columns/up.sql | 9 +- 33 files changed, 501 insertions(+), 411 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 0a08788ecfc6..7c9cb19d5c98 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3964,9 +3964,9 @@ dependencies = [ [[package]] name = "hyper-util" -version = "0.1.9" +version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41296eb09f183ac68eec06e03cdbea2e759633d4067b2f6552fc2e009bcad08b" +checksum = "df2dcfbe0677734ab2f3ffa7fa7bfd4706bfdc1ef393f2ee30184aed67e631b4" dependencies = [ "bytes 1.7.1", "futures-channel", diff --git a/api-reference-v2/openapi_spec.json b/api-reference-v2/openapi_spec.json index 4c1559fe099b..e14963c80d48 100644 --- a/api-reference-v2/openapi_spec.json +++ b/api-reference-v2/openapi_spec.json @@ -16623,7 +16623,7 @@ }, "is_network_tokenization_enabled": { "type": "boolean", - "description": "Indicates if is_network_tokenization_enabled is enabled or not.\nIf set to `true` is_network_tokenization_enabled will be checked." + "description": "Indicates if network tokenization is enabled or not." } }, "additionalProperties": false @@ -16655,7 +16655,8 @@ "enable_payment_response_hash", "redirect_to_merchant_with_http_post", "is_tax_connector_enabled", - "is_network_tokenization_enabled" + "is_network_tokenization_enabled", + "should_collect_cvv_during_payment" ], "properties": { "merchant_id": { @@ -16828,9 +16829,13 @@ }, "is_network_tokenization_enabled": { "type": "boolean", - "description": "Indicates if is_network_tokenization_enabled is enabled or not.\nIf set to `true` is_network_tokenization_enabled will be checked.", + "description": "Indicates if network tokenization is enabled or not.", "default": false, "example": false + }, + "should_collect_cvv_during_payment": { + "type": "boolean", + "description": "Indicates if CVV should be collected during payment or not." } } }, diff --git a/api-reference/openapi_spec.json b/api-reference/openapi_spec.json index f86664d459f5..733ba96237c4 100644 --- a/api-reference/openapi_spec.json +++ b/api-reference/openapi_spec.json @@ -21368,7 +21368,7 @@ }, "is_network_tokenization_enabled": { "type": "boolean", - "description": "Indicates if is_network_tokenization_enabled is enabled or not.\nIf set to `true` is_network_tokenization_enabled will be checked." + "description": "Indicates if network tokenization is enabled or not." }, "is_auto_retries_enabled": { "type": "boolean", @@ -21595,7 +21595,7 @@ }, "is_network_tokenization_enabled": { "type": "boolean", - "description": "Indicates if is_network_tokenization_enabled is enabled or not.\nIf set to `true` is_network_tokenization_enabled will be checked.", + "description": "Indicates if network tokenization is enabled or not.", "default": false, "example": false }, diff --git a/crates/api_models/src/admin.rs b/crates/api_models/src/admin.rs index c9da386a11de..96335f34710c 100644 --- a/crates/api_models/src/admin.rs +++ b/crates/api_models/src/admin.rs @@ -1980,8 +1980,7 @@ pub struct ProfileCreate { #[serde(default)] pub is_tax_connector_enabled: bool, - /// Indicates if is_network_tokenization_enabled is enabled or not. - /// If set to `true` is_network_tokenization_enabled will be checked. + /// Indicates if network tokenization is enabled or not. #[serde(default)] pub is_network_tokenization_enabled: bool, @@ -2095,8 +2094,7 @@ pub struct ProfileCreate { #[serde(default)] pub is_tax_connector_enabled: bool, - /// Indicates if is_network_tokenization_enabled is enabled or not. - /// If set to `true` is_network_tokenization_enabled will be checked. + /// Indicates if network tokenization is enabled or not. #[serde(default)] pub is_network_tokenization_enabled: bool, } @@ -2217,8 +2215,7 @@ pub struct ProfileResponse { /// If set to `true` tax_connector_id will be checked. pub is_tax_connector_enabled: bool, - /// Indicates if is_network_tokenization_enabled is enabled or not. - /// If set to `true` is_network_tokenization_enabled will be checked. + /// Indicates if network tokenization is enabled or not. #[schema(default = false, example = false)] pub is_network_tokenization_enabled: bool, @@ -2337,10 +2334,12 @@ pub struct ProfileResponse { /// If set to `true` tax_connector_id will be checked. pub is_tax_connector_enabled: bool, - /// Indicates if is_network_tokenization_enabled is enabled or not. - /// If set to `true` is_network_tokenization_enabled will be checked. + /// Indicates if network tokenization is enabled or not. #[schema(default = false, example = false)] pub is_network_tokenization_enabled: bool, + + /// Indicates if CVV should be collected during payment or not. + pub should_collect_cvv_during_payment: bool, } #[cfg(feature = "v1")] @@ -2455,7 +2454,7 @@ pub struct ProfileUpdate { #[serde(default)] pub dynamic_routing_algorithm: Option, - /// Indicates if is_network_tokenization_enabled is enabled or not. + /// Indicates if network tokenization is enabled or not. pub is_network_tokenization_enabled: Option, /// Indicates if is_auto_retries_enabled is enabled or not. @@ -2564,7 +2563,7 @@ pub struct ProfileUpdate { /// If set to `true` tax_connector_id will be checked. pub is_tax_connector_enabled: Option, - /// Indicates if is_network_tokenization_enabled is enabled or not. + /// Indicates if network tokenization is enabled or not. pub is_network_tokenization_enabled: Option, } diff --git a/crates/api_models/src/payment_methods.rs b/crates/api_models/src/payment_methods.rs index 47460e73b713..0602f69d1584 100644 --- a/crates/api_models/src/payment_methods.rs +++ b/crates/api_models/src/payment_methods.rs @@ -732,10 +732,6 @@ pub struct PaymentMethodResponse { #[schema(example = true)] pub recurring_enabled: bool, - /// You can specify up to 50 keys, with key names up to 40 characters long and values up to 500 characters long. Metadata is useful for storing additional, structured information on an object. - #[schema(value_type = Option, example = json!({ "city": "NY", "unit": "245" }))] - pub metadata: Option, - /// A timestamp (ISO 8601 code) that determines when the customer was created #[schema(value_type = Option, example = "2023-01-18T11:04:09.922Z")] #[serde(default, with = "common_utils::custom_serde::iso8601::option")] @@ -1707,10 +1703,6 @@ pub struct CustomerPaymentMethod { #[schema(example = json!({"mask": "0000"}))] pub bank: Option, - /// You can specify up to 50 keys, with key names up to 40 characters long and values up to 500 characters long. Metadata is useful for storing additional, structured information on an object. - #[schema(value_type = Option,example = json!({ "city": "NY", "unit": "245" }))] - pub metadata: Option, - /// A timestamp (ISO 8601 code) that determines when the customer was created #[schema(value_type = Option,example = "2023-01-18T11:04:09.922Z")] #[serde(default, with = "common_utils::custom_serde::iso8601::option")] diff --git a/crates/api_models/src/payments.rs b/crates/api_models/src/payments.rs index 2ace2b956e4a..3e95fb9f2315 100644 --- a/crates/api_models/src/payments.rs +++ b/crates/api_models/src/payments.rs @@ -1321,7 +1321,7 @@ pub struct ConnectorMandateReferenceId { connector_mandate_id: Option, payment_method_id: Option, update_history: Option>, - mandate_metadata: Option, + mandate_metadata: Option, connector_mandate_request_reference_id: Option, } @@ -1330,7 +1330,7 @@ impl ConnectorMandateReferenceId { connector_mandate_id: Option, payment_method_id: Option, update_history: Option>, - mandate_metadata: Option, + mandate_metadata: Option, connector_mandate_request_reference_id: Option, ) -> Self { Self { @@ -1348,7 +1348,7 @@ impl ConnectorMandateReferenceId { pub fn get_payment_method_id(&self) -> Option { self.payment_method_id.clone() } - pub fn get_mandate_metadata(&self) -> Option { + pub fn get_mandate_metadata(&self) -> Option { self.mandate_metadata.clone() } pub fn get_connector_mandate_request_reference_id(&self) -> Option { @@ -1360,7 +1360,7 @@ impl ConnectorMandateReferenceId { connector_mandate_id: Option, payment_method_id: Option, update_history: Option>, - mandate_metadata: Option, + mandate_metadata: Option, connector_mandate_request_reference_id: Option, ) { self.connector_mandate_id = connector_mandate_id.or(self.connector_mandate_id.clone()); diff --git a/crates/diesel_models/src/business_profile.rs b/crates/diesel_models/src/business_profile.rs index fd0abc16614d..ecb536206400 100644 --- a/crates/diesel_models/src/business_profile.rs +++ b/crates/diesel_models/src/business_profile.rs @@ -285,6 +285,7 @@ pub struct Profile { pub frm_routing_algorithm_id: Option, pub payout_routing_algorithm_id: Option, pub default_fallback_routing: Option, + pub should_collect_cvv_during_payment: bool, pub id: common_utils::id_type::ProfileId, pub version: common_enums::ApiVersion, pub dynamic_routing_algorithm: Option, @@ -343,6 +344,7 @@ pub struct ProfileNew { pub frm_routing_algorithm_id: Option, pub payout_routing_algorithm_id: Option, pub default_fallback_routing: Option, + pub should_collect_cvv_during_payment: bool, pub id: common_utils::id_type::ProfileId, pub version: common_enums::ApiVersion, pub is_network_tokenization_enabled: bool, @@ -386,6 +388,7 @@ pub struct ProfileUpdateInternal { pub frm_routing_algorithm_id: Option, pub payout_routing_algorithm_id: Option, pub default_fallback_routing: Option, + pub should_collect_cvv_during_payment: Option, pub is_network_tokenization_enabled: Option, pub is_auto_retries_enabled: Option, pub max_auto_retries_enabled: Option, @@ -426,6 +429,7 @@ impl ProfileUpdateInternal { frm_routing_algorithm_id, payout_routing_algorithm_id, default_fallback_routing, + should_collect_cvv_during_payment, is_network_tokenization_enabled, is_auto_retries_enabled, max_auto_retries_enabled, @@ -485,6 +489,8 @@ impl ProfileUpdateInternal { payout_routing_algorithm_id: payout_routing_algorithm_id .or(source.payout_routing_algorithm_id), default_fallback_routing: default_fallback_routing.or(source.default_fallback_routing), + should_collect_cvv_during_payment: should_collect_cvv_during_payment + .unwrap_or(source.should_collect_cvv_during_payment), version: source.version, dynamic_routing_algorithm: None, is_network_tokenization_enabled: is_network_tokenization_enabled diff --git a/crates/diesel_models/src/payment_attempt.rs b/crates/diesel_models/src/payment_attempt.rs index f8654d3cd963..f08847651e76 100644 --- a/crates/diesel_models/src/payment_attempt.rs +++ b/crates/diesel_models/src/payment_attempt.rs @@ -20,7 +20,7 @@ common_utils::impl_to_sql_from_sql_json!(ConnectorMandateReferenceId); pub struct ConnectorMandateReferenceId { pub connector_mandate_id: Option, pub payment_method_id: Option, - pub mandate_metadata: Option, + pub mandate_metadata: Option, pub connector_mandate_request_reference_id: Option, } diff --git a/crates/diesel_models/src/payment_method.rs b/crates/diesel_models/src/payment_method.rs index 3ad58e62dcf9..9ca88267ae65 100644 --- a/crates/diesel_models/src/payment_method.rs +++ b/crates/diesel_models/src/payment_method.rs @@ -1,3 +1,5 @@ +use std::collections::HashMap; + use common_enums::MerchantStorageScheme; use common_utils::{encryption::Encryption, pii}; use diesel::{AsChangeset, Identifiable, Insertable, Queryable, Selectable}; @@ -65,9 +67,7 @@ pub struct PaymentMethod { } #[cfg(all(feature = "v2", feature = "payment_methods_v2"))] -#[derive( - Clone, Debug, Eq, PartialEq, Identifiable, Queryable, Selectable, Serialize, Deserialize, -)] +#[derive(Clone, Debug, Identifiable, Queryable, Selectable, Serialize, Deserialize)] #[diesel(table_name = payment_methods, primary_key(id), check_for_backend(diesel::pg::Pg))] pub struct PaymentMethod { pub customer_id: common_utils::id_type::CustomerId, @@ -76,11 +76,10 @@ pub struct PaymentMethod { pub last_modified: PrimitiveDateTime, pub payment_method: Option, pub payment_method_type: Option, - pub metadata: Option, pub payment_method_data: Option, pub locker_id: Option, pub last_used_at: PrimitiveDateTime, - pub connector_mandate_details: Option, + pub connector_mandate_details: Option, pub customer_acceptance: Option, pub status: storage_enums::PaymentMethodStatus, pub network_transaction_id: Option, @@ -156,9 +155,7 @@ pub struct PaymentMethodNew { } #[cfg(all(feature = "v2", feature = "payment_methods_v2"))] -#[derive( - Clone, Debug, Eq, PartialEq, Insertable, router_derive::DebugAsDisplay, Serialize, Deserialize, -)] +#[derive(Clone, Debug, Insertable, router_derive::DebugAsDisplay, Serialize, Deserialize)] #[diesel(table_name = payment_methods)] pub struct PaymentMethodNew { pub customer_id: common_utils::id_type::CustomerId, @@ -167,11 +164,10 @@ pub struct PaymentMethodNew { pub payment_method_type: Option, pub created_at: PrimitiveDateTime, pub last_modified: PrimitiveDateTime, - pub metadata: Option, pub payment_method_data: Option, pub locker_id: Option, pub last_used_at: PrimitiveDateTime, - pub connector_mandate_details: Option, + pub connector_mandate_details: Option, pub customer_acceptance: Option, pub status: storage_enums::PaymentMethodStatus, pub network_transaction_id: Option, @@ -257,10 +253,6 @@ pub enum PaymentMethodUpdate { #[cfg(all(feature = "v2", feature = "payment_methods_v2"))] #[derive(Debug, Serialize, Deserialize)] pub enum PaymentMethodUpdate { - MetadataUpdateAndLastUsed { - metadata: Option, - last_used_at: PrimitiveDateTime, - }, UpdatePaymentMethodDataAndLastUsed { payment_method_data: Option, last_used_at: PrimitiveDateTime, @@ -289,7 +281,7 @@ pub enum PaymentMethodUpdate { network_token_payment_method_data: Option, }, ConnectorMandateDetailsUpdate { - connector_mandate_details: Option, + connector_mandate_details: Option, }, } @@ -308,14 +300,13 @@ impl PaymentMethodUpdate { #[derive(Clone, Debug, AsChangeset, router_derive::DebugAsDisplay, Serialize, Deserialize)] #[diesel(table_name = payment_methods)] pub struct PaymentMethodUpdateInternal { - metadata: Option, payment_method_data: Option, last_used_at: Option, network_transaction_id: Option, status: Option, locker_id: Option, payment_method: Option, - connector_mandate_details: Option, + connector_mandate_details: Option, updated_by: Option, payment_method_type: Option, last_modified: PrimitiveDateTime, @@ -326,36 +317,49 @@ pub struct PaymentMethodUpdateInternal { #[cfg(all(feature = "v2", feature = "payment_methods_v2"))] impl PaymentMethodUpdateInternal { - pub fn create_payment_method(self, source: PaymentMethod) -> PaymentMethod { - let metadata = self.metadata; - - PaymentMethod { metadata, ..source } - } - pub fn apply_changeset(self, source: PaymentMethod) -> PaymentMethod { let Self { - metadata, payment_method_data, last_used_at, network_transaction_id, status, + locker_id, + payment_method, connector_mandate_details, updated_by, - .. + payment_method_type, + last_modified, + network_token_requestor_reference_id, + network_token_locker_id, + network_token_payment_method_data, } = self; PaymentMethod { - metadata: metadata.map_or(source.metadata, Some), - payment_method_data: payment_method_data.map_or(source.payment_method_data, Some), + customer_id: source.customer_id, + merchant_id: source.merchant_id, + created_at: source.created_at, + last_modified, + payment_method: payment_method.or(source.payment_method), + payment_method_type: payment_method_type.or(source.payment_method_type), + payment_method_data: payment_method_data.or(source.payment_method_data), + locker_id: locker_id.or(source.locker_id), last_used_at: last_used_at.unwrap_or(source.last_used_at), - network_transaction_id: network_transaction_id - .map_or(source.network_transaction_id, Some), - status: status.unwrap_or(source.status), connector_mandate_details: connector_mandate_details - .map_or(source.connector_mandate_details, Some), - updated_by: updated_by.map_or(source.updated_by, Some), - last_modified: common_utils::date_time::now(), - ..source + .or(source.connector_mandate_details), + customer_acceptance: source.customer_acceptance, + status: status.unwrap_or(source.status), + network_transaction_id: network_transaction_id.or(source.network_transaction_id), + client_secret: source.client_secret, + payment_method_billing_address: source.payment_method_billing_address, + updated_by: updated_by.or(source.updated_by), + locker_fingerprint_id: source.locker_fingerprint_id, + id: source.id, + version: source.version, + network_token_requestor_reference_id: network_token_requestor_reference_id + .or(source.network_token_requestor_reference_id), + network_token_locker_id: network_token_locker_id.or(source.network_token_locker_id), + network_token_payment_method_data: network_token_payment_method_data + .or(source.network_token_payment_method_data), } } } @@ -389,12 +393,6 @@ pub struct PaymentMethodUpdateInternal { not(feature = "payment_methods_v2") ))] impl PaymentMethodUpdateInternal { - pub fn create_payment_method(self, source: PaymentMethod) -> PaymentMethod { - let metadata = self.metadata.map(Secret::new); - - PaymentMethod { metadata, ..source } - } - pub fn apply_changeset(self, source: PaymentMethod) -> PaymentMethod { let Self { metadata, @@ -402,23 +400,56 @@ impl PaymentMethodUpdateInternal { last_used_at, network_transaction_id, status, + locker_id, + network_token_requestor_reference_id, + payment_method, connector_mandate_details, updated_by, - .. + payment_method_type, + payment_method_issuer, + last_modified, + network_token_locker_id, + network_token_payment_method_data, } = self; PaymentMethod { + customer_id: source.customer_id, + merchant_id: source.merchant_id, + payment_method_id: source.payment_method_id, + accepted_currency: source.accepted_currency, + scheme: source.scheme, + token: source.token, + cardholder_name: source.cardholder_name, + issuer_name: source.issuer_name, + issuer_country: source.issuer_country, + payer_country: source.payer_country, + is_stored: source.is_stored, + swift_code: source.swift_code, + direct_debit_token: source.direct_debit_token, + created_at: source.created_at, + last_modified, + payment_method: payment_method.or(source.payment_method), + payment_method_type: payment_method_type.or(source.payment_method_type), + payment_method_issuer: payment_method_issuer.or(source.payment_method_issuer), + payment_method_issuer_code: source.payment_method_issuer_code, metadata: metadata.map_or(source.metadata, |v| Some(v.into())), - payment_method_data: payment_method_data.map_or(source.payment_method_data, Some), + payment_method_data: payment_method_data.or(source.payment_method_data), + locker_id: locker_id.or(source.locker_id), last_used_at: last_used_at.unwrap_or(source.last_used_at), - network_transaction_id: network_transaction_id - .map_or(source.network_transaction_id, Some), - status: status.unwrap_or(source.status), connector_mandate_details: connector_mandate_details - .map_or(source.connector_mandate_details, Some), - updated_by: updated_by.map_or(source.updated_by, Some), - last_modified: common_utils::date_time::now(), - ..source + .or(source.connector_mandate_details), + customer_acceptance: source.customer_acceptance, + status: status.unwrap_or(source.status), + network_transaction_id: network_transaction_id.or(source.network_transaction_id), + client_secret: source.client_secret, + payment_method_billing_address: source.payment_method_billing_address, + updated_by: updated_by.or(source.updated_by), + version: source.version, + network_token_requestor_reference_id: network_token_requestor_reference_id + .or(source.network_token_requestor_reference_id), + network_token_locker_id: network_token_locker_id.or(source.network_token_locker_id), + network_token_payment_method_data: network_token_payment_method_data + .or(source.network_token_payment_method_data), } } } @@ -597,29 +628,9 @@ impl From for PaymentMethodUpdateInternal { impl From for PaymentMethodUpdateInternal { fn from(payment_method_update: PaymentMethodUpdate) -> Self { match payment_method_update { - PaymentMethodUpdate::MetadataUpdateAndLastUsed { - metadata, - last_used_at, - } => Self { - metadata, - payment_method_data: None, - last_used_at: Some(last_used_at), - network_transaction_id: None, - status: None, - locker_id: None, - payment_method: None, - connector_mandate_details: None, - updated_by: None, - payment_method_type: None, - last_modified: common_utils::date_time::now(), - network_token_locker_id: None, - network_token_requestor_reference_id: None, - network_token_payment_method_data: None, - }, PaymentMethodUpdate::PaymentMethodDataUpdate { payment_method_data, } => Self { - metadata: None, payment_method_data, last_used_at: None, network_transaction_id: None, @@ -635,7 +646,6 @@ impl From for PaymentMethodUpdateInternal { network_token_payment_method_data: None, }, PaymentMethodUpdate::LastUsedUpdate { last_used_at } => Self { - metadata: None, payment_method_data: None, last_used_at: Some(last_used_at), network_transaction_id: None, @@ -654,7 +664,6 @@ impl From for PaymentMethodUpdateInternal { payment_method_data, last_used_at, } => Self { - metadata: None, payment_method_data, last_used_at: Some(last_used_at), network_transaction_id: None, @@ -673,7 +682,6 @@ impl From for PaymentMethodUpdateInternal { network_transaction_id, status, } => Self { - metadata: None, payment_method_data: None, last_used_at: None, network_transaction_id, @@ -689,7 +697,6 @@ impl From for PaymentMethodUpdateInternal { network_token_payment_method_data: None, }, PaymentMethodUpdate::StatusUpdate { status } => Self { - metadata: None, payment_method_data: None, last_used_at: None, network_transaction_id: None, @@ -714,7 +721,6 @@ impl From for PaymentMethodUpdateInternal { network_token_locker_id, network_token_payment_method_data, } => Self { - metadata: None, payment_method_data, last_used_at: None, network_transaction_id: None, @@ -732,7 +738,6 @@ impl From for PaymentMethodUpdateInternal { PaymentMethodUpdate::ConnectorMandateDetailsUpdate { connector_mandate_details, } => Self { - metadata: None, payment_method_data: None, last_used_at: None, status: None, @@ -813,7 +818,6 @@ impl From<&PaymentMethodNew> for PaymentMethod { last_modified: payment_method_new.last_modified, payment_method: payment_method_new.payment_method, payment_method_type: payment_method_new.payment_method_type, - metadata: payment_method_new.metadata.clone(), payment_method_data: payment_method_new.payment_method_data.clone(), last_used_at: payment_method_new.last_used_at, connector_mandate_details: payment_method_new.connector_mandate_details.clone(), @@ -838,3 +842,37 @@ impl From<&PaymentMethodNew> for PaymentMethod { } } } + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +pub struct PaymentsMandateReferenceRecord { + pub connector_mandate_id: String, + pub payment_method_type: Option, + pub original_payment_authorized_amount: Option, + pub original_payment_authorized_currency: Option, + pub mandate_metadata: Option, + pub connector_mandate_status: Option, + pub connector_mandate_request_reference_id: Option, +} + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, diesel::AsExpression)] +#[diesel(sql_type = diesel::sql_types::Jsonb)] +pub struct PaymentsMandateReference( + pub HashMap, +); + +impl std::ops::Deref for PaymentsMandateReference { + type Target = + HashMap; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl std::ops::DerefMut for PaymentsMandateReference { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + +common_utils::impl_to_sql_from_sql_json!(PaymentsMandateReference); diff --git a/crates/diesel_models/src/schema_v2.rs b/crates/diesel_models/src/schema_v2.rs index 5651bf95dd9d..617d6cf182b9 100644 --- a/crates/diesel_models/src/schema_v2.rs +++ b/crates/diesel_models/src/schema_v2.rs @@ -213,6 +213,7 @@ diesel::table! { #[max_length = 64] payout_routing_algorithm_id -> Nullable, default_fallback_routing -> Nullable, + should_collect_cvv_during_payment -> Bool, #[max_length = 64] id -> Varchar, version -> ApiVersion, @@ -940,7 +941,6 @@ diesel::table! { payment_method -> Nullable, #[max_length = 64] payment_method_type -> Nullable, - metadata -> Nullable, payment_method_data -> Nullable, #[max_length = 64] locker_id -> Nullable, diff --git a/crates/hyperswitch_connectors/src/connectors/deutschebank/transformers.rs b/crates/hyperswitch_connectors/src/connectors/deutschebank/transformers.rs index 9dd842768f19..328940d83aee 100644 --- a/crates/hyperswitch_connectors/src/connectors/deutschebank/transformers.rs +++ b/crates/hyperswitch_connectors/src/connectors/deutschebank/transformers.rs @@ -310,7 +310,8 @@ impl Box::new(Some(MandateReference { connector_mandate_id: item.response.mandate_id, payment_method_id: None, - mandate_metadata: Some(serde_json::json!(DeutschebankMandateMetadata { + mandate_metadata: Some(Secret::new( + serde_json::json!(DeutschebankMandateMetadata { account_holder: item.data.get_billing_address()?.get_full_name()?, iban: match item.data.request.payment_method_data.clone() { PaymentMethodData::BankDebit(BankDebitData::SepaBankDebit { @@ -324,7 +325,8 @@ impl }?, reference: Secret::from(reference.clone()), signed_on, - })), + }), + )), connector_mandate_request_reference_id: None, })) } else { diff --git a/crates/hyperswitch_domain_models/src/business_profile.rs b/crates/hyperswitch_domain_models/src/business_profile.rs index 75cdd40024f8..fb846e288626 100644 --- a/crates/hyperswitch_domain_models/src/business_profile.rs +++ b/crates/hyperswitch_domain_models/src/business_profile.rs @@ -208,13 +208,13 @@ pub enum ProfileUpdate { dynamic_routing_algorithm: Option, }, ExtendedCardInfoUpdate { - is_extended_card_info_enabled: Option, + is_extended_card_info_enabled: bool, }, ConnectorAgnosticMitUpdate { - is_connector_agnostic_mit_enabled: Option, + is_connector_agnostic_mit_enabled: bool, }, NetworkTokenizationUpdate { - is_network_tokenization_enabled: Option, + is_network_tokenization_enabled: bool, }, } @@ -391,7 +391,7 @@ impl From for ProfileUpdateInternal { session_expiry: None, authentication_connector_details: None, payout_link_config: None, - is_extended_card_info_enabled, + is_extended_card_info_enabled: Some(is_extended_card_info_enabled), extended_card_info_config: None, is_connector_agnostic_mit_enabled: None, use_billing_as_payment_method_billing: None, @@ -430,7 +430,7 @@ impl From for ProfileUpdateInternal { payout_link_config: None, is_extended_card_info_enabled: None, extended_card_info_config: None, - is_connector_agnostic_mit_enabled, + is_connector_agnostic_mit_enabled: Some(is_connector_agnostic_mit_enabled), use_billing_as_payment_method_billing: None, collect_shipping_details_from_wallet_connector: None, collect_billing_details_from_wallet_connector: None, @@ -477,7 +477,7 @@ impl From for ProfileUpdateInternal { tax_connector_id: None, is_tax_connector_enabled: None, dynamic_routing_algorithm: None, - is_network_tokenization_enabled, + is_network_tokenization_enabled: Some(is_network_tokenization_enabled), is_auto_retries_enabled: None, max_auto_retries_enabled: None, }, @@ -695,6 +695,7 @@ pub struct Profile { pub frm_routing_algorithm_id: Option, pub payout_routing_algorithm_id: Option, pub default_fallback_routing: Option, + pub should_collect_cvv_during_payment: bool, pub tax_connector_id: Option, pub is_tax_connector_enabled: bool, pub version: common_enums::ApiVersion, @@ -735,6 +736,7 @@ pub struct ProfileSetter { pub frm_routing_algorithm_id: Option, pub payout_routing_algorithm_id: Option, pub default_fallback_routing: Option, + pub should_collect_cvv_during_payment: bool, pub tax_connector_id: Option, pub is_tax_connector_enabled: bool, pub is_network_tokenization_enabled: bool, @@ -780,6 +782,7 @@ impl From for Profile { frm_routing_algorithm_id: value.frm_routing_algorithm_id, payout_routing_algorithm_id: value.payout_routing_algorithm_id, default_fallback_routing: value.default_fallback_routing, + should_collect_cvv_during_payment: value.should_collect_cvv_during_payment, tax_connector_id: value.tax_connector_id, is_tax_connector_enabled: value.is_tax_connector_enabled, version: consts::API_VERSION, @@ -848,13 +851,16 @@ pub enum ProfileUpdate { default_fallback_routing: Option, }, ExtendedCardInfoUpdate { - is_extended_card_info_enabled: Option, + is_extended_card_info_enabled: bool, }, ConnectorAgnosticMitUpdate { - is_connector_agnostic_mit_enabled: Option, + is_connector_agnostic_mit_enabled: bool, }, NetworkTokenizationUpdate { - is_network_tokenization_enabled: Option, + is_network_tokenization_enabled: bool, + }, + CollectCvvDuringPaymentUpdate { + should_collect_cvv_during_payment: bool, }, } @@ -921,6 +927,7 @@ impl From for ProfileUpdateInternal { frm_routing_algorithm_id: None, payout_routing_algorithm_id: None, default_fallback_routing: None, + should_collect_cvv_during_payment: None, tax_connector_id: None, is_tax_connector_enabled: None, is_network_tokenization_enabled, @@ -961,6 +968,7 @@ impl From for ProfileUpdateInternal { frm_routing_algorithm_id: None, payout_routing_algorithm_id, default_fallback_routing: None, + should_collect_cvv_during_payment: None, tax_connector_id: None, is_tax_connector_enabled: None, is_network_tokenization_enabled: None, @@ -984,7 +992,7 @@ impl From for ProfileUpdateInternal { session_expiry: None, authentication_connector_details: None, payout_link_config: None, - is_extended_card_info_enabled, + is_extended_card_info_enabled: Some(is_extended_card_info_enabled), extended_card_info_config: None, is_connector_agnostic_mit_enabled: None, use_billing_as_payment_method_billing: None, @@ -999,6 +1007,7 @@ impl From for ProfileUpdateInternal { order_fulfillment_time_origin: None, frm_routing_algorithm_id: None, default_fallback_routing: None, + should_collect_cvv_during_payment: None, tax_connector_id: None, is_tax_connector_enabled: None, is_network_tokenization_enabled: None, @@ -1024,7 +1033,7 @@ impl From for ProfileUpdateInternal { payout_link_config: None, is_extended_card_info_enabled: None, extended_card_info_config: None, - is_connector_agnostic_mit_enabled, + is_connector_agnostic_mit_enabled: Some(is_connector_agnostic_mit_enabled), use_billing_as_payment_method_billing: None, collect_shipping_details_from_wallet_connector: None, collect_billing_details_from_wallet_connector: None, @@ -1037,6 +1046,7 @@ impl From for ProfileUpdateInternal { order_fulfillment_time_origin: None, frm_routing_algorithm_id: None, default_fallback_routing: None, + should_collect_cvv_during_payment: None, tax_connector_id: None, is_tax_connector_enabled: None, is_network_tokenization_enabled: None, @@ -1075,6 +1085,7 @@ impl From for ProfileUpdateInternal { order_fulfillment_time_origin: None, frm_routing_algorithm_id: None, default_fallback_routing, + should_collect_cvv_during_payment: None, tax_connector_id: None, is_tax_connector_enabled: None, is_network_tokenization_enabled: None, @@ -1113,9 +1124,49 @@ impl From for ProfileUpdateInternal { order_fulfillment_time_origin: None, frm_routing_algorithm_id: None, default_fallback_routing: None, + should_collect_cvv_during_payment: None, tax_connector_id: None, is_tax_connector_enabled: None, - is_network_tokenization_enabled, + is_network_tokenization_enabled: Some(is_network_tokenization_enabled), + is_auto_retries_enabled: None, + max_auto_retries_enabled: None, + }, + ProfileUpdate::CollectCvvDuringPaymentUpdate { + should_collect_cvv_during_payment, + } => Self { + profile_name: None, + modified_at: now, + return_url: None, + enable_payment_response_hash: None, + payment_response_hash_key: None, + redirect_to_merchant_with_http_post: None, + webhook_details: None, + metadata: None, + is_recon_enabled: None, + applepay_verified_domains: None, + payment_link_config: None, + session_expiry: None, + authentication_connector_details: None, + payout_link_config: None, + is_extended_card_info_enabled: None, + extended_card_info_config: None, + is_connector_agnostic_mit_enabled: None, + use_billing_as_payment_method_billing: None, + collect_shipping_details_from_wallet_connector: None, + collect_billing_details_from_wallet_connector: None, + outgoing_webhook_custom_http_headers: None, + always_collect_billing_details_from_wallet_connector: None, + always_collect_shipping_details_from_wallet_connector: None, + routing_algorithm_id: None, + payout_routing_algorithm_id: None, + order_fulfillment_time: None, + order_fulfillment_time_origin: None, + frm_routing_algorithm_id: None, + default_fallback_routing: None, + should_collect_cvv_during_payment: Some(should_collect_cvv_during_payment), + tax_connector_id: None, + is_tax_connector_enabled: None, + is_network_tokenization_enabled: None, is_auto_retries_enabled: None, max_auto_retries_enabled: None, }, @@ -1169,6 +1220,7 @@ impl super::behaviour::Conversion for Profile { order_fulfillment_time_origin: self.order_fulfillment_time_origin, frm_routing_algorithm_id: self.frm_routing_algorithm_id, default_fallback_routing: self.default_fallback_routing, + should_collect_cvv_during_payment: self.should_collect_cvv_during_payment, tax_connector_id: self.tax_connector_id, is_tax_connector_enabled: Some(self.is_tax_connector_enabled), version: self.version, @@ -1239,6 +1291,7 @@ impl super::behaviour::Conversion for Profile { frm_routing_algorithm_id: item.frm_routing_algorithm_id, payout_routing_algorithm_id: item.payout_routing_algorithm_id, default_fallback_routing: item.default_fallback_routing, + should_collect_cvv_during_payment: item.should_collect_cvv_during_payment, tax_connector_id: item.tax_connector_id, is_tax_connector_enabled: item.is_tax_connector_enabled.unwrap_or(false), version: item.version, @@ -1291,6 +1344,7 @@ impl super::behaviour::Conversion for Profile { frm_routing_algorithm_id: self.frm_routing_algorithm_id, payout_routing_algorithm_id: self.payout_routing_algorithm_id, default_fallback_routing: self.default_fallback_routing, + should_collect_cvv_during_payment: self.should_collect_cvv_during_payment, tax_connector_id: self.tax_connector_id, is_tax_connector_enabled: Some(self.is_tax_connector_enabled), version: self.version, diff --git a/crates/hyperswitch_domain_models/src/merchant_connector_account.rs b/crates/hyperswitch_domain_models/src/merchant_connector_account.rs index 0d730be203b1..ed2388488e27 100644 --- a/crates/hyperswitch_domain_models/src/merchant_connector_account.rs +++ b/crates/hyperswitch_domain_models/src/merchant_connector_account.rs @@ -511,3 +511,32 @@ impl From for MerchantConnectorAccountUpdateInte } } } + +#[derive(Debug)] +pub struct MerchantConnectorAccounts(Vec); + +impl MerchantConnectorAccounts { + pub fn new(merchant_connector_accounts: Vec) -> Self { + Self(merchant_connector_accounts) + } + + pub fn is_merchant_connector_account_id_in_connector_mandate_details( + &self, + profile_id: Option<&id_type::ProfileId>, + connector_mandate_details: &diesel_models::PaymentsMandateReference, + ) -> bool { + let mca_ids = self + .0 + .iter() + .filter(|mca| { + mca.disabled.is_some_and(|disabled| !disabled) + && profile_id.is_some_and(|profile_id| *profile_id == mca.profile_id) + }) + .map(|mca| mca.get_id()) + .collect::>(); + + connector_mandate_details + .keys() + .any(|mca_id| mca_ids.contains(mca_id)) + } +} diff --git a/crates/hyperswitch_domain_models/src/payment_methods.rs b/crates/hyperswitch_domain_models/src/payment_methods.rs index 445ad5ab8005..4ff246d3760b 100644 --- a/crates/hyperswitch_domain_models/src/payment_methods.rs +++ b/crates/hyperswitch_domain_models/src/payment_methods.rs @@ -79,11 +79,10 @@ pub struct PaymentMethod { pub last_modified: PrimitiveDateTime, pub payment_method: Option, pub payment_method_type: Option, - pub metadata: Option, pub payment_method_data: OptionalEncryptableValue, pub locker_id: Option, pub last_used_at: PrimitiveDateTime, - pub connector_mandate_details: Option, + pub connector_mandate_details: Option, pub customer_acceptance: Option, pub status: storage_enums::PaymentMethodStatus, pub network_transaction_id: Option, @@ -314,7 +313,6 @@ impl super::behaviour::Conversion for PaymentMethod { last_modified: self.last_modified, payment_method: self.payment_method, payment_method_type: self.payment_method_type, - metadata: self.metadata, payment_method_data: self.payment_method_data.map(|val| val.into()), locker_id: self.locker_id.map(|id| id.get_string_repr().clone()), last_used_at: self.last_used_at, @@ -355,7 +353,6 @@ impl super::behaviour::Conversion for PaymentMethod { last_modified: item.last_modified, payment_method: item.payment_method, payment_method_type: item.payment_method_type, - metadata: item.metadata, payment_method_data: item .payment_method_data .async_lift(|inner| async { @@ -427,7 +424,6 @@ impl super::behaviour::Conversion for PaymentMethod { last_modified: self.last_modified, payment_method: self.payment_method, payment_method_type: self.payment_method_type, - metadata: self.metadata, payment_method_data: self.payment_method_data.map(|val| val.into()), locker_id: self.locker_id.map(|id| id.get_string_repr().clone()), last_used_at: self.last_used_at, diff --git a/crates/hyperswitch_domain_models/src/router_data.rs b/crates/hyperswitch_domain_models/src/router_data.rs index 186a4f012629..96a2a57a4bfd 100644 --- a/crates/hyperswitch_domain_models/src/router_data.rs +++ b/crates/hyperswitch_domain_models/src/router_data.rs @@ -312,7 +312,7 @@ pub struct RecurringMandatePaymentData { pub payment_method_type: Option, //required for making recurring payment using saved payment method through stripe pub original_payment_authorized_amount: Option, pub original_payment_authorized_currency: Option, - pub mandate_metadata: Option, + pub mandate_metadata: Option, } #[derive(Debug, Clone)] diff --git a/crates/hyperswitch_domain_models/src/router_response_types.rs b/crates/hyperswitch_domain_models/src/router_response_types.rs index 6356301c7264..e6af5c5eb3bc 100644 --- a/crates/hyperswitch_domain_models/src/router_response_types.rs +++ b/crates/hyperswitch_domain_models/src/router_response_types.rs @@ -82,7 +82,7 @@ pub struct TaxCalculationResponseData { pub struct MandateReference { pub connector_mandate_id: Option, pub payment_method_id: Option, - pub mandate_metadata: Option, + pub mandate_metadata: Option, pub connector_mandate_request_reference_id: Option, } diff --git a/crates/router/src/core/admin.rs b/crates/router/src/core/admin.rs index cffb8267bff9..68f2b9d63431 100644 --- a/crates/router/src/core/admin.rs +++ b/crates/router/src/core/admin.rs @@ -3659,6 +3659,7 @@ impl ProfileCreateBridge for api::ProfileCreate { .or(Some(common_utils::consts::DEFAULT_ORDER_FULFILLMENT_TIME)), order_fulfillment_time_origin: self.order_fulfillment_time_origin, default_fallback_routing: None, + should_collect_cvv_during_payment: false, tax_connector_id: self.tax_connector_id, is_tax_connector_enabled: self.is_tax_connector_enabled, is_network_tokenization_enabled: self.is_network_tokenization_enabled, @@ -4222,7 +4223,7 @@ pub async fn extended_card_info_toggle( .is_some_and(|existing_config| existing_config != ext_card_info_choice.enabled) { let profile_update = domain::ProfileUpdate::ExtendedCardInfoUpdate { - is_extended_card_info_enabled: Some(ext_card_info_choice.enabled), + is_extended_card_info_enabled: ext_card_info_choice.enabled, }; db.update_profile_by_profile_id( @@ -4276,7 +4277,7 @@ pub async fn connector_agnostic_mit_toggle( != Some(connector_agnostic_mit_choice.enabled) { let profile_update = domain::ProfileUpdate::ConnectorAgnosticMitUpdate { - is_connector_agnostic_mit_enabled: Some(connector_agnostic_mit_choice.enabled), + is_connector_agnostic_mit_enabled: connector_agnostic_mit_choice.enabled, }; db.update_profile_by_profile_id( diff --git a/crates/router/src/core/payment_methods.rs b/crates/router/src/core/payment_methods.rs index 87078dfa5aff..a765c1feaa96 100644 --- a/crates/router/src/core/payment_methods.rs +++ b/crates/router/src/core/payment_methods.rs @@ -1130,11 +1130,10 @@ pub async fn create_payment_method_in_db( payment_method_id: id_type::GlobalPaymentMethodId, locker_id: Option, merchant_id: &id_type::MerchantId, - pm_metadata: Option, customer_acceptance: Option, payment_method_data: crypto::OptionalEncryptableValue, key_store: &domain::MerchantKeyStore, - connector_mandate_details: Option, + connector_mandate_details: Option, status: Option, network_transaction_id: Option, storage_scheme: enums::MerchantStorageScheme, @@ -1156,7 +1155,6 @@ pub async fn create_payment_method_in_db( locker_id, payment_method: Some(req.payment_method), payment_method_type: Some(req.payment_method_type), - metadata: pm_metadata, payment_method_data, connector_mandate_details, customer_acceptance, @@ -1211,7 +1209,6 @@ pub async fn create_payment_method_for_intent( locker_id: None, payment_method: None, payment_method_type: None, - metadata, payment_method_data: None, connector_mandate_details: None, customer_acceptance: None, @@ -1400,6 +1397,7 @@ async fn get_pm_list_context( pub async fn list_customer_payment_method_util( state: SessionState, merchant_account: domain::MerchantAccount, + profile: domain::Profile, key_store: domain::MerchantKeyStore, req: Option, customer_id: Option, @@ -1430,7 +1428,8 @@ pub async fn list_customer_payment_method_util( let resp = if let Some(cust) = customer_id { Box::pin(list_customer_payment_method( &state, - merchant_account, + &merchant_account, + profile, key_store, payment_intent, &cust, @@ -1449,6 +1448,7 @@ pub async fn list_customer_payment_method_util( Ok(resp) } +#[allow(clippy::too_many_arguments)] #[cfg(all( feature = "v2", feature = "payment_methods_v2", @@ -1456,7 +1456,8 @@ pub async fn list_customer_payment_method_util( ))] pub async fn list_customer_payment_method( state: &SessionState, - merchant_account: domain::MerchantAccount, + merchant_account: &domain::MerchantAccount, + profile: domain::Profile, key_store: domain::MerchantKeyStore, payment_intent: Option, customer_id: &id_type::CustomerId, @@ -1465,7 +1466,6 @@ pub async fn list_customer_payment_method( ) -> RouterResponse { let db = &*state.store; let key_manager_state = &(state).into(); - // let key = key_store.key.get_inner().peek(); let customer = db .find_customer_by_merchant_reference_id_merchant_id( @@ -1482,7 +1482,8 @@ pub async fn list_customer_payment_method( .async_map(|pi| { pm_types::SavedPMLPaymentsInfo::form_payments_info( pi, - &merchant_account, + merchant_account, + profile, db, key_manager_state, &key_store, @@ -1525,13 +1526,33 @@ pub async fn list_customer_payment_method( } } + let merchant_connector_accounts = if filtered_saved_payment_methods_ctx.iter().any( + |(_pm_list_context, _parent_payment_method_token, pm)| { + pm.connector_mandate_details.is_some() + }, + ) { + db.find_merchant_connector_account_by_merchant_id_and_disabled_list( + key_manager_state, + merchant_account.get_id(), + true, + &key_store, + ) + .await + .change_context(errors::ApiErrorResponse::MerchantAccountNotFound)? + } else { + Vec::new() + }; + let merchant_connector_accounts = + domain::MerchantConnectorAccounts::new(merchant_connector_accounts); + let pm_list_futures = filtered_saved_payment_methods_ctx .into_iter() .map(|ctx| { generate_saved_pm_response( state, &key_store, - &merchant_account, + merchant_account, + &merchant_connector_accounts, ctx, &customer, payments_info.as_ref(), @@ -1539,13 +1560,11 @@ pub async fn list_customer_payment_method( }) .collect::>(); - let final_result = futures::future::join_all(pm_list_futures).await; - - let mut customer_pms = Vec::new(); - for result in final_result.into_iter() { - let pma = result.attach_printable("saved pm list failed")?; - customer_pms.push(pma); - } + let customer_pms = futures::future::join_all(pm_list_futures) + .await + .into_iter() + .collect::, _>>() + .attach_printable("Failed to obtain customer payment methods")?; let mut response = api::CustomerPaymentMethodsListResponse { customer_payment_methods: customer_pms, @@ -1558,7 +1577,7 @@ pub async fn list_customer_payment_method( state, merchant_account, key_store, - payments_info.and_then(|pi| pi.business_profile), + payments_info.map(|pi| pi.profile), &mut response, )) .await?; @@ -1572,7 +1591,8 @@ async fn generate_saved_pm_response( state: &SessionState, key_store: &domain::MerchantKeyStore, merchant_account: &domain::MerchantAccount, - pm_list_context: ( + merchant_connector_accounts: &domain::MerchantConnectorAccounts, + (pm_list_context, parent_payment_method_token, pm): ( PaymentMethodListContext, Option, domain::PaymentMethod, @@ -1580,7 +1600,6 @@ async fn generate_saved_pm_response( customer: &domain::Customer, payment_info: Option<&pm_types::SavedPMLPaymentsInfo>, ) -> Result> { - let (pm_list_context, parent_payment_method_token, pm) = pm_list_context; let payment_method = pm.payment_method.get_required_value("payment_method")?; let bank_details = if payment_method == enums::PaymentMethod::BankDebit { @@ -1603,24 +1622,14 @@ async fn generate_saved_pm_response( .change_context(errors::ApiErrorResponse::InternalServerError) .attach_printable("unable to parse payment method billing address details")?; - let connector_mandate_details = pm - .connector_mandate_details - .clone() - .map(|val| val.parse_value::("PaymentsMandateReference")) - .transpose() - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Failed to deserialize to Payment Mandate Reference ")?; - let (is_connector_agnostic_mit_enabled, requires_cvv, off_session_payment_flag, profile_id) = payment_info .map(|pi| { ( pi.is_connector_agnostic_mit_enabled, - pi.requires_cvv, + pi.collect_cvv_during_payment, pi.off_session_payment_flag, - pi.business_profile - .as_ref() - .map(|profile| profile.get_id().to_owned()), + Some(pi.profile.get_id().to_owned()), ) }) .unwrap_or((false, false, false, Default::default())); @@ -1631,10 +1640,11 @@ async fn generate_saved_pm_response( profile_id, merchant_account.get_id(), is_connector_agnostic_mit_enabled, - connector_mandate_details, + pm.connector_mandate_details.as_ref(), pm.network_transaction_id.as_ref(), + merchant_connector_accounts, ) - .await?; + .await; let requires_cvv = if is_connector_agnostic_mit_enabled { requires_cvv @@ -1662,7 +1672,6 @@ async fn generate_saved_pm_response( payment_method, payment_method_type: pm.payment_method_type, payment_method_data: pmd, - metadata: pm.metadata.clone(), recurring_enabled: mca_enabled, created: Some(pm.created_at), bank: bank_details, @@ -1670,8 +1679,10 @@ async fn generate_saved_pm_response( requires_cvv: requires_cvv && !(off_session_payment_flag && pm.connector_mandate_details.is_some()), last_used_at: Some(pm.last_used_at), - is_default: customer.default_payment_method_id.is_some() - && customer.default_payment_method_id.as_deref() == Some(pm.get_id().get_string_repr()), + is_default: customer + .default_payment_method_id + .as_ref() + .is_some_and(|payment_method_id| payment_method_id == pm.get_id().get_string_repr()), billing: payment_method_billing, }; @@ -1726,7 +1737,6 @@ pub async fn retrieve_payment_method( payment_method_id: payment_method.id.get_string_repr().to_string(), payment_method: payment_method.payment_method, payment_method_type: payment_method.payment_method_type, - metadata: payment_method.metadata.clone(), created: Some(payment_method.created_at), recurring_enabled: false, last_used_at: Some(payment_method.last_used_at), @@ -1902,50 +1912,25 @@ impl pm_types::SavedPMLPaymentsInfo { pub async fn form_payments_info( payment_intent: PaymentIntent, merchant_account: &domain::MerchantAccount, + profile: domain::Profile, db: &dyn StorageInterface, key_manager_state: &util_types::keymanager::KeyManagerState, key_store: &domain::MerchantKeyStore, ) -> RouterResult { - let requires_cvv = db - .find_config_by_key_unwrap_or( - format!( - "{}_requires_cvv", - merchant_account.get_id().get_string_repr() - ) - .as_str(), - Some("true".to_string()), - ) - .await - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Failed to fetch requires_cvv config")? - .config - != "false"; + let collect_cvv_during_payment = profile.should_collect_cvv_during_payment; let off_session_payment_flag = matches!( payment_intent.setup_future_usage, common_enums::FutureUsage::OffSession ); - let profile_id = &payment_intent.profile_id; - - let business_profile = core_utils::validate_and_get_business_profile( - db, - key_manager_state, - key_store, - Some(profile_id), - merchant_account.get_id(), - ) - .await?; - - let is_connector_agnostic_mit_enabled = business_profile - .as_ref() - .and_then(|business_profile| business_profile.is_connector_agnostic_mit_enabled) - .unwrap_or(false); + let is_connector_agnostic_mit_enabled = + profile.is_connector_agnostic_mit_enabled.unwrap_or(false); Ok(Self { payment_intent, - business_profile, - requires_cvv, + profile, + collect_cvv_during_payment, off_session_payment_flag, is_connector_agnostic_mit_enabled, }) @@ -1966,44 +1951,14 @@ impl pm_types::SavedPMLPaymentsInfo { .get_required_value("PaymentTokenData")?; let intent_fulfillment_time = self - .business_profile - .as_ref() - .and_then(|b_profile| b_profile.get_order_fulfillment_time()) + .profile + .get_order_fulfillment_time() .unwrap_or(common_utils::consts::DEFAULT_INTENT_FULFILLMENT_TIME); pm_routes::ParentPaymentMethodToken::create_key_for_token((token, pma.payment_method)) .insert(intent_fulfillment_time, hyperswitch_token_data, state) .await?; - if let Some(metadata) = pma.metadata.clone() { - let pm_metadata_vec: pm_transforms::PaymentMethodMetadata = metadata - .parse_value("PaymentMethodMetadata") - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable( - "Failed to deserialize metadata to PaymentmethodMetadata struct", - )?; - - let redis_conn = state - .store - .get_redis_conn() - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Failed to get redis connection")?; - - for pm_metadata in pm_metadata_vec.payment_method_tokenization { - let key = format!( - "pm_token_{}_{}_{}", - token, pma.payment_method, pm_metadata.0 - ); - - redis_conn - .set_key_with_expiry(&key, pm_metadata.1, intent_fulfillment_time) - .await - .change_context(errors::StorageError::KVError) - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Failed to add data in redis")?; - } - } - Ok(()) } } diff --git a/crates/router/src/core/payment_methods/cards.rs b/crates/router/src/core/payment_methods/cards.rs index cbd535903ae2..fcf0c6ca4d1d 100644 --- a/crates/router/src/core/payment_methods/cards.rs +++ b/crates/router/src/core/payment_methods/cards.rs @@ -2563,25 +2563,6 @@ pub async fn update_payment_method_metadata_and_last_used( Ok(()) } -#[cfg(all(feature = "v2", feature = "payment_methods_v2"))] -pub async fn update_payment_method_metadata_and_last_used( - state: &routes::SessionState, - key_store: &domain::MerchantKeyStore, - db: &dyn db::StorageInterface, - pm: domain::PaymentMethod, - pm_metadata: Option, - storage_scheme: MerchantStorageScheme, -) -> errors::CustomResult<(), errors::VaultError> { - let pm_update = payment_method::PaymentMethodUpdate::MetadataUpdateAndLastUsed { - metadata: pm_metadata.map(Secret::new), - last_used_at: common_utils::date_time::now(), - }; - db.update_payment_method(&(state.into()), key_store, pm, pm_update, storage_scheme) - .await - .change_context(errors::VaultError::UpdateInPaymentMethodDataTableFailed)?; - Ok(()) -} - pub async fn update_payment_method_and_last_used( state: &routes::SessionState, key_store: &domain::MerchantKeyStore, @@ -2606,11 +2587,11 @@ pub async fn update_payment_method_connector_mandate_details( key_store: &domain::MerchantKeyStore, db: &dyn db::StorageInterface, pm: domain::PaymentMethod, - connector_mandate_details: Option, + connector_mandate_details: Option, storage_scheme: MerchantStorageScheme, ) -> errors::CustomResult<(), errors::VaultError> { let pm_update = payment_method::PaymentMethodUpdate::ConnectorMandateDetailsUpdate { - connector_mandate_details: connector_mandate_details.map(Secret::new), + connector_mandate_details, }; db.update_payment_method(&(state.into()), key_store, pm, pm_update, storage_scheme) @@ -4724,7 +4705,9 @@ pub async fn list_customer_payment_method( .connector_mandate_details .clone() .map(|val| { - val.parse_value::("PaymentsMandateReference") + val.parse_value::( + "PaymentsMandateReference", + ) }) .transpose() .change_context(errors::ApiErrorResponse::InternalServerError) @@ -4986,7 +4969,7 @@ async fn perform_surcharge_ops( pub async fn perform_surcharge_ops( _payment_intent: Option, _state: &routes::SessionState, - _merchant_account: domain::MerchantAccount, + _merchant_account: &domain::MerchantAccount, _key_store: domain::MerchantKeyStore, _business_profile: Option, _response: &mut api::CustomerPaymentMethodsListResponse, @@ -4994,13 +4977,14 @@ pub async fn perform_surcharge_ops( todo!() } +#[cfg(feature = "v1")] pub async fn get_mca_status( state: &routes::SessionState, key_store: &domain::MerchantKeyStore, profile_id: Option, merchant_id: &id_type::MerchantId, is_connector_agnostic_mit_enabled: bool, - connector_mandate_details: Option, + connector_mandate_details: Option, network_transaction_id: Option<&String>, ) -> errors::RouterResult { if is_connector_agnostic_mit_enabled && network_transaction_id.is_some() { @@ -5019,25 +5003,42 @@ pub async fn get_mca_status( .change_context(errors::ApiErrorResponse::MerchantConnectorAccountNotFound { id: merchant_id.get_string_repr().to_owned(), })?; - let mut mca_ids = HashSet::new(); - let mcas = mcas - .into_iter() - .filter(|mca| { - mca.disabled == Some(false) && profile_id.clone() == Some(mca.profile_id.clone()) - }) - .collect::>(); + let merchant_connector_accounts = domain::MerchantConnectorAccounts::new(mcas); - for mca in mcas { - mca_ids.insert(mca.get_id()); - } - for mca_id in connector_mandate_details.keys() { - if mca_ids.contains(mca_id) { - return Ok(true); - } - } + return Ok(merchant_connector_accounts + .is_merchant_connector_account_id_in_connector_mandate_details( + profile_id.as_ref(), + &connector_mandate_details, + )); } Ok(false) } + +#[cfg(all(feature = "v2", feature = "payment_methods_v2"))] +#[allow(clippy::too_many_arguments)] +pub async fn get_mca_status( + state: &routes::SessionState, + key_store: &domain::MerchantKeyStore, + profile_id: Option, + merchant_id: &id_type::MerchantId, + is_connector_agnostic_mit_enabled: bool, + connector_mandate_details: Option<&payment_method::PaymentsMandateReference>, + network_transaction_id: Option<&String>, + merchant_connector_accounts: &domain::MerchantConnectorAccounts, +) -> bool { + if is_connector_agnostic_mit_enabled && network_transaction_id.is_some() { + return true; + } + match connector_mandate_details { + Some(connector_mandate_details) => merchant_connector_accounts + .is_merchant_connector_account_id_in_connector_mandate_details( + profile_id.as_ref(), + connector_mandate_details, + ), + None => false, + } +} + pub async fn decrypt_generic_data( state: &routes::SessionState, data: Option, diff --git a/crates/router/src/core/payment_methods/transformers.rs b/crates/router/src/core/payment_methods/transformers.rs index 78770c34579c..51a109e8a909 100644 --- a/crates/router/src/core/payment_methods/transformers.rs +++ b/crates/router/src/core/payment_methods/transformers.rs @@ -570,7 +570,6 @@ pub fn generate_payment_method_response( payment_method_id: pm.id.get_string_repr().to_owned(), payment_method: pm.payment_method, payment_method_type: pm.payment_method_type, - metadata: pm.metadata.clone(), created: Some(pm.created_at), recurring_enabled: false, last_used_at: Some(pm.last_used_at), diff --git a/crates/router/src/core/payments.rs b/crates/router/src/core/payments.rs index 4ad00df9c247..a025b4b3cc85 100644 --- a/crates/router/src/core/payments.rs +++ b/crates/router/src/core/payments.rs @@ -5159,7 +5159,8 @@ where .connector_mandate_details .clone() .map(|details| { - details.parse_value::("connector_mandate_details") + details + .parse_value::("connector_mandate_details") }) .transpose() .change_context(errors::ApiErrorResponse::InternalServerError) diff --git a/crates/router/src/core/payments/operations/payment_response.rs b/crates/router/src/core/payments/operations/payment_response.rs index f4ec0ef9eb55..4b784ea9463b 100644 --- a/crates/router/src/core/payments/operations/payment_response.rs +++ b/crates/router/src/core/payments/operations/payment_response.rs @@ -1586,7 +1586,7 @@ async fn payment_response_update_tracker( .connector_mandate_details .clone() .map(|val| { - val.parse_value::( + val.parse_value::( "PaymentsMandateReference", ) }) @@ -2367,7 +2367,7 @@ impl PostUpdateTracker, types::PaymentsAuthor #[cfg(feature = "v1")] fn update_connector_mandate_details_for_the_flow( connector_mandate_id: Option, - mandate_metadata: Option, + mandate_metadata: Option>, connector_mandate_request_reference_id: Option, payment_data: &mut PaymentData, ) -> RouterResult<()> { diff --git a/crates/router/src/core/payments/tokenization.rs b/crates/router/src/core/payments/tokenization.rs index 553eb6724868..6f01af96bb8e 100644 --- a/crates/router/src/core/payments/tokenization.rs +++ b/crates/router/src/core/payments/tokenization.rs @@ -32,7 +32,7 @@ use crate::{ self, api::{self, CardDetailFromLocker, CardDetailsPaymentMethod, PaymentMethodCreateExt}, domain, - storage::{self, enums as storage_enums}, + storage::enums as storage_enums, }, utils::{generate_id, OptionExt}, }; @@ -1149,9 +1149,9 @@ pub fn add_connector_mandate_details_in_payment_method( authorized_currency: Option, merchant_connector_id: Option, connector_mandate_id: Option, - mandate_metadata: Option, + mandate_metadata: Option>, connector_mandate_request_reference_id: Option, -) -> Option { +) -> Option { let mut mandate_details = HashMap::new(); if let Some((mca_id, connector_mandate_id)) = @@ -1159,7 +1159,7 @@ pub fn add_connector_mandate_details_in_payment_method( { mandate_details.insert( mca_id, - storage::PaymentsMandateReferenceRecord { + diesel_models::PaymentsMandateReferenceRecord { connector_mandate_id, payment_method_type, original_payment_authorized_amount: authorized_amount, @@ -1169,20 +1169,20 @@ pub fn add_connector_mandate_details_in_payment_method( connector_mandate_request_reference_id, }, ); - Some(storage::PaymentsMandateReference(mandate_details)) + Some(diesel_models::PaymentsMandateReference(mandate_details)) } else { None } } #[allow(clippy::too_many_arguments)] pub fn update_connector_mandate_details( - mandate_details: Option, + mandate_details: Option, payment_method_type: Option, authorized_amount: Option, authorized_currency: Option, merchant_connector_id: Option, connector_mandate_id: Option, - mandate_metadata: Option, + mandate_metadata: Option>, connector_mandate_request_reference_id: Option, ) -> RouterResult> { let mandate_reference = match mandate_details { @@ -1190,7 +1190,7 @@ pub fn update_connector_mandate_details( if let Some((mca_id, connector_mandate_id)) = merchant_connector_id.clone().zip(connector_mandate_id) { - let updated_record = storage::PaymentsMandateReferenceRecord { + let updated_record = diesel_models::PaymentsMandateReferenceRecord { connector_mandate_id: connector_mandate_id.clone(), payment_method_type, original_payment_authorized_amount: authorized_amount, @@ -1204,7 +1204,7 @@ pub fn update_connector_mandate_details( payment_mandate_reference .entry(mca_id) .and_modify(|pm| *pm = updated_record) - .or_insert(storage::PaymentsMandateReferenceRecord { + .or_insert(diesel_models::PaymentsMandateReferenceRecord { connector_mandate_id, payment_method_type, original_payment_authorized_amount: authorized_amount, diff --git a/crates/router/src/db/payment_method.rs b/crates/router/src/db/payment_method.rs index ac66ed707f13..28a3ae7ec095 100644 --- a/crates/router/src/db/payment_method.rs +++ b/crates/router/src/db/payment_method.rs @@ -1,4 +1,4 @@ -use common_utils::{id_type, types::keymanager::KeyManagerState}; +use common_utils::{ext_traits::AsyncExt, id_type, types::keymanager::KeyManagerState}; use diesel_models::payment_method::PaymentMethodUpdateInternal; use error_stack::ResultExt; use hyperswitch_domain_models::behaviour::{Conversion, ReverseConversion}; @@ -1524,34 +1524,38 @@ impl PaymentMethodInterface for MockDb { payment_method_update: storage_types::PaymentMethodUpdate, _storage_scheme: MerchantStorageScheme, ) -> CustomResult { - let pm_update_res = self - .payment_methods + self.payment_methods .lock() .await .iter_mut() .find(|pm| pm.get_id() == payment_method.get_id()) - .map(|pm| { + .async_map(|pm| async { let payment_method_updated = - PaymentMethodUpdateInternal::from(payment_method_update) - .create_payment_method(pm.clone()); + PaymentMethodUpdateInternal::from(payment_method_update).apply_changeset( + Conversion::convert(payment_method) + .await + .change_context(errors::StorageError::EncryptionError)?, + ); + *pm = payment_method_updated.clone(); - payment_method_updated - }); - match pm_update_res { - Some(result) => Ok(result - .convert( - state, - key_store.key.get_inner(), - key_store.merchant_id.clone().into(), + payment_method_updated + .convert( + state, + key_store.key.get_inner(), + key_store.merchant_id.clone().into(), + ) + .await + .change_context(errors::StorageError::DecryptionError) + }) + .await + .transpose()? + .ok_or( + errors::StorageError::ValueNotFound( + "cannot find payment method to update".to_string(), ) - .await - .change_context(errors::StorageError::DecryptionError)?), - None => Err(errors::StorageError::ValueNotFound( - "cannot find payment method to update".to_string(), + .into(), ) - .into()), - } } #[cfg(all(feature = "v2", feature = "payment_methods_v2"))] @@ -1565,34 +1569,38 @@ impl PaymentMethodInterface for MockDb { status: Some(common_enums::PaymentMethodStatus::Inactive), }; - let pm_update_res = self - .payment_methods + self.payment_methods .lock() .await .iter_mut() .find(|pm| pm.get_id() == payment_method.get_id()) - .map(|pm| { + .async_map(|pm| async { let payment_method_updated = - PaymentMethodUpdateInternal::from(payment_method_update) - .create_payment_method(pm.clone()); + PaymentMethodUpdateInternal::from(payment_method_update).apply_changeset( + Conversion::convert(payment_method) + .await + .change_context(errors::StorageError::EncryptionError)?, + ); + *pm = payment_method_updated.clone(); - payment_method_updated - }); - match pm_update_res { - Some(result) => Ok(result - .convert( - state, - key_store.key.get_inner(), - key_store.merchant_id.clone().into(), + payment_method_updated + .convert( + state, + key_store.key.get_inner(), + key_store.merchant_id.clone().into(), + ) + .await + .change_context(errors::StorageError::DecryptionError) + }) + .await + .transpose()? + .ok_or( + errors::StorageError::ValueNotFound( + "cannot find payment method to update".to_string(), ) - .await - .change_context(errors::StorageError::DecryptionError)?), - None => Err(errors::StorageError::ValueNotFound( - "cannot find payment method to update".to_string(), + .into(), ) - .into()), - } } #[cfg(all(feature = "v2", feature = "payment_methods_v2"))] diff --git a/crates/router/src/routes/payment_methods.rs b/crates/router/src/routes/payment_methods.rs index b92e2f260fcb..764e75df02a0 100644 --- a/crates/router/src/routes/payment_methods.rs +++ b/crates/router/src/routes/payment_methods.rs @@ -29,7 +29,6 @@ use crate::{ domain, storage::payment_method::PaymentTokenData, }, - utils::Encode, }; #[cfg(all( any(feature = "v1", feature = "v2", feature = "olap", feature = "oltp"), @@ -416,6 +415,7 @@ pub async fn save_payment_method_api( .await } +#[cfg(feature = "v1")] #[instrument(skip_all, fields(flow = ?Flow::PaymentMethodsList))] pub async fn list_payment_method_api( state: web::Data, @@ -557,6 +557,7 @@ pub async fn list_customer_payment_method_for_payment( list_customer_payment_method_util( state, auth.merchant_account, + auth.profile, auth.key_store, Some(req), None, @@ -622,6 +623,7 @@ pub async fn list_customer_payment_method_api( list_customer_payment_method_util( state, auth.merchant_account, + auth.profile, auth.key_store, Some(req), Some(customer_id.clone()), @@ -900,6 +902,7 @@ pub async fn list_countries_currencies_for_connector_payment_method( .await } +#[cfg(feature = "v1")] #[instrument(skip_all, fields(flow = ?Flow::DefaultPaymentMethodsSet))] pub async fn default_payment_method_set_api( state: web::Data, @@ -936,6 +939,7 @@ pub async fn default_payment_method_set_api( )) .await } + #[cfg(test)] mod tests { #![allow(clippy::unwrap_used)] @@ -983,17 +987,13 @@ impl ParentPaymentMethodToken { token: PaymentTokenData, state: &SessionState, ) -> CustomResult<(), errors::ApiErrorResponse> { - let token_json_str = token - .encode_to_string_of_json() - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("failed to serialize hyperswitch token to json")?; let redis_conn = state .store .get_redis_conn() .change_context(errors::ApiErrorResponse::InternalServerError) .attach_printable("Failed to get redis connection")?; redis_conn - .set_key_with_expiry(&self.key_for_token, token_json_str, fulfillment_time) + .serialize_and_set_key_with_expiry(&self.key_for_token, token, fulfillment_time) .await .change_context(errors::StorageError::KVError) .change_context(errors::ApiErrorResponse::InternalServerError) diff --git a/crates/router/src/services/authentication.rs b/crates/router/src/services/authentication.rs index cbd694f3acdb..586252ff0ad7 100644 --- a/crates/router/src/services/authentication.rs +++ b/crates/router/src/services/authentication.rs @@ -2525,6 +2525,43 @@ pub fn get_auth_type_and_flow( Ok((Box::new(HeaderAuth(ApiKeyAuth)), api::AuthFlow::Merchant)) } +#[cfg(feature = "v1")] +pub fn check_client_secret_and_get_auth( + headers: &HeaderMap, + payload: &impl ClientSecretFetch, +) -> RouterResult<( + Box>, + api::AuthFlow, +)> +where + T: SessionStateInfo + Sync + Send, + ApiKeyAuth: AuthenticateAndFetch, + PublishableKeyAuth: AuthenticateAndFetch, +{ + let api_key = get_api_key(headers)?; + if api_key.starts_with("pk_") { + payload + .get_client_secret() + .check_value_present("client_secret") + .map_err(|_| errors::ApiErrorResponse::MissingRequiredField { + field_name: "client_secret", + })?; + return Ok(( + Box::new(HeaderAuth(PublishableKeyAuth)), + api::AuthFlow::Client, + )); + } + + if payload.get_client_secret().is_some() { + return Err(errors::ApiErrorResponse::InvalidRequestData { + message: "client_secret is not a valid parameter".to_owned(), + } + .into()); + } + Ok((Box::new(HeaderAuth(ApiKeyAuth)), api::AuthFlow::Merchant)) +} + +#[cfg(feature = "v2")] pub fn check_client_secret_and_get_auth( headers: &HeaderMap, payload: &impl ClientSecretFetch, @@ -2557,9 +2594,11 @@ where } .into()); } + Ok((Box::new(HeaderAuth(ApiKeyAuth)), api::AuthFlow::Merchant)) } +#[cfg(feature = "v1")] pub async fn get_ephemeral_or_other_auth( headers: &HeaderMap, is_merchant_flow: bool, @@ -2592,6 +2631,7 @@ where } } +#[cfg(feature = "v1")] pub fn is_ephemeral_auth( headers: &HeaderMap, ) -> RouterResult>> { @@ -2604,6 +2644,13 @@ pub fn is_ephemeral_auth( } } +#[cfg(feature = "v2")] +pub fn is_ephemeral_auth( + headers: &HeaderMap, +) -> RouterResult>> { + todo!() +} + pub fn is_jwt_auth(headers: &HeaderMap) -> bool { headers.get(headers::AUTHORIZATION).is_some() || get_cookie_from_header(headers) diff --git a/crates/router/src/types/api/admin.rs b/crates/router/src/types/api/admin.rs index 02951d1a1880..c81fc7ceb484 100644 --- a/crates/router/src/types/api/admin.rs +++ b/crates/router/src/types/api/admin.rs @@ -233,6 +233,7 @@ impl ForeignTryFrom for ProfileResponse { outgoing_webhook_custom_http_headers, order_fulfillment_time, order_fulfillment_time_origin: item.order_fulfillment_time_origin, + should_collect_cvv_during_payment: item.should_collect_cvv_during_payment, tax_connector_id: item.tax_connector_id, is_tax_connector_enabled: item.is_tax_connector_enabled, is_network_tokenization_enabled: item.is_network_tokenization_enabled, diff --git a/crates/router/src/types/payment_methods.rs b/crates/router/src/types/payment_methods.rs index 29290f0b7fcf..1a6b9053dcb9 100644 --- a/crates/router/src/types/payment_methods.rs +++ b/crates/router/src/types/payment_methods.rs @@ -139,8 +139,8 @@ impl PaymentMethodClientSecret { #[cfg(all(feature = "v2", feature = "payment_methods_v2"))] pub struct SavedPMLPaymentsInfo { pub payment_intent: storage::PaymentIntent, - pub business_profile: Option, - pub requires_cvv: bool, + pub profile: domain::Profile, + pub collect_cvv_during_payment: bool, pub off_session_payment_flag: bool, pub is_connector_agnostic_mit_enabled: bool, } diff --git a/crates/router/src/types/storage/payment_method.rs b/crates/router/src/types/storage/payment_method.rs index 7739c0f0ba8f..3214f911f567 100644 --- a/crates/router/src/types/storage/payment_method.rs +++ b/crates/router/src/types/storage/payment_method.rs @@ -1,8 +1,3 @@ -use std::{ - collections::HashMap, - ops::{Deref, DerefMut}, -}; - use api_models::payment_methods; use diesel_models::enums; pub use diesel_models::payment_method::{ @@ -118,37 +113,6 @@ pub struct PaymentMethodListContext { pub bank_transfer_details: Option, } -#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] -pub struct PaymentsMandateReferenceRecord { - pub connector_mandate_id: String, - pub payment_method_type: Option, - pub original_payment_authorized_amount: Option, - pub original_payment_authorized_currency: Option, - pub mandate_metadata: Option, - pub connector_mandate_status: Option, - pub connector_mandate_request_reference_id: Option, -} - -#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] -pub struct PaymentsMandateReference( - pub HashMap, -); - -impl Deref for PaymentsMandateReference { - type Target = - HashMap; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -impl DerefMut for PaymentsMandateReference { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.0 - } -} - #[derive(Debug, serde::Deserialize, serde::Serialize, Clone)] pub struct PaymentMethodStatusTrackingData { pub payment_method_id: String, diff --git a/v2_migrations/2024-08-23-112510_payment_methods_v2_db_changes/down.sql b/v2_migrations/2024-08-23-112510_payment_methods_v2_db_changes/down.sql index cde77d754f88..ce485048f4ef 100644 --- a/v2_migrations/2024-08-23-112510_payment_methods_v2_db_changes/down.sql +++ b/v2_migrations/2024-08-23-112510_payment_methods_v2_db_changes/down.sql @@ -1,23 +1,16 @@ -- This file should undo anything in `up.sql` -ALTER TABLE payment_methods ADD COLUMN IF NOT EXISTS accepted_currency "Currency"[]; - -ALTER TABLE payment_methods ADD COLUMN IF NOT EXISTS scheme VARCHAR(32); - -ALTER TABLE payment_methods ADD COLUMN IF NOT EXISTS token VARCHAR(128); - -ALTER TABLE payment_methods ADD COLUMN IF NOT EXISTS cardholder_name VARCHAR(255); - -ALTER TABLE payment_methods ADD COLUMN IF NOT EXISTS issuer_name VARCHAR(64); - -ALTER TABLE payment_methods ADD COLUMN IF NOT EXISTS issuer_country VARCHAR(64); - -ALTER TABLE payment_methods ADD COLUMN IF NOT EXISTS is_stored BOOLEAN; - -ALTER TABLE payment_methods ADD COLUMN IF NOT EXISTS direct_debit_token VARCHAR(128); - -ALTER TABLE payment_methods ADD COLUMN IF NOT EXISTS swift_code VARCHAR(32); - -ALTER TABLE payment_methods ADD COLUMN IF NOT EXISTS payment_method_issuer VARCHAR(128); +ALTER TABLE payment_methods + ADD COLUMN IF NOT EXISTS accepted_currency "Currency" [ ], + ADD COLUMN IF NOT EXISTS scheme VARCHAR(32), + ADD COLUMN IF NOT EXISTS token VARCHAR(128), + ADD COLUMN IF NOT EXISTS cardholder_name VARCHAR(255), + ADD COLUMN IF NOT EXISTS issuer_name VARCHAR(64), + ADD COLUMN IF NOT EXISTS issuer_country VARCHAR(64), + ADD COLUMN IF NOT EXISTS is_stored BOOLEAN, + ADD COLUMN IF NOT EXISTS direct_debit_token VARCHAR(128), + ADD COLUMN IF NOT EXISTS swift_code VARCHAR(32), + ADD COLUMN IF NOT EXISTS payment_method_issuer VARCHAR(128), + ADD COLUMN IF NOT EXISTS metadata JSON; CREATE TYPE "PaymentMethodIssuerCode" AS ENUM ( 'jp_hdfc', @@ -41,4 +34,4 @@ UPDATE payment_methods SET payment_method_id = id; ALTER TABLE payment_methods DROP CONSTRAINT IF EXISTS payment_methods_pkey; ALTER TABLE payment_methods ADD CONSTRAINT payment_methods_pkey PRIMARY KEY (payment_method_id); ALTER TABLE payment_methods DROP COLUMN IF EXISTS id; -ALTER TABLE payment_methods ADD COLUMN IF NOT EXISTS id SERIAL; \ No newline at end of file +ALTER TABLE payment_methods ADD COLUMN IF NOT EXISTS id SERIAL; diff --git a/v2_migrations/2024-08-23-112510_payment_methods_v2_db_changes/up.sql b/v2_migrations/2024-08-23-112510_payment_methods_v2_db_changes/up.sql index 151cf068cff2..23a274b68444 100644 --- a/v2_migrations/2024-08-23-112510_payment_methods_v2_db_changes/up.sql +++ b/v2_migrations/2024-08-23-112510_payment_methods_v2_db_changes/up.sql @@ -1,27 +1,18 @@ -- Your SQL goes here -ALTER TABLE payment_methods DROP COLUMN IF EXISTS accepted_currency; - -ALTER TABLE payment_methods DROP COLUMN IF EXISTS scheme; - -ALTER TABLE payment_methods DROP COLUMN IF EXISTS token; - -ALTER TABLE payment_methods DROP COLUMN IF EXISTS cardholder_name; - -ALTER TABLE payment_methods DROP COLUMN IF EXISTS issuer_name; - -ALTER TABLE payment_methods DROP COLUMN IF EXISTS issuer_country; - -ALTER TABLE payment_methods DROP COLUMN IF EXISTS payer_country; - -ALTER TABLE payment_methods DROP COLUMN IF EXISTS is_stored; - -ALTER TABLE payment_methods DROP COLUMN IF EXISTS direct_debit_token; - -ALTER TABLE payment_methods DROP COLUMN IF EXISTS swift_code; - -ALTER TABLE payment_methods DROP COLUMN IF EXISTS payment_method_issuer; - -ALTER TABLE payment_methods DROP COLUMN IF EXISTS payment_method_issuer_code; +ALTER TABLE payment_methods + DROP COLUMN IF EXISTS accepted_currency, + DROP COLUMN IF EXISTS scheme, + DROP COLUMN IF EXISTS token, + DROP COLUMN IF EXISTS cardholder_name, + DROP COLUMN IF EXISTS issuer_name, + DROP COLUMN IF EXISTS issuer_country, + DROP COLUMN IF EXISTS payer_country, + DROP COLUMN IF EXISTS is_stored, + DROP COLUMN IF EXISTS direct_debit_token, + DROP COLUMN IF EXISTS swift_code, + DROP COLUMN IF EXISTS payment_method_issuer, + DROP COLUMN IF EXISTS payment_method_issuer_code, + DROP COLUMN IF EXISTS metadata; DROP TYPE IF EXISTS "PaymentMethodIssuerCode"; @@ -32,4 +23,4 @@ ALTER TABLE payment_methods ADD COLUMN IF NOT EXISTS id VARCHAR(64); UPDATE payment_methods SET id = payment_method_id; ALTER TABLE payment_methods DROP CONSTRAINT IF EXISTS payment_methods_pkey; ALTER TABLE payment_methods ADD CONSTRAINT payment_methods_pkey PRIMARY KEY (id); -ALTER TABLE payment_methods DROP COLUMN IF EXISTS payment_method_id; \ No newline at end of file +ALTER TABLE payment_methods DROP COLUMN IF EXISTS payment_method_id; diff --git a/v2_migrations/2024-08-28-081721_add_v2_columns/down.sql b/v2_migrations/2024-08-28-081721_add_v2_columns/down.sql index 4687c57fae31..70aa43951050 100644 --- a/v2_migrations/2024-08-28-081721_add_v2_columns/down.sql +++ b/v2_migrations/2024-08-28-081721_add_v2_columns/down.sql @@ -10,7 +10,8 @@ ALTER TABLE business_profile DROP COLUMN routing_algorithm_id, DROP COLUMN order_fulfillment_time_origin, DROP COLUMN frm_routing_algorithm_id, DROP COLUMN payout_routing_algorithm_id, - DROP COLUMN default_fallback_routing; + DROP COLUMN default_fallback_routing, + DROP COLUMN should_collect_cvv_during_payment; DROP TYPE "OrderFulfillmentTimeOrigin"; diff --git a/v2_migrations/2024-08-28-081721_add_v2_columns/up.sql b/v2_migrations/2024-08-28-081721_add_v2_columns/up.sql index 3852c9c2ece3..64decb1a0a87 100644 --- a/v2_migrations/2024-08-28-081721_add_v2_columns/up.sql +++ b/v2_migrations/2024-08-28-081721_add_v2_columns/up.sql @@ -14,7 +14,14 @@ ADD COLUMN routing_algorithm_id VARCHAR(64) DEFAULT NULL, ADD COLUMN order_fulfillment_time_origin "OrderFulfillmentTimeOrigin" DEFAULT NULL, ADD COLUMN frm_routing_algorithm_id VARCHAR(64) DEFAULT NULL, ADD COLUMN payout_routing_algorithm_id VARCHAR(64) DEFAULT NULL, - ADD COLUMN default_fallback_routing JSONB DEFAULT NULL; + ADD COLUMN default_fallback_routing JSONB DEFAULT NULL, + -- Intentionally not adding a default value here since we would have to + -- check if any merchants have enabled this from configs table, + -- before filling data for this column. + -- If no merchants have enabled this, then we can use `false` as the default value + -- when adding the column, later we can drop the default added for the column + -- so that we ensure new records inserted always have a value for the column. + ADD COLUMN should_collect_cvv_during_payment BOOLEAN NOT NULL; ALTER TABLE payment_intent ADD COLUMN merchant_reference_id VARCHAR(64), From 6f24bb4ee349683ea95cd5eb9d682d83c92a637d Mon Sep 17 00:00:00 2001 From: Sai Harsha Vardhan <56996463+sai-harsha-vardhan@users.noreply.github.com> Date: Fri, 8 Nov 2024 19:20:25 +0530 Subject: [PATCH 23/25] feat(router): add `start_redirection` api for three_ds flow in v2 (#6470) Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com> --- api-reference-v2/openapi_spec.json | 8 ++ crates/api_models/src/events/payment.rs | 13 +++- crates/api_models/src/payments.rs | 26 +++++++ crates/diesel_models/src/payment_attempt.rs | 2 +- crates/diesel_models/src/payment_intent.rs | 19 +++-- .../hyperswitch_domain_models/src/payments.rs | 24 +++++- .../src/payments/payment_attempt.rs | 5 ++ .../src/payments/payment_intent.rs | 30 +++++--- crates/router/src/core/payments.rs | 75 ++++++++++++++++++- .../operations/payment_confirm_intent.rs | 1 + .../payments/operations/payment_response.rs | 9 ++- .../router/src/core/payments/transformers.rs | 14 +++- crates/router/src/routes/app.rs | 4 + crates/router/src/routes/lock_utils.rs | 3 +- crates/router/src/routes/payments.rs | 50 +++++++++++++ crates/router/src/services/authentication.rs | 55 ++++++++++++++ crates/router_env/src/logger/types.rs | 2 + .../down.sql | 1 + .../up.sql | 3 +- 19 files changed, 316 insertions(+), 28 deletions(-) diff --git a/api-reference-v2/openapi_spec.json b/api-reference-v2/openapi_spec.json index e14963c80d48..84562b99343d 100644 --- a/api-reference-v2/openapi_spec.json +++ b/api-reference-v2/openapi_spec.json @@ -13383,6 +13383,14 @@ "payment_method_subtype": { "$ref": "#/components/schemas/PaymentMethodType" }, + "next_action": { + "allOf": [ + { + "$ref": "#/components/schemas/NextActionData" + } + ], + "nullable": true + }, "connector_transaction_id": { "type": "string", "description": "A unique identifier for a payment provided by the connector", diff --git a/crates/api_models/src/events/payment.rs b/crates/api_models/src/events/payment.rs index b9dc1476fdb8..ab09a05d0b1c 100644 --- a/crates/api_models/src/events/payment.rs +++ b/crates/api_models/src/events/payment.rs @@ -2,8 +2,8 @@ use common_utils::events::{ApiEventMetric, ApiEventsType}; #[cfg(feature = "v2")] use super::{ - PaymentsConfirmIntentResponse, PaymentsCreateIntentRequest, PaymentsGetIntentRequest, - PaymentsIntentResponse, + PaymentStartRedirectionRequest, PaymentsConfirmIntentResponse, PaymentsCreateIntentRequest, + PaymentsGetIntentRequest, PaymentsIntentResponse, }; #[cfg(all( any(feature = "v2", feature = "v1"), @@ -365,3 +365,12 @@ impl ApiEventMetric for PaymentsSessionResponse { }) } } + +#[cfg(feature = "v2")] +impl ApiEventMetric for PaymentStartRedirectionRequest { + fn get_api_event_type(&self) -> Option { + Some(ApiEventsType::Payment { + payment_id: self.id.clone(), + }) + } +} diff --git a/crates/api_models/src/payments.rs b/crates/api_models/src/payments.rs index 3e95fb9f2315..2494952e7ddc 100644 --- a/crates/api_models/src/payments.rs +++ b/crates/api_models/src/payments.rs @@ -3876,9 +3876,16 @@ pub enum NextActionType { #[serde(tag = "type", rename_all = "snake_case")] pub enum NextActionData { /// Contains the url for redirection flow + #[cfg(feature = "v1")] RedirectToUrl { redirect_to_url: String, }, + /// Contains the url for redirection flow + #[cfg(feature = "v2")] + RedirectToUrl { + #[schema(value_type = String)] + redirect_to_url: Url, + }, /// Informs the next steps for bank transfer and also contains the charges details (ex: amount received, amount charged etc) DisplayBankTransferInformation { bank_transfer_steps_and_charges_details: BankTransferNextStepsData, @@ -4538,6 +4545,9 @@ pub struct PaymentsConfirmIntentResponse { #[schema(value_type = PaymentMethodType, example = "apple_pay")] pub payment_method_subtype: api_enums::PaymentMethodType, + /// Additional information required for redirection + pub next_action: Option, + /// A unique identifier for a payment provided by the connector #[schema(value_type = Option, example = "993672945374576J")] pub connector_transaction_id: Option, @@ -4558,6 +4568,22 @@ pub struct PaymentsConfirmIntentResponse { pub error: Option, } +#[derive(Debug, serde::Deserialize, serde::Serialize, Clone)] +#[cfg(feature = "v2")] +pub struct PaymentStartRedirectionRequest { + /// Global Payment ID + pub id: id_type::GlobalPaymentId, +} + +#[derive(Debug, serde::Deserialize, serde::Serialize, Clone)] +#[cfg(feature = "v2")] +pub struct PaymentStartRedirectionParams { + /// The identifier for the Merchant Account. + pub publishable_key: String, + /// The identifier for business profile + pub profile_id: id_type::ProfileId, +} + /// Fee information to be charged on the payment being collected #[derive(Setter, Clone, Default, Debug, PartialEq, serde::Serialize, ToSchema)] pub struct PaymentChargeResponse { diff --git a/crates/diesel_models/src/payment_attempt.rs b/crates/diesel_models/src/payment_attempt.rs index f08847651e76..e233cfe390bb 100644 --- a/crates/diesel_models/src/payment_attempt.rs +++ b/crates/diesel_models/src/payment_attempt.rs @@ -770,7 +770,7 @@ pub struct PaymentAttemptUpdateInternal { pub updated_by: String, pub merchant_connector_id: Option, pub connector: Option, - // authentication_data: Option, + pub authentication_data: Option, // encoded_data: Option, pub unified_code: Option>, pub unified_message: Option>, diff --git a/crates/diesel_models/src/payment_intent.rs b/crates/diesel_models/src/payment_intent.rs index 883e23930977..65fa044478ec 100644 --- a/crates/diesel_models/src/payment_intent.rs +++ b/crates/diesel_models/src/payment_intent.rs @@ -33,7 +33,7 @@ pub struct PaymentIntent { pub last_synced: Option, pub setup_future_usage: Option, pub client_secret: common_utils::types::ClientSecret, - pub active_attempt_id: Option, + pub active_attempt_id: Option, #[diesel(deserialize_as = super::OptionalDieselArray>)] pub order_details: Option>>, pub allowed_payment_method_types: Option, @@ -250,7 +250,7 @@ pub struct PaymentIntentNew { pub last_synced: Option, pub setup_future_usage: Option, pub client_secret: common_utils::types::ClientSecret, - pub active_attempt_id: Option, + pub active_attempt_id: Option, #[diesel(deserialize_as = super::OptionalDieselArray>)] pub order_details: Option>>, pub allowed_payment_method_types: Option, @@ -359,6 +359,7 @@ pub enum PaymentIntentUpdate { ConfirmIntent { status: storage_enums::IntentStatus, updated_by: String, + active_attempt_id: common_utils::id_type::GlobalAttemptId, }, /// Update the payment intent details on payment intent confirmation, after calling the connector ConfirmIntentPostUpdate { @@ -520,7 +521,7 @@ pub struct PaymentIntentUpdateInternal { // pub setup_future_usage: Option, // pub metadata: Option, pub modified_at: PrimitiveDateTime, - // pub active_attempt_id: Option, + pub active_attempt_id: Option, // pub description: Option, // pub statement_descriptor: Option, // #[diesel(deserialize_as = super::OptionalDieselArray)] @@ -594,7 +595,7 @@ impl PaymentIntentUpdate { // setup_future_usage, // metadata, modified_at: _, - // active_attempt_id, + active_attempt_id, // description, // statement_descriptor, // order_details, @@ -620,7 +621,7 @@ impl PaymentIntentUpdate { // setup_future_usage: setup_future_usage.or(source.setup_future_usage), // metadata: metadata.or(source.metadata), modified_at: common_utils::date_time::now(), - // active_attempt_id: active_attempt_id.unwrap_or(source.active_attempt_id), + active_attempt_id: active_attempt_id.or(source.active_attempt_id), // description: description.or(source.description), // statement_descriptor: statement_descriptor.or(source.statement_descriptor), // order_details: order_details.or(source.order_details), @@ -735,15 +736,21 @@ impl PaymentIntentUpdate { impl From for PaymentIntentUpdateInternal { fn from(payment_intent_update: PaymentIntentUpdate) -> Self { match payment_intent_update { - PaymentIntentUpdate::ConfirmIntent { status, updated_by } => Self { + PaymentIntentUpdate::ConfirmIntent { + status, + updated_by, + active_attempt_id, + } => Self { status: Some(status), modified_at: common_utils::date_time::now(), updated_by, + active_attempt_id: Some(active_attempt_id), }, PaymentIntentUpdate::ConfirmIntentPostUpdate { status, updated_by } => Self { status: Some(status), modified_at: common_utils::date_time::now(), updated_by, + active_attempt_id: None, }, } } diff --git a/crates/hyperswitch_domain_models/src/payments.rs b/crates/hyperswitch_domain_models/src/payments.rs index a21788e1bb0a..c1c32e08f4e5 100644 --- a/crates/hyperswitch_domain_models/src/payments.rs +++ b/crates/hyperswitch_domain_models/src/payments.rs @@ -111,6 +111,24 @@ impl PaymentIntent { pub fn get_id(&self) -> &id_type::GlobalPaymentId { &self.id } + + #[cfg(feature = "v2")] + pub fn create_start_redirection_url( + &self, + base_url: &str, + publishable_key: String, + ) -> CustomResult { + let start_redirection_url = &format!( + "{}/v2/payments/{}/start_redirection?publishable_key={}&profile_id={}", + base_url, + self.get_id().get_string_repr(), + publishable_key, + self.profile_id.get_string_repr() + ); + url::Url::parse(start_redirection_url) + .change_context(errors::api_error_response::ApiErrorResponse::InternalServerError) + .attach_printable("Error creating start redirection url") + } } #[cfg(feature = "v2")] @@ -272,8 +290,8 @@ pub struct PaymentIntent { pub setup_future_usage: storage_enums::FutureUsage, /// The client secret that is generated for the payment. This is used to authenticate the payment from client facing apis. pub client_secret: common_utils::types::ClientSecret, - /// The active attempt for the payment intent. This is the payment attempt that is currently active for the payment intent. - pub active_attempt: Option>, + /// The active attempt id for the payment intent. This is the payment attempt that is currently active for the payment intent. + pub active_attempt_id: Option, /// The order details for the payment. pub order_details: Option>>, /// This is the list of payment method types that are allowed for the payment intent. @@ -421,7 +439,7 @@ impl PaymentIntent { last_synced: None, setup_future_usage: request.setup_future_usage.unwrap_or_default(), client_secret, - active_attempt: None, + active_attempt_id: None, order_details, allowed_payment_method_types, connector_metadata, diff --git a/crates/hyperswitch_domain_models/src/payments/payment_attempt.rs b/crates/hyperswitch_domain_models/src/payments/payment_attempt.rs index 831e97b54a17..a65abe1495e1 100644 --- a/crates/hyperswitch_domain_models/src/payments/payment_attempt.rs +++ b/crates/hyperswitch_domain_models/src/payments/payment_attempt.rs @@ -1296,6 +1296,7 @@ pub enum PaymentAttemptUpdate { status: storage_enums::AttemptStatus, connector_payment_id: Option, updated_by: String, + authentication_data: Option, }, /// Update the payment attempt on confirming the intent, after calling the connector on error response ConfirmIntentError { @@ -1923,6 +1924,7 @@ impl From for diesel_models::PaymentAttemptUpdateInternal unified_message: None, connector_payment_id: None, connector: Some(connector), + authentication_data: None, }, PaymentAttemptUpdate::ConfirmIntentError { status, @@ -1941,11 +1943,13 @@ impl From for diesel_models::PaymentAttemptUpdateInternal unified_message: None, connector_payment_id: None, connector: None, + authentication_data: None, }, PaymentAttemptUpdate::ConfirmIntentResponse { status, connector_payment_id, updated_by, + authentication_data, } => Self { status: Some(status), error_message: None, @@ -1959,6 +1963,7 @@ impl From for diesel_models::PaymentAttemptUpdateInternal unified_message: None, connector_payment_id, connector: None, + authentication_data, }, } } diff --git a/crates/hyperswitch_domain_models/src/payments/payment_intent.rs b/crates/hyperswitch_domain_models/src/payments/payment_intent.rs index 3b26ff94e547..4e3f861d76ef 100644 --- a/crates/hyperswitch_domain_models/src/payments/payment_intent.rs +++ b/crates/hyperswitch_domain_models/src/payments/payment_intent.rs @@ -274,6 +274,7 @@ pub enum PaymentIntentUpdate { ConfirmIntent { status: storage_enums::IntentStatus, updated_by: String, + active_attempt_id: id_type::GlobalAttemptId, }, ConfirmIntentPostUpdate { status: storage_enums::IntentStatus, @@ -363,9 +364,14 @@ pub struct PaymentIntentUpdateInternal { impl From for PaymentIntentUpdateInternal { fn from(payment_intent_update: PaymentIntentUpdate) -> Self { match payment_intent_update { - PaymentIntentUpdate::ConfirmIntent { status, updated_by } => Self { + PaymentIntentUpdate::ConfirmIntent { + status, + updated_by, + active_attempt_id, + } => Self { status: Some(status), updated_by, + active_attempt_id: Some(active_attempt_id), ..Default::default() }, PaymentIntentUpdate::ConfirmIntentPostUpdate { status, updated_by } => Self { @@ -582,9 +588,15 @@ use diesel_models::{ impl From for DieselPaymentIntentUpdate { fn from(value: PaymentIntentUpdate) -> Self { match value { - PaymentIntentUpdate::ConfirmIntent { status, updated_by } => { - Self::ConfirmIntent { status, updated_by } - } + PaymentIntentUpdate::ConfirmIntent { + status, + updated_by, + active_attempt_id, + } => Self::ConfirmIntent { + status, + updated_by, + active_attempt_id, + }, PaymentIntentUpdate::ConfirmIntentPostUpdate { status, updated_by } => { Self::ConfirmIntentPostUpdate { status, updated_by } } @@ -1134,7 +1146,7 @@ impl behaviour::Conversion for PaymentIntent { last_synced, setup_future_usage, client_secret, - active_attempt, + active_attempt_id, order_details, allowed_payment_method_types, connector_metadata, @@ -1182,7 +1194,7 @@ impl behaviour::Conversion for PaymentIntent { last_synced, setup_future_usage: Some(setup_future_usage), client_secret, - active_attempt_id: active_attempt.map(|attempt| attempt.get_id()), + active_attempt_id, order_details: order_details.map(|order_details| { order_details .into_iter() @@ -1319,9 +1331,7 @@ impl behaviour::Conversion for PaymentIntent { last_synced: storage_model.last_synced, setup_future_usage: storage_model.setup_future_usage.unwrap_or_default(), client_secret: storage_model.client_secret, - active_attempt: storage_model - .active_attempt_id - .map(RemoteStorageObject::ForeignID), + active_attempt_id: storage_model.active_attempt_id, order_details: storage_model.order_details.map(|order_details| { order_details .into_iter() @@ -1395,7 +1405,7 @@ impl behaviour::Conversion for PaymentIntent { last_synced: self.last_synced, setup_future_usage: Some(self.setup_future_usage), client_secret: self.client_secret, - active_attempt_id: self.active_attempt.map(|attempt| attempt.get_id()), + active_attempt_id: self.active_attempt_id, order_details: self.order_details, allowed_payment_method_types: self .allowed_payment_method_types diff --git a/crates/router/src/core/payments.rs b/crates/router/src/core/payments.rs index a025b4b3cc85..ebf49daa809d 100644 --- a/crates/router/src/core/payments.rs +++ b/crates/router/src/core/payments.rs @@ -37,6 +37,8 @@ use futures::future::join_all; use helpers::{decrypt_paze_token, ApplePayData}; #[cfg(feature = "v2")] use hyperswitch_domain_models::payments::{PaymentConfirmData, PaymentIntentData}; +#[cfg(feature = "v2")] +use hyperswitch_domain_models::router_response_types::RedirectForm; pub use hyperswitch_domain_models::{ mandates::{CustomerAcceptance, MandateData}, payment_address::PaymentAddress, @@ -1463,7 +1465,7 @@ where let (payment_data, _req, customer) = payments_intent_operation_core::<_, _, _, _>( &state, req_state, - merchant_account, + merchant_account.clone(), profile, key_store, operation.clone(), @@ -1481,6 +1483,7 @@ where None, None, header_payload.x_hs_latency, + &merchant_account, ) } @@ -1521,7 +1524,7 @@ where payments_operation_core::<_, _, _, _, _>( &state, req_state, - merchant_account, + merchant_account.clone(), key_store, profile, operation.clone(), @@ -1541,6 +1544,7 @@ where connector_http_status_code, external_latency, header_payload.x_hs_latency, + &merchant_account, ) } @@ -5966,6 +5970,73 @@ pub async fn payment_external_authentication( )) } +#[instrument(skip_all)] +#[cfg(feature = "v2")] +pub async fn payment_start_redirection( + state: SessionState, + merchant_account: domain::MerchantAccount, + key_store: domain::MerchantKeyStore, + req: api_models::payments::PaymentStartRedirectionRequest, +) -> RouterResponse { + let db = &*state.store; + let key_manager_state = &(&state).into(); + + let storage_scheme = merchant_account.storage_scheme; + + let payment_intent = db + .find_payment_intent_by_id(key_manager_state, &req.id, &key_store, storage_scheme) + .await + .to_not_found_response(errors::ApiErrorResponse::PaymentNotFound)?; + + //TODO: send valid html error pages in this case, or atleast redirect to valid html error pages + utils::when( + payment_intent.status != storage_enums::IntentStatus::RequiresCustomerAction, + || { + Err(errors::ApiErrorResponse::PaymentUnexpectedState { + current_flow: "PaymentStartRedirection".to_string(), + field_name: "status".to_string(), + current_value: payment_intent.status.to_string(), + states: ["requires_customer_action".to_string()].join(", "), + }) + }, + )?; + + let payment_attempt = db + .find_payment_attempt_by_id( + key_manager_state, + &key_store, + payment_intent + .active_attempt_id + .clone() + .ok_or(errors::ApiErrorResponse::InternalServerError) + .attach_printable("missing active attempt in payment_intent")? + .get_string_repr(), + storage_scheme, + ) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Error while fetching payment_attempt")?; + let redirection_data = payment_attempt + .authentication_data + .clone() + .ok_or(errors::ApiErrorResponse::InternalServerError) + .attach_printable("missing authentication_data in payment_attempt")?; + + let form: RedirectForm = serde_json::from_value(redirection_data.expose()).map_err(|err| { + logger::error!(error = ?err, "Failed to deserialize redirection data"); + errors::ApiErrorResponse::InternalServerError + })?; + + Ok(services::ApplicationResponse::Form(Box::new( + services::RedirectionFormData { + redirect_form: form, + payment_method_data: None, + amount: payment_attempt.amount_details.net_amount.to_string(), + currency: payment_intent.amount_details.currency.to_string(), + }, + ))) +} + #[instrument(skip_all)] pub async fn get_extended_card_info( state: SessionState, diff --git a/crates/router/src/core/payments/operations/payment_confirm_intent.rs b/crates/router/src/core/payments/operations/payment_confirm_intent.rs index 083bb5adca28..4c61becdf4a9 100644 --- a/crates/router/src/core/payments/operations/payment_confirm_intent.rs +++ b/crates/router/src/core/payments/operations/payment_confirm_intent.rs @@ -328,6 +328,7 @@ impl UpdateTracker, PaymentsConfirmIntentRequ hyperswitch_domain_models::payments::payment_intent::PaymentIntentUpdate::ConfirmIntent { status: intent_status, updated_by: storage_scheme.to_string(), + active_attempt_id: payment_data.payment_attempt.get_id().clone(), }; let payment_attempt_update = hyperswitch_domain_models::payments::payment_attempt::PaymentAttemptUpdate::ConfirmIntent { diff --git a/crates/router/src/core/payments/operations/payment_response.rs b/crates/router/src/core/payments/operations/payment_response.rs index 4b784ea9463b..427a406dbf59 100644 --- a/crates/router/src/core/payments/operations/payment_response.rs +++ b/crates/router/src/core/payments/operations/payment_response.rs @@ -2245,6 +2245,13 @@ impl PostUpdateTracker, types::PaymentsAuthor types::ResponseId::ConnectorTransactionId(id) | types::ResponseId::EncodedData(id) => Some(id), }; + let authentication_data = (*redirection_data) + .as_ref() + .map(Encode::encode_to_value) + .transpose() + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Could not parse the connector response")? + .map(masking::Secret::new); let payment_intent_update = hyperswitch_domain_models::payments::payment_intent::PaymentIntentUpdate::ConfirmIntentPostUpdate { status: intent_status, updated_by: storage_scheme.to_string() }; let updated_payment_intent = db @@ -2260,7 +2267,7 @@ impl PostUpdateTracker, types::PaymentsAuthor .attach_printable("Unable to update payment intent")?; payment_data.payment_intent = updated_payment_intent; - let payment_attempt_update = hyperswitch_domain_models::payments::payment_attempt::PaymentAttemptUpdate::ConfirmIntentResponse { status: attempt_status, connector_payment_id, updated_by: storage_scheme.to_string() }; + let payment_attempt_update = hyperswitch_domain_models::payments::payment_attempt::PaymentAttemptUpdate::ConfirmIntentResponse { status: attempt_status, connector_payment_id, updated_by: storage_scheme.to_string(), authentication_data }; let updated_payment_attempt = db .update_payment_attempt( key_manager_state, diff --git a/crates/router/src/core/payments/transformers.rs b/crates/router/src/core/payments/transformers.rs index 45d0f75ccec8..d37b249cd8a9 100644 --- a/crates/router/src/core/payments/transformers.rs +++ b/crates/router/src/core/payments/transformers.rs @@ -628,6 +628,7 @@ where connector_http_status_code: Option, external_latency: Option, is_latency_header_enabled: Option, + merchant_account: &domain::MerchantAccount, ) -> RouterResponse; } @@ -793,6 +794,7 @@ where _connector_http_status_code: Option, _external_latency: Option, _is_latency_header_enabled: Option, + _merchant_account: &domain::MerchantAccount, ) -> RouterResponse { let payment_intent = payment_data.get_payment_intent(); Ok(services::ApplicationResponse::JsonWithHeaders(( @@ -862,12 +864,13 @@ where fn generate_response( payment_data: D, _customer: Option, - _base_url: &str, + base_url: &str, operation: Op, _connector_request_reference_id_config: &ConnectorRequestReferenceIdConfig, _connector_http_status_code: Option, _external_latency: Option, _is_latency_header_enabled: Option, + merchant_account: &domain::MerchantAccount, ) -> RouterResponse { let payment_intent = payment_data.get_payment_intent(); let payment_attempt = payment_data.get_payment_attempt(); @@ -896,6 +899,14 @@ where .clone() .map(api_models::payments::ErrorDetails::foreign_from); + // TODO: Add support for other next actions, currently only supporting redirect to url + let redirect_to_url = payment_intent + .create_start_redirection_url(base_url, merchant_account.publishable_key.clone())?; + let next_action = payment_attempt + .authentication_data + .as_ref() + .map(|_| api_models::payments::NextActionData::RedirectToUrl { redirect_to_url }); + let response = Self { id: payment_intent.id.clone(), status: payment_intent.status, @@ -906,6 +917,7 @@ where payment_method_data: None, payment_method_type: payment_attempt.payment_method_type, payment_method_subtype: payment_attempt.payment_method_subtype, + next_action, connector_transaction_id: payment_attempt.connector_payment_id.clone(), connector_reference_id: None, merchant_connector_id, diff --git a/crates/router/src/routes/app.rs b/crates/router/src/routes/app.rs index 5ad8f3864418..9f0a83a053f6 100644 --- a/crates/router/src/routes/app.rs +++ b/crates/router/src/routes/app.rs @@ -535,6 +535,10 @@ impl Payments { .service( web::resource("/create-external-sdk-tokens") .route(web::post().to(payments::payments_connector_session)), + ) + .service( + web::resource("/start_redirection") + .route(web::get().to(payments::payments_start_redirection)), ), ); diff --git a/crates/router/src/routes/lock_utils.rs b/crates/router/src/routes/lock_utils.rs index 43ddc35b8ae6..4d3718b967dd 100644 --- a/crates/router/src/routes/lock_utils.rs +++ b/crates/router/src/routes/lock_utils.rs @@ -140,7 +140,8 @@ impl From for ApiIdentifier { | Flow::PaymentsConfirmIntent | Flow::PaymentsCreateIntent | Flow::PaymentsGetIntent - | Flow::PaymentsPostSessionTokens => Self::Payments, + | Flow::PaymentsPostSessionTokens + | Flow::PaymentStartRedirection => Self::Payments, Flow::PayoutsCreate | Flow::PayoutsRetrieve diff --git a/crates/router/src/routes/payments.rs b/crates/router/src/routes/payments.rs index ba785fbee8e6..c20f487a28e8 100644 --- a/crates/router/src/routes/payments.rs +++ b/crates/router/src/routes/payments.rs @@ -2057,6 +2057,56 @@ mod internal_payload_types { } } +#[cfg(feature = "v2")] +#[instrument(skip_all, fields(flow = ?Flow::PaymentStartRedirection, payment_id))] +pub async fn payments_start_redirection( + state: web::Data, + req: actix_web::HttpRequest, + payload: web::Query, + path: web::Path, +) -> impl Responder { + let flow = Flow::PaymentStartRedirection; + + let global_payment_id = path.into_inner(); + tracing::Span::current().record("payment_id", global_payment_id.get_string_repr()); + + let publishable_key = &payload.publishable_key; + let profile_id = &payload.profile_id; + + let payment_start_redirection_request = api_models::payments::PaymentStartRedirectionRequest { + id: global_payment_id.clone(), + }; + + let internal_payload = internal_payload_types::PaymentsGenericRequestWithResourceId { + global_payment_id: global_payment_id.clone(), + payload: payment_start_redirection_request.clone(), + }; + + let locking_action = internal_payload.get_locking_input(flow.clone()); + + Box::pin(api::server_wrap( + flow, + state, + &req, + payment_start_redirection_request.clone(), + |state, auth: auth::AuthenticationData, _req, req_state| async { + payments::payment_start_redirection( + state, + auth.merchant_account, + auth.key_store, + payment_start_redirection_request.clone(), + ) + .await + }, + &auth::PublishableKeyAndProfileIdAuth { + publishable_key: publishable_key.clone(), + profile_id: profile_id.clone(), + }, + locking_action, + )) + .await +} + #[cfg(feature = "v2")] #[instrument(skip_all, fields(flow = ?Flow::PaymentsConfirmIntent, payment_id))] pub async fn payment_confirm_intent( diff --git a/crates/router/src/services/authentication.rs b/crates/router/src/services/authentication.rs index 586252ff0ad7..70974f0ae6f6 100644 --- a/crates/router/src/services/authentication.rs +++ b/crates/router/src/services/authentication.rs @@ -1288,6 +1288,61 @@ where } } +#[derive(Debug)] +#[cfg(feature = "v2")] +pub struct PublishableKeyAndProfileIdAuth { + pub publishable_key: String, + pub profile_id: id_type::ProfileId, +} + +#[async_trait] +#[cfg(feature = "v2")] +impl AuthenticateAndFetch for PublishableKeyAndProfileIdAuth +where + A: SessionStateInfo + Sync, +{ + async fn authenticate_and_fetch( + &self, + _request_headers: &HeaderMap, + state: &A, + ) -> RouterResult<(AuthenticationData, AuthenticationType)> { + let key_manager_state = &(&state.session_state()).into(); + let (merchant_account, key_store) = state + .store() + .find_merchant_account_by_publishable_key( + key_manager_state, + self.publishable_key.as_str(), + ) + .await + .map_err(|e| { + if e.current_context().is_db_not_found() { + e.change_context(errors::ApiErrorResponse::Unauthorized) + } else { + e.change_context(errors::ApiErrorResponse::InternalServerError) + } + })?; + + let profile = state + .store() + .find_business_profile_by_profile_id(key_manager_state, &key_store, &self.profile_id) + .await + .to_not_found_response(errors::ApiErrorResponse::ProfileNotFound { + id: self.profile_id.get_string_repr().to_owned(), + })?; + + let merchant_id = merchant_account.get_id().clone(); + + Ok(( + AuthenticationData { + merchant_account, + key_store, + profile, + }, + AuthenticationType::PublishableKey { merchant_id }, + )) + } +} + #[derive(Debug)] pub struct PublishableKeyAuth; diff --git a/crates/router_env/src/logger/types.rs b/crates/router_env/src/logger/types.rs index 2410be9750cb..5d59e7ddbac4 100644 --- a/crates/router_env/src/logger/types.rs +++ b/crates/router_env/src/logger/types.rs @@ -512,6 +512,8 @@ pub enum Flow { PaymentsConfirmIntent, /// Payments post session tokens flow PaymentsPostSessionTokens, + /// Payments start redirection flow + PaymentStartRedirection, } /// diff --git a/v2_migrations/2024-08-28-081838_update_v2_primary_key_constraints/down.sql b/v2_migrations/2024-08-28-081838_update_v2_primary_key_constraints/down.sql index 359d5ce6359c..d0bc53f8ca07 100644 --- a/v2_migrations/2024-08-28-081838_update_v2_primary_key_constraints/down.sql +++ b/v2_migrations/2024-08-28-081838_update_v2_primary_key_constraints/down.sql @@ -87,6 +87,7 @@ ALTER COLUMN currency DROP NOT NULL, ALTER COLUMN client_secret DROP NOT NULL, ALTER COLUMN profile_id DROP NOT NULL; ALTER TABLE payment_intent ALTER COLUMN active_attempt_id SET NOT NULL; +ALTER TABLE payment_intent ALTER COLUMN active_attempt_id SET DEFAULT 'xxx'; ALTER TABLE payment_intent ALTER COLUMN session_expiry DROP NOT NULL; ------------------------ Payment Attempt ----------------------- diff --git a/v2_migrations/2024-08-28-081838_update_v2_primary_key_constraints/up.sql b/v2_migrations/2024-08-28-081838_update_v2_primary_key_constraints/up.sql index d4a1e7879b6b..8e72e06b66ab 100644 --- a/v2_migrations/2024-08-28-081838_update_v2_primary_key_constraints/up.sql +++ b/v2_migrations/2024-08-28-081838_update_v2_primary_key_constraints/up.sql @@ -130,4 +130,5 @@ ALTER TABLE payment_intent ALTER COLUMN session_expiry SET NOT NULL; -- This migration is to make fields optional in payment_intent table ALTER TABLE payment_intent ALTER COLUMN active_attempt_id DROP NOT NULL; - +ALTER TABLE payment_intent +ALTER COLUMN active_attempt_id DROP DEFAULT; From 5af532a1212ee0bf91bd485b0c761e38127bb76e Mon Sep 17 00:00:00 2001 From: Shankar Singh C <83439957+ShankarSinghC@users.noreply.github.com> Date: Fri, 8 Nov 2024 20:07:24 +0530 Subject: [PATCH 24/25] fix(router): add card expiry check in the `network_transaction_id_and_card_details` based `MIT` flow (#6504) --- crates/router/src/core/payments.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/crates/router/src/core/payments.rs b/crates/router/src/core/payments.rs index ebf49daa809d..61572763af2b 100644 --- a/crates/router/src/core/payments.rs +++ b/crates/router/src/core/payments.rs @@ -4470,6 +4470,11 @@ where let (mandate_reference_id, card_details_for_network_transaction_id)= hyperswitch_domain_models::payment_method_data::CardDetailsForNetworkTransactionId::get_nti_and_card_details_for_mit_flow(recurring_payment_details.clone()).get_required_value("network transaction id and card details").attach_printable("Failed to fetch network transaction id and card details for mit")?; + helpers::validate_card_expiry( + &card_details_for_network_transaction_id.card_exp_month, + &card_details_for_network_transaction_id.card_exp_year, + )?; + let network_transaction_id_supported_connectors = &state .conf .network_transaction_id_supported_connectors From ce95b6538dca4515b04ac65c2b1063bdd0a9c3a7 Mon Sep 17 00:00:00 2001 From: Sandeep Kumar <83278309+tsdk02@users.noreply.github.com> Date: Fri, 8 Nov 2024 21:03:57 +0530 Subject: [PATCH 25/25] feat(analytics): revert remove additional filters from PaymentIntentFilters (#6492) --- crates/analytics/src/payment_intents/core.rs | 9 ++ .../analytics/src/payment_intents/filters.rs | 11 ++- .../analytics/src/payment_intents/metrics.rs | 9 ++ .../metrics/payment_intent_count.rs | 9 ++ .../metrics/payment_processed_amount.rs | 9 ++ .../metrics/payments_success_rate.rs | 9 ++ .../payment_intent_count.rs | 9 ++ .../payment_processed_amount.rs | 9 ++ .../payments_distribution.rs | 9 ++ .../payments_success_rate.rs | 9 ++ .../smart_retried_amount.rs | 9 ++ .../successful_smart_retries.rs | 9 ++ .../total_smart_retries.rs | 9 ++ .../metrics/smart_retried_amount.rs | 9 ++ .../metrics/successful_smart_retries.rs | 9 ++ .../metrics/total_smart_retries.rs | 9 ++ crates/analytics/src/payment_intents/types.rs | 57 +++++++++++ crates/analytics/src/sqlx.rs | 96 +++++++++++++++++++ crates/analytics/src/utils.rs | 6 ++ .../src/analytics/payment_intents.rs | 67 ++++++++++++- 20 files changed, 370 insertions(+), 2 deletions(-) diff --git a/crates/analytics/src/payment_intents/core.rs b/crates/analytics/src/payment_intents/core.rs index 7ea8e9007f7b..64ca7c3f82b4 100644 --- a/crates/analytics/src/payment_intents/core.rs +++ b/crates/analytics/src/payment_intents/core.rs @@ -462,6 +462,15 @@ pub async fn get_filters( PaymentIntentDimensions::PaymentIntentStatus => fil.status.map(|i| i.as_ref().to_string()), PaymentIntentDimensions::Currency => fil.currency.map(|i| i.as_ref().to_string()), PaymentIntentDimensions::ProfileId => fil.profile_id, + PaymentIntentDimensions::Connector => fil.connector, + PaymentIntentDimensions::AuthType => fil.authentication_type.map(|i| i.as_ref().to_string()), + PaymentIntentDimensions::PaymentMethod => fil.payment_method, + PaymentIntentDimensions::PaymentMethodType => fil.payment_method_type, + PaymentIntentDimensions::CardNetwork => fil.card_network, + PaymentIntentDimensions::MerchantId => fil.merchant_id, + PaymentIntentDimensions::CardLast4 => fil.card_last_4, + PaymentIntentDimensions::CardIssuer => fil.card_issuer, + PaymentIntentDimensions::ErrorReason => fil.error_reason, }) .collect::>(); res.query_data.push(PaymentIntentFilterValue { diff --git a/crates/analytics/src/payment_intents/filters.rs b/crates/analytics/src/payment_intents/filters.rs index d03d6c2a15f9..1468a67570aa 100644 --- a/crates/analytics/src/payment_intents/filters.rs +++ b/crates/analytics/src/payment_intents/filters.rs @@ -1,6 +1,6 @@ use api_models::analytics::{payment_intents::PaymentIntentDimensions, Granularity, TimeRange}; use common_utils::errors::ReportSwitchExt; -use diesel_models::enums::{Currency, IntentStatus}; +use diesel_models::enums::{AuthenticationType, Currency, IntentStatus}; use error_stack::ResultExt; use time::PrimitiveDateTime; @@ -54,5 +54,14 @@ pub struct PaymentIntentFilterRow { pub status: Option>, pub currency: Option>, pub profile_id: Option, + pub connector: Option, + pub authentication_type: Option>, + pub payment_method: Option, + pub payment_method_type: Option, + pub card_network: Option, + pub merchant_id: Option, + pub card_last_4: Option, + pub card_issuer: Option, + pub error_reason: Option, pub customer_id: Option, } diff --git a/crates/analytics/src/payment_intents/metrics.rs b/crates/analytics/src/payment_intents/metrics.rs index 9aa7d3e97719..ee3d4773e243 100644 --- a/crates/analytics/src/payment_intents/metrics.rs +++ b/crates/analytics/src/payment_intents/metrics.rs @@ -36,6 +36,15 @@ pub struct PaymentIntentMetricRow { pub status: Option>, pub currency: Option>, pub profile_id: Option, + pub connector: Option, + pub authentication_type: Option>, + pub payment_method: Option, + pub payment_method_type: Option, + pub card_network: Option, + pub merchant_id: Option, + pub card_last_4: Option, + pub card_issuer: Option, + pub error_reason: Option, pub first_attempt: Option, pub total: Option, pub count: Option, diff --git a/crates/analytics/src/payment_intents/metrics/payment_intent_count.rs b/crates/analytics/src/payment_intents/metrics/payment_intent_count.rs index 4632cbe9f370..b301a9b9b23b 100644 --- a/crates/analytics/src/payment_intents/metrics/payment_intent_count.rs +++ b/crates/analytics/src/payment_intents/metrics/payment_intent_count.rs @@ -101,6 +101,15 @@ where i.status.as_ref().map(|i| i.0), i.currency.as_ref().map(|i| i.0), i.profile_id.clone(), + i.connector.clone(), + i.authentication_type.as_ref().map(|i| i.0), + i.payment_method.clone(), + i.payment_method_type.clone(), + i.card_network.clone(), + i.merchant_id.clone(), + i.card_last_4.clone(), + i.card_issuer.clone(), + i.error_reason.clone(), TimeRange { start_time: match (granularity, i.start_bucket) { (Some(g), Some(st)) => g.clip_to_start(st)?, diff --git a/crates/analytics/src/payment_intents/metrics/payment_processed_amount.rs b/crates/analytics/src/payment_intents/metrics/payment_processed_amount.rs index 51b574f4ad38..cf733b0c3da9 100644 --- a/crates/analytics/src/payment_intents/metrics/payment_processed_amount.rs +++ b/crates/analytics/src/payment_intents/metrics/payment_processed_amount.rs @@ -138,6 +138,15 @@ where None, i.currency.as_ref().map(|i| i.0), i.profile_id.clone(), + i.connector.clone(), + i.authentication_type.as_ref().map(|i| i.0), + i.payment_method.clone(), + i.payment_method_type.clone(), + i.card_network.clone(), + i.merchant_id.clone(), + i.card_last_4.clone(), + i.card_issuer.clone(), + i.error_reason.clone(), TimeRange { start_time: match (granularity, i.start_bucket) { (Some(g), Some(st)) => g.clip_to_start(st)?, diff --git a/crates/analytics/src/payment_intents/metrics/payments_success_rate.rs b/crates/analytics/src/payment_intents/metrics/payments_success_rate.rs index 14e168b3523d..07b1bfcf69fe 100644 --- a/crates/analytics/src/payment_intents/metrics/payments_success_rate.rs +++ b/crates/analytics/src/payment_intents/metrics/payments_success_rate.rs @@ -114,6 +114,15 @@ where None, i.currency.as_ref().map(|i| i.0), i.profile_id.clone(), + i.connector.clone(), + i.authentication_type.as_ref().map(|i| i.0), + i.payment_method.clone(), + i.payment_method_type.clone(), + i.card_network.clone(), + i.merchant_id.clone(), + i.card_last_4.clone(), + i.card_issuer.clone(), + i.error_reason.clone(), TimeRange { start_time: match (granularity, i.start_bucket) { (Some(g), Some(st)) => g.clip_to_start(st)?, diff --git a/crates/analytics/src/payment_intents/metrics/sessionized_metrics/payment_intent_count.rs b/crates/analytics/src/payment_intents/metrics/sessionized_metrics/payment_intent_count.rs index 644bf35a7236..7475a75bb532 100644 --- a/crates/analytics/src/payment_intents/metrics/sessionized_metrics/payment_intent_count.rs +++ b/crates/analytics/src/payment_intents/metrics/sessionized_metrics/payment_intent_count.rs @@ -101,6 +101,15 @@ where i.status.as_ref().map(|i| i.0), i.currency.as_ref().map(|i| i.0), i.profile_id.clone(), + i.connector.clone(), + i.authentication_type.as_ref().map(|i| i.0), + i.payment_method.clone(), + i.payment_method_type.clone(), + i.card_network.clone(), + i.merchant_id.clone(), + i.card_last_4.clone(), + i.card_issuer.clone(), + i.error_reason.clone(), TimeRange { start_time: match (granularity, i.start_bucket) { (Some(g), Some(st)) => g.clip_to_start(st)?, diff --git a/crates/analytics/src/payment_intents/metrics/sessionized_metrics/payment_processed_amount.rs b/crates/analytics/src/payment_intents/metrics/sessionized_metrics/payment_processed_amount.rs index 01d580534834..2ba75ca8519b 100644 --- a/crates/analytics/src/payment_intents/metrics/sessionized_metrics/payment_processed_amount.rs +++ b/crates/analytics/src/payment_intents/metrics/sessionized_metrics/payment_processed_amount.rs @@ -131,6 +131,15 @@ where None, i.currency.as_ref().map(|i| i.0), i.profile_id.clone(), + i.connector.clone(), + i.authentication_type.as_ref().map(|i| i.0), + i.payment_method.clone(), + i.payment_method_type.clone(), + i.card_network.clone(), + i.merchant_id.clone(), + i.card_last_4.clone(), + i.card_issuer.clone(), + i.error_reason.clone(), TimeRange { start_time: match (granularity, i.start_bucket) { (Some(g), Some(st)) => g.clip_to_start(st)?, diff --git a/crates/analytics/src/payment_intents/metrics/sessionized_metrics/payments_distribution.rs b/crates/analytics/src/payment_intents/metrics/sessionized_metrics/payments_distribution.rs index eed6bf85a2c4..0b55c101a7c9 100644 --- a/crates/analytics/src/payment_intents/metrics/sessionized_metrics/payments_distribution.rs +++ b/crates/analytics/src/payment_intents/metrics/sessionized_metrics/payments_distribution.rs @@ -113,6 +113,15 @@ where None, i.currency.as_ref().map(|i| i.0), i.profile_id.clone(), + i.connector.clone(), + i.authentication_type.as_ref().map(|i| i.0), + i.payment_method.clone(), + i.payment_method_type.clone(), + i.card_network.clone(), + i.merchant_id.clone(), + i.card_last_4.clone(), + i.card_issuer.clone(), + i.error_reason.clone(), TimeRange { start_time: match (granularity, i.start_bucket) { (Some(g), Some(st)) => g.clip_to_start(st)?, diff --git a/crates/analytics/src/payment_intents/metrics/sessionized_metrics/payments_success_rate.rs b/crates/analytics/src/payment_intents/metrics/sessionized_metrics/payments_success_rate.rs index bd1f8bbbcd95..8c340d0b2d6e 100644 --- a/crates/analytics/src/payment_intents/metrics/sessionized_metrics/payments_success_rate.rs +++ b/crates/analytics/src/payment_intents/metrics/sessionized_metrics/payments_success_rate.rs @@ -114,6 +114,15 @@ where None, i.currency.as_ref().map(|i| i.0), i.profile_id.clone(), + i.connector.clone(), + i.authentication_type.as_ref().map(|i| i.0), + i.payment_method.clone(), + i.payment_method_type.clone(), + i.card_network.clone(), + i.merchant_id.clone(), + i.card_last_4.clone(), + i.card_issuer.clone(), + i.error_reason.clone(), TimeRange { start_time: match (granularity, i.start_bucket) { (Some(g), Some(st)) => g.clip_to_start(st)?, diff --git a/crates/analytics/src/payment_intents/metrics/sessionized_metrics/smart_retried_amount.rs b/crates/analytics/src/payment_intents/metrics/sessionized_metrics/smart_retried_amount.rs index cf7af6e11e7e..b92b73569243 100644 --- a/crates/analytics/src/payment_intents/metrics/sessionized_metrics/smart_retried_amount.rs +++ b/crates/analytics/src/payment_intents/metrics/sessionized_metrics/smart_retried_amount.rs @@ -126,6 +126,15 @@ where i.status.as_ref().map(|i| i.0), i.currency.as_ref().map(|i| i.0), i.profile_id.clone(), + i.connector.clone(), + i.authentication_type.as_ref().map(|i| i.0), + i.payment_method.clone(), + i.payment_method_type.clone(), + i.card_network.clone(), + i.merchant_id.clone(), + i.card_last_4.clone(), + i.card_issuer.clone(), + i.error_reason.clone(), TimeRange { start_time: match (granularity, i.start_bucket) { (Some(g), Some(st)) => g.clip_to_start(st)?, diff --git a/crates/analytics/src/payment_intents/metrics/sessionized_metrics/successful_smart_retries.rs b/crates/analytics/src/payment_intents/metrics/sessionized_metrics/successful_smart_retries.rs index bf97e4c41eff..0b28cb5366d6 100644 --- a/crates/analytics/src/payment_intents/metrics/sessionized_metrics/successful_smart_retries.rs +++ b/crates/analytics/src/payment_intents/metrics/sessionized_metrics/successful_smart_retries.rs @@ -111,6 +111,15 @@ where i.status.as_ref().map(|i| i.0), i.currency.as_ref().map(|i| i.0), i.profile_id.clone(), + i.connector.clone(), + i.authentication_type.as_ref().map(|i| i.0), + i.payment_method.clone(), + i.payment_method_type.clone(), + i.card_network.clone(), + i.merchant_id.clone(), + i.card_last_4.clone(), + i.card_issuer.clone(), + i.error_reason.clone(), TimeRange { start_time: match (granularity, i.start_bucket) { (Some(g), Some(st)) => g.clip_to_start(st)?, diff --git a/crates/analytics/src/payment_intents/metrics/sessionized_metrics/total_smart_retries.rs b/crates/analytics/src/payment_intents/metrics/sessionized_metrics/total_smart_retries.rs index cea5b2fa4656..20ef8be62770 100644 --- a/crates/analytics/src/payment_intents/metrics/sessionized_metrics/total_smart_retries.rs +++ b/crates/analytics/src/payment_intents/metrics/sessionized_metrics/total_smart_retries.rs @@ -106,6 +106,15 @@ where i.status.as_ref().map(|i| i.0), i.currency.as_ref().map(|i| i.0), i.profile_id.clone(), + i.connector.clone(), + i.authentication_type.as_ref().map(|i| i.0), + i.payment_method.clone(), + i.payment_method_type.clone(), + i.card_network.clone(), + i.merchant_id.clone(), + i.card_last_4.clone(), + i.card_issuer.clone(), + i.error_reason.clone(), TimeRange { start_time: match (granularity, i.start_bucket) { (Some(g), Some(st)) => g.clip_to_start(st)?, diff --git a/crates/analytics/src/payment_intents/metrics/smart_retried_amount.rs b/crates/analytics/src/payment_intents/metrics/smart_retried_amount.rs index 9497dc89f42c..ac08f59f358c 100644 --- a/crates/analytics/src/payment_intents/metrics/smart_retried_amount.rs +++ b/crates/analytics/src/payment_intents/metrics/smart_retried_amount.rs @@ -125,6 +125,15 @@ where i.status.as_ref().map(|i| i.0), i.currency.as_ref().map(|i| i.0), i.profile_id.clone(), + i.connector.clone(), + i.authentication_type.as_ref().map(|i| i.0), + i.payment_method.clone(), + i.payment_method_type.clone(), + i.card_network.clone(), + i.merchant_id.clone(), + i.card_last_4.clone(), + i.card_issuer.clone(), + i.error_reason.clone(), TimeRange { start_time: match (granularity, i.start_bucket) { (Some(g), Some(st)) => g.clip_to_start(st)?, diff --git a/crates/analytics/src/payment_intents/metrics/successful_smart_retries.rs b/crates/analytics/src/payment_intents/metrics/successful_smart_retries.rs index 4fe5f3a26f51..a19bdec518c4 100644 --- a/crates/analytics/src/payment_intents/metrics/successful_smart_retries.rs +++ b/crates/analytics/src/payment_intents/metrics/successful_smart_retries.rs @@ -111,6 +111,15 @@ where i.status.as_ref().map(|i| i.0), i.currency.as_ref().map(|i| i.0), i.profile_id.clone(), + i.connector.clone(), + i.authentication_type.as_ref().map(|i| i.0), + i.payment_method.clone(), + i.payment_method_type.clone(), + i.card_network.clone(), + i.merchant_id.clone(), + i.card_last_4.clone(), + i.card_issuer.clone(), + i.error_reason.clone(), TimeRange { start_time: match (granularity, i.start_bucket) { (Some(g), Some(st)) => g.clip_to_start(st)?, diff --git a/crates/analytics/src/payment_intents/metrics/total_smart_retries.rs b/crates/analytics/src/payment_intents/metrics/total_smart_retries.rs index e98efa9f6abc..f5539abd9f50 100644 --- a/crates/analytics/src/payment_intents/metrics/total_smart_retries.rs +++ b/crates/analytics/src/payment_intents/metrics/total_smart_retries.rs @@ -106,6 +106,15 @@ where i.status.as_ref().map(|i| i.0), i.currency.as_ref().map(|i| i.0), i.profile_id.clone(), + i.connector.clone(), + i.authentication_type.as_ref().map(|i| i.0), + i.payment_method.clone(), + i.payment_method_type.clone(), + i.card_network.clone(), + i.merchant_id.clone(), + i.card_last_4.clone(), + i.card_issuer.clone(), + i.error_reason.clone(), TimeRange { start_time: match (granularity, i.start_bucket) { (Some(g), Some(st)) => g.clip_to_start(st)?, diff --git a/crates/analytics/src/payment_intents/types.rs b/crates/analytics/src/payment_intents/types.rs index bb5141297c56..a27ef2d840df 100644 --- a/crates/analytics/src/payment_intents/types.rs +++ b/crates/analytics/src/payment_intents/types.rs @@ -30,6 +30,63 @@ where .add_filter_in_range_clause(PaymentIntentDimensions::ProfileId, &self.profile_id) .attach_printable("Error adding profile id filter")?; } + if !self.connector.is_empty() { + builder + .add_filter_in_range_clause(PaymentIntentDimensions::Connector, &self.connector) + .attach_printable("Error adding connector filter")?; + } + if !self.auth_type.is_empty() { + builder + .add_filter_in_range_clause(PaymentIntentDimensions::AuthType, &self.auth_type) + .attach_printable("Error adding auth type filter")?; + } + if !self.payment_method.is_empty() { + builder + .add_filter_in_range_clause( + PaymentIntentDimensions::PaymentMethod, + &self.payment_method, + ) + .attach_printable("Error adding payment method filter")?; + } + if !self.payment_method_type.is_empty() { + builder + .add_filter_in_range_clause( + PaymentIntentDimensions::PaymentMethodType, + &self.payment_method_type, + ) + .attach_printable("Error adding payment method type filter")?; + } + if !self.card_network.is_empty() { + builder + .add_filter_in_range_clause( + PaymentIntentDimensions::CardNetwork, + &self.card_network, + ) + .attach_printable("Error adding card network filter")?; + } + if !self.merchant_id.is_empty() { + builder + .add_filter_in_range_clause(PaymentIntentDimensions::MerchantId, &self.merchant_id) + .attach_printable("Error adding merchant id filter")?; + } + if !self.card_last_4.is_empty() { + builder + .add_filter_in_range_clause(PaymentIntentDimensions::CardLast4, &self.card_last_4) + .attach_printable("Error adding card last 4 filter")?; + } + if !self.card_issuer.is_empty() { + builder + .add_filter_in_range_clause(PaymentIntentDimensions::CardIssuer, &self.card_issuer) + .attach_printable("Error adding card issuer filter")?; + } + if !self.error_reason.is_empty() { + builder + .add_filter_in_range_clause( + PaymentIntentDimensions::ErrorReason, + &self.error_reason, + ) + .attach_printable("Error adding error reason filter")?; + } if !self.customer_id.is_empty() { builder .add_filter_in_range_clause("customer_id", &self.customer_id) diff --git a/crates/analytics/src/sqlx.rs b/crates/analytics/src/sqlx.rs index 7c90e37c55fb..0a641fbc5f95 100644 --- a/crates/analytics/src/sqlx.rs +++ b/crates/analytics/src/sqlx.rs @@ -604,6 +604,45 @@ impl<'a> FromRow<'a, PgRow> for super::payment_intents::metrics::PaymentIntentMe 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 card_network: Option = row.try_get("card_network").or_else(|e| match e { + ColumnNotFound(_) => Ok(Default::default()), + e => Err(e), + })?; + let merchant_id: Option = row.try_get("merchant_id").or_else(|e| match e { + ColumnNotFound(_) => Ok(Default::default()), + e => Err(e), + })?; + let card_last_4: Option = row.try_get("card_last_4").or_else(|e| match e { + ColumnNotFound(_) => Ok(Default::default()), + e => Err(e), + })?; + let card_issuer: Option = row.try_get("card_issuer").or_else(|e| match e { + ColumnNotFound(_) => Ok(Default::default()), + e => Err(e), + })?; + let error_reason: Option = row.try_get("error_reason").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), @@ -627,6 +666,15 @@ impl<'a> FromRow<'a, PgRow> for super::payment_intents::metrics::PaymentIntentMe status, currency, profile_id, + connector, + authentication_type, + payment_method, + payment_method_type, + card_network, + merchant_id, + card_last_4, + card_issuer, + error_reason, first_attempt, total, count, @@ -652,6 +700,45 @@ impl<'a> FromRow<'a, PgRow> for super::payment_intents::filters::PaymentIntentFi 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 card_network: Option = row.try_get("card_network").or_else(|e| match e { + ColumnNotFound(_) => Ok(Default::default()), + e => Err(e), + })?; + let merchant_id: Option = row.try_get("merchant_id").or_else(|e| match e { + ColumnNotFound(_) => Ok(Default::default()), + e => Err(e), + })?; + let card_last_4: Option = row.try_get("card_last_4").or_else(|e| match e { + ColumnNotFound(_) => Ok(Default::default()), + e => Err(e), + })?; + let card_issuer: Option = row.try_get("card_issuer").or_else(|e| match e { + ColumnNotFound(_) => Ok(Default::default()), + e => Err(e), + })?; + let error_reason: Option = row.try_get("error_reason").or_else(|e| match e { + ColumnNotFound(_) => Ok(Default::default()), + e => Err(e), + })?; let customer_id: Option = row.try_get("customer_id").or_else(|e| match e { ColumnNotFound(_) => Ok(Default::default()), e => Err(e), @@ -660,6 +747,15 @@ impl<'a> FromRow<'a, PgRow> for super::payment_intents::filters::PaymentIntentFi status, currency, profile_id, + connector, + authentication_type, + payment_method, + payment_method_type, + card_network, + merchant_id, + card_last_4, + card_issuer, + error_reason, customer_id, }) } diff --git a/crates/analytics/src/utils.rs b/crates/analytics/src/utils.rs index 435e95451fe4..fc21bf098192 100644 --- a/crates/analytics/src/utils.rs +++ b/crates/analytics/src/utils.rs @@ -35,6 +35,12 @@ pub fn get_payment_intent_dimensions() -> Vec { PaymentIntentDimensions::PaymentIntentStatus, PaymentIntentDimensions::Currency, PaymentIntentDimensions::ProfileId, + PaymentIntentDimensions::Connector, + PaymentIntentDimensions::AuthType, + PaymentIntentDimensions::PaymentMethod, + PaymentIntentDimensions::PaymentMethodType, + PaymentIntentDimensions::CardNetwork, + PaymentIntentDimensions::MerchantId, ] .into_iter() .map(Into::into) diff --git a/crates/api_models/src/analytics/payment_intents.rs b/crates/api_models/src/analytics/payment_intents.rs index 3ac3c09d35f6..dd51c97d9358 100644 --- a/crates/api_models/src/analytics/payment_intents.rs +++ b/crates/api_models/src/analytics/payment_intents.rs @@ -6,7 +6,9 @@ use std::{ use common_utils::id_type; use super::{NameDescription, TimeRange}; -use crate::enums::{Currency, IntentStatus}; +use crate::enums::{ + AuthenticationType, Connector, Currency, IntentStatus, PaymentMethod, PaymentMethodType, +}; #[derive(Clone, Debug, Default, serde::Deserialize, serde::Serialize)] pub struct PaymentIntentFilters { @@ -17,6 +19,24 @@ pub struct PaymentIntentFilters { #[serde(default)] pub profile_id: Vec, #[serde(default)] + pub connector: Vec, + #[serde(default)] + pub auth_type: Vec, + #[serde(default)] + pub payment_method: Vec, + #[serde(default)] + pub payment_method_type: Vec, + #[serde(default)] + pub card_network: Vec, + #[serde(default)] + pub merchant_id: Vec, + #[serde(default)] + pub card_last_4: Vec, + #[serde(default)] + pub card_issuer: Vec, + #[serde(default)] + pub error_reason: Vec, + #[serde(default)] pub customer_id: Vec, } @@ -42,6 +62,15 @@ pub enum PaymentIntentDimensions { PaymentIntentStatus, Currency, ProfileId, + Connector, + AuthType, + PaymentMethod, + PaymentMethodType, + CardNetwork, + MerchantId, + CardLast4, + CardIssuer, + ErrorReason, } #[derive( @@ -112,6 +141,15 @@ pub struct PaymentIntentMetricsBucketIdentifier { pub status: Option, pub currency: Option, pub profile_id: Option, + pub connector: Option, + pub auth_type: Option, + pub payment_method: Option, + pub payment_method_type: Option, + pub card_network: Option, + pub merchant_id: Option, + pub card_last_4: Option, + pub card_issuer: Option, + pub error_reason: Option, #[serde(rename = "time_range")] pub time_bucket: TimeRange, #[serde(rename = "time_bucket")] @@ -125,12 +163,30 @@ impl PaymentIntentMetricsBucketIdentifier { status: Option, currency: Option, profile_id: Option, + connector: Option, + auth_type: Option, + payment_method: Option, + payment_method_type: Option, + card_network: Option, + merchant_id: Option, + card_last_4: Option, + card_issuer: Option, + error_reason: Option, normalized_time_range: TimeRange, ) -> Self { Self { status, currency, profile_id, + connector, + auth_type, + payment_method, + payment_method_type, + card_network, + merchant_id, + card_last_4, + card_issuer, + error_reason, time_bucket: normalized_time_range, start_time: normalized_time_range.start_time, } @@ -142,6 +198,15 @@ impl Hash for PaymentIntentMetricsBucketIdentifier { self.status.map(|i| i.to_string()).hash(state); self.currency.hash(state); self.profile_id.hash(state); + 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.card_network.hash(state); + self.merchant_id.hash(state); + self.card_last_4.hash(state); + self.card_issuer.hash(state); + self.error_reason.hash(state); self.time_bucket.hash(state); } }