From daf0f09f8e3293ee6a3599a25362d9171fc5b2e7 Mon Sep 17 00:00:00 2001 From: Hrithikesh <61539176+hrithikesh026@users.noreply.github.com> Date: Mon, 4 Dec 2023 22:30:51 +0530 Subject: [PATCH 1/5] feat: calculate surcharge for customer saved card list (#3039) Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com> --- Cargo.lock | 1 + crates/api_models/src/payment_methods.rs | 68 +++++---- crates/common_utils/Cargo.toml | 1 + .../router/src/core/payment_methods/cards.rs | 108 ++++++++++++-- .../surcharge_decision_configs.rs | 93 ++++++++++-- crates/router/src/core/payments.rs | 77 ++++++---- crates/router/src/core/payments/helpers.rs | 3 +- .../payments/operations/payment_confirm.rs | 77 +--------- .../payments/operations/payment_create.rs | 2 +- .../payments/operations/payment_update.rs | 2 +- crates/router/src/core/payments/types.rs | 139 ++++++++++++------ crates/router/src/core/utils.rs | 74 +--------- crates/router/src/openapi.rs | 5 +- crates/router/src/types/api.rs | 6 +- openapi/openapi_spec.json | 108 ++++++++++++++ 15 files changed, 477 insertions(+), 287 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e8719b29f51d..4231c62d9499 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1708,6 +1708,7 @@ dependencies = [ "thiserror", "time", "tokio 1.32.0", + "utoipa", ] [[package]] diff --git a/crates/api_models/src/payment_methods.rs b/crates/api_models/src/payment_methods.rs index b3c6b049d5d9..84830498b344 100644 --- a/crates/api_models/src/payment_methods.rs +++ b/crates/api_models/src/payment_methods.rs @@ -8,7 +8,7 @@ use common_utils::{ types::{Percentage, Surcharge}, }; use serde::de; -use utoipa::ToSchema; +use utoipa::{schema, ToSchema}; #[cfg(feature = "payouts")] use crate::payouts; @@ -264,19 +264,6 @@ pub struct CardNetworkTypes { pub card_network: api_enums::CardNetwork, /// surcharge details for this card network - #[schema(example = r#" - { - "surcharge": { - "type": "rate", - "value": { - "percentage": 2.5 - } - }, - "tax_on_surcharge": { - "percentage": 1.5 - } - } - "#)] pub surcharge_details: Option, /// The list of eligible connectors for a given card network @@ -313,31 +300,19 @@ pub struct ResponsePaymentMethodTypes { pub required_fields: Option>, /// surcharge details for this payment method type if exists - #[schema(example = r#" - { - "surcharge": { - "type": "rate", - "value": { - "percentage": 2.5 - } - }, - "tax_on_surcharge": { - "percentage": 1.5 - } - } - "#)] pub surcharge_details: Option, /// auth service connector label for this payment method type, if exists pub pm_auth_connector: Option, } -#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize, ToSchema)] + +#[derive(Clone, Debug, PartialEq, serde::Serialize, ToSchema)] #[serde(rename_all = "snake_case")] pub struct SurchargeDetailsResponse { /// surcharge value - pub surcharge: Surcharge, + pub surcharge: SurchargeResponse, /// tax on surcharge value - pub tax_on_surcharge: Option>, + pub tax_on_surcharge: Option, /// surcharge amount for this payment pub display_surcharge_amount: f64, /// tax on surcharge amount for this payment @@ -348,6 +323,36 @@ pub struct SurchargeDetailsResponse { pub display_final_amount: f64, } +#[derive(Clone, Debug, PartialEq, serde::Serialize, ToSchema)] +#[serde(rename_all = "snake_case", tag = "type", content = "value")] +pub enum SurchargeResponse { + /// Fixed Surcharge value + Fixed(i64), + /// Surcharge percentage + Rate(SurchargePercentage), +} + +impl From for SurchargeResponse { + fn from(value: Surcharge) -> Self { + match value { + Surcharge::Fixed(amount) => Self::Fixed(amount), + Surcharge::Rate(percentage) => Self::Rate(percentage.into()), + } + } +} + +#[derive(Clone, Default, Debug, PartialEq, serde::Serialize, ToSchema)] +pub struct SurchargePercentage { + percentage: f32, +} + +impl From> for SurchargePercentage { + fn from(value: Percentage) -> Self { + Self { + percentage: value.get_percentage(), + } + } +} /// Required fields info used while listing the payment_method_data #[derive(Debug, serde::Deserialize, serde::Serialize, Clone, PartialEq, Eq, ToSchema, Hash)] pub struct RequiredFieldInfo { @@ -716,6 +721,9 @@ pub struct CustomerPaymentMethod { #[schema(example = json!({"mask": "0000"}))] pub bank: Option, + /// Surcharge details for this saved card + pub surcharge_details: Option, + /// Whether this payment method requires CVV to be collected #[schema(example = true)] pub requires_cvv: bool, diff --git a/crates/common_utils/Cargo.toml b/crates/common_utils/Cargo.toml index 3619c93d772c..3a41b111b39d 100644 --- a/crates/common_utils/Cargo.toml +++ b/crates/common_utils/Cargo.toml @@ -38,6 +38,7 @@ strum = { version = "0.24.1", features = ["derive"] } thiserror = "1.0.40" time = { version = "0.3.21", features = ["serde", "serde-well-known", "std"] } tokio = { version = "1.28.2", features = ["macros", "rt-multi-thread"], optional = true } +utoipa = { version = "3.3.0", features = ["preserve_order"] } # First party crates common_enums = { version = "0.1.0", path = "../common_enums" } diff --git a/crates/router/src/core/payment_methods/cards.rs b/crates/router/src/core/payment_methods/cards.rs index 545733e298ab..bbcfe45a1d0c 100644 --- a/crates/router/src/core/payment_methods/cards.rs +++ b/crates/router/src/core/payment_methods/cards.rs @@ -25,7 +25,10 @@ use error_stack::{report, IntoReport, ResultExt}; use masking::Secret; use router_env::{instrument, tracing}; -use super::surcharge_decision_configs::perform_surcharge_decision_management_for_payment_method_list; +use super::surcharge_decision_configs::{ + perform_surcharge_decision_management_for_payment_method_list, + perform_surcharge_decision_management_for_saved_cards, +}; use crate::{ configs::settings, core::{ @@ -38,7 +41,6 @@ use crate::{ helpers, routing::{self, SessionFlowRoutingInput}, }, - utils::persist_individual_surcharge_details_in_redis, }, db, logger, pii::prelude::*, @@ -1687,12 +1689,9 @@ pub async fn call_surcharge_decision_management( .change_context(errors::ApiErrorResponse::InternalServerError) .attach_printable("error performing surcharge decision operation")?; if !surcharge_results.is_empty_result() { - persist_individual_surcharge_details_in_redis( - &state, - merchant_account, - &surcharge_results, - ) - .await?; + surcharge_results + .persist_individual_surcharge_details_in_redis(&state, merchant_account) + .await?; let _ = state .store .update_payment_intent( @@ -1711,6 +1710,56 @@ pub async fn call_surcharge_decision_management( } } +pub async fn call_surcharge_decision_management_for_saved_card( + state: &routes::AppState, + merchant_account: &domain::MerchantAccount, + payment_attempt: &storage::PaymentAttempt, + payment_intent: storage::PaymentIntent, + customer_payment_method_response: &mut api::CustomerPaymentMethodsListResponse, +) -> errors::RouterResult<()> { + if payment_attempt.surcharge_amount.is_some() { + Ok(()) + } else { + let algorithm_ref: routing_types::RoutingAlgorithmRef = merchant_account + .routing_algorithm + .clone() + .map(|val| val.parse_value("routing algorithm")) + .transpose() + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Could not decode the routing algorithm")? + .unwrap_or_default(); + let surcharge_results = perform_surcharge_decision_management_for_saved_cards( + state, + algorithm_ref, + payment_attempt, + &payment_intent, + &mut customer_payment_method_response.customer_payment_methods, + ) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("error performing surcharge decision operation")?; + if !surcharge_results.is_empty_result() { + surcharge_results + .persist_individual_surcharge_details_in_redis(state, merchant_account) + .await?; + let _ = state + .store + .update_payment_intent( + payment_intent, + storage::PaymentIntentUpdate::SurchargeApplicableUpdate { + surcharge_applicable: true, + updated_by: merchant_account.storage_scheme.to_string(), + }, + merchant_account.storage_scheme, + ) + .await + .to_not_found_response(errors::ApiErrorResponse::PaymentNotFound) + .attach_printable("Failed to update surcharge_applicable in Payment Intent"); + } + Ok(()) + } +} + #[allow(clippy::too_many_arguments)] pub async fn filter_payment_methods( payment_methods: Vec, @@ -2195,12 +2244,13 @@ pub async fn do_list_customer_pm_fetch_customer_if_not_passed( .await } else { let cloned_secret = req.and_then(|r| r.client_secret.as_ref().cloned()); - let payment_intent = helpers::verify_payment_intent_time_and_client_secret( - db, - &merchant_account, - cloned_secret, - ) - .await?; + let payment_intent: Option = + helpers::verify_payment_intent_time_and_client_secret( + db, + &merchant_account, + cloned_secret, + ) + .await?; let customer_id = payment_intent .as_ref() .and_then(|intent| intent.customer_id.to_owned()) @@ -2326,6 +2376,7 @@ pub async fn list_customer_payment_method( created: Some(pm.created_at), bank_transfer: pmd, bank: bank_details, + surcharge_details: None, requires_cvv, }; customer_pms.push(pma.to_owned()); @@ -2377,9 +2428,36 @@ pub async fn list_customer_payment_method( } } - let response = api::CustomerPaymentMethodsListResponse { + let mut response = api::CustomerPaymentMethodsListResponse { customer_payment_methods: customer_pms, }; + let payment_attempt = payment_intent + .as_ref() + .async_map(|payment_intent| async { + state + .store + .find_payment_attempt_by_payment_id_merchant_id_attempt_id( + &payment_intent.payment_id, + &merchant_account.merchant_id, + &payment_intent.active_attempt.get_id(), + merchant_account.storage_scheme, + ) + .await + .to_not_found_response(errors::ApiErrorResponse::PaymentNotFound) + }) + .await + .transpose()?; + + if let Some((payment_attempt, payment_intent)) = payment_attempt.zip(payment_intent) { + call_surcharge_decision_management_for_saved_card( + state, + &merchant_account, + &payment_attempt, + payment_intent, + &mut response, + ) + .await?; + } Ok(services::ApplicationResponse::Json(response)) } diff --git a/crates/router/src/core/payment_methods/surcharge_decision_configs.rs b/crates/router/src/core/payment_methods/surcharge_decision_configs.rs index 38ae71754b87..e130795e945a 100644 --- a/crates/router/src/core/payment_methods/surcharge_decision_configs.rs +++ b/crates/router/src/core/payment_methods/surcharge_decision_configs.rs @@ -112,9 +112,11 @@ pub async fn perform_surcharge_decision_management_for_payment_method_list( payment_attempt, )?; surcharge_metadata.insert_surcharge_details( - &payment_methods_enabled.payment_method, - &payment_method_type_response.payment_method_type, - Some(&card_network_type.card_network), + types::SurchargeKey::PaymentMethodData( + payment_methods_enabled.payment_method, + payment_method_type_response.payment_method_type, + Some(card_network_type.card_network.clone()), + ), surcharge_details.clone(), ); SurchargeDetailsResponse::foreign_try_from(( @@ -138,9 +140,11 @@ pub async fn perform_surcharge_decision_management_for_payment_method_list( payment_attempt, )?; surcharge_metadata.insert_surcharge_details( - &payment_methods_enabled.payment_method, - &payment_method_type_response.payment_method_type, - None, + types::SurchargeKey::PaymentMethodData( + payment_methods_enabled.payment_method, + payment_method_type_response.payment_method_type, + None, + ), surcharge_details.clone(), ); SurchargeDetailsResponse::foreign_try_from(( @@ -201,15 +205,82 @@ where let surcharge_output = execute_dsl_and_get_conditional_config(backend_input.clone(), interpreter)?; if let Some(surcharge_details) = surcharge_output.surcharge_details { - let surcharge_details_response = get_surcharge_details_from_surcharge_output( + let surcharge_details = get_surcharge_details_from_surcharge_output( surcharge_details, &payment_data.payment_attempt, )?; surcharge_metadata.insert_surcharge_details( - &payment_method_type.to_owned().into(), - payment_method_type, - None, - surcharge_details_response, + types::SurchargeKey::PaymentMethodData( + payment_method_type.to_owned().into(), + *payment_method_type, + None, + ), + surcharge_details, + ); + } + } + Ok(surcharge_metadata) +} +pub async fn perform_surcharge_decision_management_for_saved_cards( + state: &AppState, + algorithm_ref: routing::RoutingAlgorithmRef, + payment_attempt: &oss_storage::PaymentAttempt, + payment_intent: &oss_storage::PaymentIntent, + customer_payment_method_list: &mut [api_models::payment_methods::CustomerPaymentMethod], +) -> ConditionalConfigResult { + let mut surcharge_metadata = types::SurchargeMetadata::new(payment_attempt.attempt_id.clone()); + let algorithm_id = if let Some(id) = algorithm_ref.surcharge_config_algo_id { + id + } else { + return Ok(surcharge_metadata); + }; + + let key = ensure_algorithm_cached( + &*state.store, + &payment_attempt.merchant_id, + algorithm_ref.timestamp, + algorithm_id.as_str(), + ) + .await?; + let cached_algo = CONF_CACHE + .retrieve(&key) + .into_report() + .change_context(ConfigError::CacheMiss) + .attach_printable("Unable to retrieve cached routing algorithm even after refresh")?; + let mut backend_input = make_dsl_input_for_surcharge(payment_attempt, payment_intent, None) + .change_context(ConfigError::InputConstructionError)?; + let interpreter = &cached_algo.cached_alogorith; + + for customer_payment_method in customer_payment_method_list.iter_mut() { + backend_input.payment_method.payment_method = Some(customer_payment_method.payment_method); + backend_input.payment_method.payment_method_type = + customer_payment_method.payment_method_type; + backend_input.payment_method.card_network = customer_payment_method + .card + .as_ref() + .and_then(|card| card.scheme.as_ref()) + .map(|scheme| { + scheme + .clone() + .parse_enum("CardNetwork") + .change_context(ConfigError::DslExecutionError) + }) + .transpose()?; + let surcharge_output = + execute_dsl_and_get_conditional_config(backend_input.clone(), interpreter)?; + if let Some(surcharge_details_output) = surcharge_output.surcharge_details { + let surcharge_details = get_surcharge_details_from_surcharge_output( + surcharge_details_output, + payment_attempt, + )?; + surcharge_metadata.insert_surcharge_details( + types::SurchargeKey::Token(customer_payment_method.payment_token.clone()), + surcharge_details.clone(), + ); + customer_payment_method.surcharge_details = Some( + SurchargeDetailsResponse::foreign_try_from((&surcharge_details, payment_attempt)) + .into_report() + .change_context(ConfigError::DslParsingError)?, ); } } diff --git a/crates/router/src/core/payments.rs b/crates/router/src/core/payments.rs index 2a3ed0f1596f..16fda276f6a5 100644 --- a/crates/router/src/core/payments.rs +++ b/crates/router/src/core/payments.rs @@ -39,11 +39,8 @@ use self::{ helpers::get_key_params_for_surcharge_details, operations::{payment_complete_authorize, BoxedOperation, Operation}, routing::{self as self_routing, SessionFlowRoutingInput}, - types::SurchargeDetails, -}; -use super::{ - errors::StorageErrorExt, payment_methods::surcharge_decision_configs, utils as core_utils, }; +use super::{errors::StorageErrorExt, payment_methods::surcharge_decision_configs}; use crate::{ configs::settings::PaymentMethodTypeTokenFilter, core::{ @@ -408,26 +405,39 @@ where .surcharge_applicable .unwrap_or(false) { - let payment_method_data = payment_data + let raw_card_key = payment_data .payment_method_data - .clone() - .get_required_value("payment_method_data")?; - let (payment_method, payment_method_type, card_network) = - get_key_params_for_surcharge_details(payment_method_data)?; + .as_ref() + .map(get_key_params_for_surcharge_details) + .transpose()? + .map(|(payment_method, payment_method_type, card_network)| { + types::SurchargeKey::PaymentMethodData( + payment_method, + payment_method_type, + card_network, + ) + }); + let saved_card_key = payment_data.token.clone().map(types::SurchargeKey::Token); - let calculated_surcharge_details = match utils::get_individual_surcharge_detail_from_redis( - state, - &payment_method, - &payment_method_type, - card_network, - &payment_data.payment_attempt.attempt_id, - ) - .await - { - Ok(surcharge_details) => Some(surcharge_details), - Err(err) if err.current_context() == &RedisError::NotFound => None, - Err(err) => Err(err).change_context(errors::ApiErrorResponse::InternalServerError)?, - }; + let surcharge_key = raw_card_key + .or(saved_card_key) + .get_required_value("payment_method_data or payment_token")?; + logger::debug!(surcharge_key_confirm =? surcharge_key); + + let calculated_surcharge_details = + match types::SurchargeMetadata::get_individual_surcharge_detail_from_redis( + state, + surcharge_key, + &payment_data.payment_attempt.attempt_id, + ) + .await + { + Ok(surcharge_details) => Some(surcharge_details), + Err(err) if err.current_context() == &RedisError::NotFound => None, + Err(err) => { + Err(err).change_context(errors::ApiErrorResponse::InternalServerError)? + } + }; payment_data.surcharge_details = calculated_surcharge_details; } else { @@ -436,7 +446,10 @@ where .payment_attempt .get_surcharge_details() .map(|surcharge_details| { - SurchargeDetails::from((&surcharge_details, &payment_data.payment_attempt)) + types::SurchargeDetails::from(( + &surcharge_details, + &payment_data.payment_attempt, + )) }); payment_data.surcharge_details = surcharge_details; } @@ -469,7 +482,7 @@ where let final_amount = payment_data.payment_attempt.amount + surcharge_amount + tax_on_surcharge_amount; Ok(Some(api::SessionSurchargeDetails::PreDetermined( - SurchargeDetails { + types::SurchargeDetails { surcharge: Surcharge::Fixed(surcharge_amount), tax_on_surcharge: None, surcharge_amount, @@ -501,12 +514,9 @@ where .change_context(errors::ApiErrorResponse::InternalServerError) .attach_printable("error performing surcharge decision operation")?; - core_utils::persist_individual_surcharge_details_in_redis( - state, - merchant_account, - &surcharge_results, - ) - .await?; + surcharge_results + .persist_individual_surcharge_details_in_redis(state, merchant_account) + .await?; Ok(if surcharge_results.is_empty_result() { None @@ -917,6 +927,11 @@ where merchant_connector_account.get_mca_id(); } + operation + .to_domain()? + .populate_payment_data(state, payment_data, merchant_account) + .await?; + let (pd, tokenization_action) = get_connector_tokenization_action_when_confirm_true( state, operation, @@ -1846,7 +1861,7 @@ where pub recurring_mandate_payment_data: Option, pub ephemeral_key: Option, pub redirect_response: Option, - pub surcharge_details: Option, + pub surcharge_details: Option, pub frm_message: Option, pub payment_link_data: Option, pub incremental_authorization_details: Option, diff --git a/crates/router/src/core/payments/helpers.rs b/crates/router/src/core/payments/helpers.rs index c9ab77c6a332..4e491964e96c 100644 --- a/crates/router/src/core/payments/helpers.rs +++ b/crates/router/src/core/payments/helpers.rs @@ -3626,7 +3626,7 @@ impl ApplePayData { } pub fn get_key_params_for_surcharge_details( - payment_method_data: api_models::payments::PaymentMethodData, + payment_method_data: &api_models::payments::PaymentMethodData, ) -> RouterResult<( common_enums::PaymentMethod, common_enums::PaymentMethodType, @@ -3636,6 +3636,7 @@ pub fn get_key_params_for_surcharge_details( api_models::payments::PaymentMethodData::Card(card) => { let card_network = card .card_network + .clone() .get_required_value("payment_method_data.card.card_network")?; // surcharge generated will always be same for credit as well as debit // since surcharge conditions cannot be defined on card_type diff --git a/crates/router/src/core/payments/operations/payment_confirm.rs b/crates/router/src/core/payments/operations/payment_confirm.rs index 578395860c9a..af2a9fa49c8b 100644 --- a/crates/router/src/core/payments/operations/payment_confirm.rs +++ b/crates/router/src/core/payments/operations/payment_confirm.rs @@ -5,7 +5,6 @@ use async_trait::async_trait; use common_utils::ext_traits::{AsyncExt, Encode}; use error_stack::{report, IntoReport, ResultExt}; use futures::FutureExt; -use redis_interface::errors::RedisError; use router_derive::PaymentOperation; use router_env::{instrument, tracing}; use tracing_futures::Instrument; @@ -19,7 +18,7 @@ use crate::{ self, helpers, operations, populate_surcharge_details, CustomerDetails, PaymentAddress, PaymentData, }, - utils::{self as core_utils, get_individual_surcharge_detail_from_redis}, + utils::{self as core_utils}, }, db::StorageInterface, routes::AppState, @@ -439,13 +438,6 @@ impl sm }); - Self::validate_request_surcharge_details_with_session_surcharge_details( - state, - &payment_attempt, - request, - ) - .await?; - let additional_pm_data = request .payment_method_data .as_ref() @@ -904,70 +896,3 @@ impl ValidateRequest RouterResult<()> { - match ( - request.surcharge_details, - request.payment_method_data.as_ref(), - ) { - (Some(request_surcharge_details), Some(payment_method_data)) => { - if let Some(payment_method_type) = - payment_method_data.get_payment_method_type_if_session_token_type() - { - let invalid_surcharge_details_error = Err(errors::ApiErrorResponse::InvalidRequestData { - message: "surcharge_details sent in session token flow doesn't match with the one sent in confirm request".into(), - }.into()); - if let Some(attempt_surcharge_amount) = payment_attempt.surcharge_amount { - // payment_attempt.surcharge_amount will be Some if some surcharge was sent in payment create - // if surcharge was sent in payment create call, the same would have been sent to the connector during session call - // So verify the same - if request_surcharge_details.surcharge_amount != attempt_surcharge_amount - || request_surcharge_details.tax_amount != payment_attempt.tax_amount - { - return invalid_surcharge_details_error; - } - } else { - // if not sent in payment create - // verify that any calculated surcharge sent in session flow is same as the one sent in confirm - return match get_individual_surcharge_detail_from_redis( - state, - &payment_method_type.into(), - &payment_method_type, - None, - &payment_attempt.attempt_id, - ) - .await - { - Ok(surcharge_details) => utils::when( - !surcharge_details - .is_request_surcharge_matching(request_surcharge_details), - || invalid_surcharge_details_error, - ), - Err(err) if err.current_context() == &RedisError::NotFound => { - utils::when(!request_surcharge_details.is_surcharge_zero(), || { - invalid_surcharge_details_error - }) - } - Err(err) => Err(err) - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Failed to fetch redis value"), - }; - } - } - Ok(()) - } - (Some(_request_surcharge_details), None) => { - Err(errors::ApiErrorResponse::MissingRequiredField { - field_name: "payment_method_data", - } - .into()) - } - _ => Ok(()), - } - } -} diff --git a/crates/router/src/core/payments/operations/payment_create.rs b/crates/router/src/core/payments/operations/payment_create.rs index 287e4945951b..eb7f31ba24d1 100644 --- a/crates/router/src/core/payments/operations/payment_create.rs +++ b/crates/router/src/core/payments/operations/payment_create.rs @@ -287,7 +287,7 @@ impl let setup_mandate = setup_mandate.map(MandateData::from); let surcharge_details = request.surcharge_details.map(|request_surcharge_details| { - payments::SurchargeDetails::from((&request_surcharge_details, &payment_attempt)) + payments::types::SurchargeDetails::from((&request_surcharge_details, &payment_attempt)) }); let payment_method_data_after_card_bin_call = request diff --git a/crates/router/src/core/payments/operations/payment_update.rs b/crates/router/src/core/payments/operations/payment_update.rs index e1c373171682..f1a35cffce87 100644 --- a/crates/router/src/core/payments/operations/payment_update.rs +++ b/crates/router/src/core/payments/operations/payment_update.rs @@ -337,7 +337,7 @@ impl })?; let surcharge_details = request.surcharge_details.map(|request_surcharge_details| { - payments::SurchargeDetails::from((&request_surcharge_details, &payment_attempt)) + payments::types::SurchargeDetails::from((&request_surcharge_details, &payment_attempt)) }); let payment_data = PaymentData { diff --git a/crates/router/src/core/payments/types.rs b/crates/router/src/core/payments/types.rs index f97cdc17d724..001082d2c92e 100644 --- a/crates/router/src/core/payments/types.rs +++ b/crates/router/src/core/payments/types.rs @@ -1,13 +1,18 @@ use std::{collections::HashMap, num::TryFromIntError}; use api_models::{payment_methods::SurchargeDetailsResponse, payments::RequestSurchargeDetails}; -use common_utils::{consts, types as common_types}; +use common_utils::{consts, errors::CustomResult, ext_traits::Encode, types as common_types}; use data_models::payments::payment_attempt::PaymentAttempt; use error_stack::{IntoReport, ResultExt}; +use redis_interface::errors::RedisError; +use router_env::{instrument, tracing}; use crate::{ + consts as router_consts, core::errors::{self, RouterResult}, + routes::AppState, types::{ + domain, storage::{self, enums as storage_enums}, transformers::ForeignTryFrom, }, @@ -215,8 +220,8 @@ impl ForeignTryFrom<(&SurchargeDetails, &PaymentAttempt)> for SurchargeDetailsRe let display_final_amount = currency.to_currency_base_unit_asf64(surcharge_details.final_amount)?; Ok(Self { - surcharge: surcharge_details.surcharge.clone(), - tax_on_surcharge: surcharge_details.tax_on_surcharge.clone(), + surcharge: surcharge_details.surcharge.clone().into(), + tax_on_surcharge: surcharge_details.tax_on_surcharge.clone().map(Into::into), display_surcharge_amount, display_tax_on_surcharge_amount, display_total_surcharge_amount: display_surcharge_amount @@ -239,16 +244,19 @@ impl SurchargeDetails { } } +#[derive(Eq, Hash, PartialEq, Clone, Debug, strum::Display)] +pub enum SurchargeKey { + Token(String), + PaymentMethodData( + common_enums::PaymentMethod, + common_enums::PaymentMethodType, + Option, + ), +} + #[derive(Clone, Debug)] pub struct SurchargeMetadata { - surcharge_results: HashMap< - ( - common_enums::PaymentMethod, - common_enums::PaymentMethodType, - Option, - ), - SurchargeDetails, - >, + surcharge_results: HashMap, pub payment_attempt_id: String, } @@ -267,30 +275,14 @@ impl SurchargeMetadata { } pub fn insert_surcharge_details( &mut self, - payment_method: &common_enums::PaymentMethod, - payment_method_type: &common_enums::PaymentMethodType, - card_network: Option<&common_enums::CardNetwork>, + surcharge_key: SurchargeKey, surcharge_details: SurchargeDetails, ) { - let key = ( - payment_method.to_owned(), - payment_method_type.to_owned(), - card_network.cloned(), - ); - self.surcharge_results.insert(key, surcharge_details); + self.surcharge_results + .insert(surcharge_key, surcharge_details); } - pub fn get_surcharge_details( - &self, - payment_method: &common_enums::PaymentMethod, - payment_method_type: &common_enums::PaymentMethodType, - card_network: Option<&common_enums::CardNetwork>, - ) -> Option<&SurchargeDetails> { - let key = &( - payment_method.to_owned(), - payment_method_type.to_owned(), - card_network.cloned(), - ); - self.surcharge_results.get(key) + pub fn get_surcharge_details(&self, surcharge_key: SurchargeKey) -> Option<&SurchargeDetails> { + self.surcharge_results.get(&surcharge_key) } pub fn get_surcharge_metadata_redis_key(payment_attempt_id: &str) -> String { format!("surcharge_metadata_{}", payment_attempt_id) @@ -298,25 +290,78 @@ impl SurchargeMetadata { pub fn get_individual_surcharge_key_value_pairs(&self) -> Vec<(String, SurchargeDetails)> { self.surcharge_results .iter() - .map(|((pm, pmt, card_network), surcharge_details)| { - let key = - Self::get_surcharge_details_redis_hashset_key(pm, pmt, card_network.as_ref()); + .map(|(surcharge_key, surcharge_details)| { + let key = Self::get_surcharge_details_redis_hashset_key(surcharge_key); (key, surcharge_details.to_owned()) }) .collect() } - pub fn get_surcharge_details_redis_hashset_key( - payment_method: &common_enums::PaymentMethod, - payment_method_type: &common_enums::PaymentMethodType, - card_network: Option<&common_enums::CardNetwork>, - ) -> String { - if let Some(card_network) = card_network { - format!( - "{}_{}_{}", - payment_method, payment_method_type, card_network - ) - } else { - format!("{}_{}", payment_method, payment_method_type) + pub fn get_surcharge_details_redis_hashset_key(surcharge_key: &SurchargeKey) -> String { + match surcharge_key { + SurchargeKey::Token(token) => { + format!("token_{}", token) + } + SurchargeKey::PaymentMethodData(payment_method, payment_method_type, card_network) => { + if let Some(card_network) = card_network { + format!( + "{}_{}_{}", + payment_method, payment_method_type, card_network + ) + } else { + format!("{}_{}", payment_method, payment_method_type) + } + } } } + #[instrument(skip_all)] + pub async fn persist_individual_surcharge_details_in_redis( + &self, + state: &AppState, + merchant_account: &domain::MerchantAccount, + ) -> RouterResult<()> { + if !self.is_empty_result() { + let redis_conn = state + .store + .get_redis_conn() + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed to get redis connection")?; + let redis_key = Self::get_surcharge_metadata_redis_key(&self.payment_attempt_id); + + let mut value_list = Vec::with_capacity(self.get_surcharge_results_size()); + for (key, value) in self.get_individual_surcharge_key_value_pairs().into_iter() { + value_list.push(( + key, + Encode::::encode_to_string_of_json(&value) + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed to encode to string of json")?, + )); + } + let intent_fulfillment_time = merchant_account + .intent_fulfillment_time + .unwrap_or(router_consts::DEFAULT_FULFILLMENT_TIME); + redis_conn + .set_hash_fields(&redis_key, value_list, Some(intent_fulfillment_time)) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed to write to redis")?; + } + Ok(()) + } + + #[instrument(skip_all)] + pub async fn get_individual_surcharge_detail_from_redis( + state: &AppState, + surcharge_key: SurchargeKey, + payment_attempt_id: &str, + ) -> CustomResult { + let redis_conn = state + .store + .get_redis_conn() + .attach_printable("Failed to get redis connection")?; + let redis_key = Self::get_surcharge_metadata_redis_key(payment_attempt_id); + let value_key = Self::get_surcharge_details_redis_hashset_key(&surcharge_key); + redis_conn + .get_hash_field_and_deserialize(&redis_key, &value_key, "SurchargeDetails") + .await + } } diff --git a/crates/router/src/core/utils.rs b/crates/router/src/core/utils.rs index 6d82c44d803a..724a698ff700 100644 --- a/crates/router/src/core/utils.rs +++ b/crates/router/src/core/utils.rs @@ -4,17 +4,12 @@ use api_models::enums::{DisputeStage, DisputeStatus}; use common_enums::RequestIncrementalAuthorization; #[cfg(feature = "payouts")] use common_utils::{crypto::Encryptable, pii::Email}; -use common_utils::{ - errors::CustomResult, - ext_traits::{AsyncExt, Encode}, -}; +use common_utils::{errors::CustomResult, ext_traits::AsyncExt}; use error_stack::{report, IntoReport, ResultExt}; -use euclid::enums as euclid_enums; -use redis_interface::errors::RedisError; use router_env::{instrument, tracing}; use uuid::Uuid; -use super::payments::{helpers, types as payments_types, PaymentAddress}; +use super::payments::{helpers, PaymentAddress}; #[cfg(feature = "payouts")] use super::payouts::PayoutData; #[cfg(feature = "payouts")] @@ -1068,71 +1063,6 @@ pub fn get_flow_name() -> RouterResult { .to_string()) } -#[instrument(skip_all)] -pub async fn persist_individual_surcharge_details_in_redis( - state: &AppState, - merchant_account: &domain::MerchantAccount, - surcharge_metadata: &payments_types::SurchargeMetadata, -) -> RouterResult<()> { - if !surcharge_metadata.is_empty_result() { - let redis_conn = state - .store - .get_redis_conn() - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Failed to get redis connection")?; - let redis_key = payments_types::SurchargeMetadata::get_surcharge_metadata_redis_key( - &surcharge_metadata.payment_attempt_id, - ); - - let mut value_list = Vec::with_capacity(surcharge_metadata.get_surcharge_results_size()); - for (key, value) in surcharge_metadata - .get_individual_surcharge_key_value_pairs() - .into_iter() - { - value_list.push(( - key, - Encode::::encode_to_string_of_json(&value) - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Failed to encode to string of json")?, - )); - } - let intent_fulfillment_time = merchant_account - .intent_fulfillment_time - .unwrap_or(consts::DEFAULT_FULFILLMENT_TIME); - redis_conn - .set_hash_fields(&redis_key, value_list, Some(intent_fulfillment_time)) - .await - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Failed to write to redis")?; - } - Ok(()) -} - -#[instrument(skip_all)] -pub async fn get_individual_surcharge_detail_from_redis( - state: &AppState, - payment_method: &euclid_enums::PaymentMethod, - payment_method_type: &euclid_enums::PaymentMethodType, - card_network: Option, - payment_attempt_id: &str, -) -> CustomResult { - let redis_conn = state - .store - .get_redis_conn() - .attach_printable("Failed to get redis connection")?; - let redis_key = - payments_types::SurchargeMetadata::get_surcharge_metadata_redis_key(payment_attempt_id); - let value_key = payments_types::SurchargeMetadata::get_surcharge_details_redis_hashset_key( - payment_method, - payment_method_type, - card_network.as_ref(), - ); - - redis_conn - .get_hash_field_and_deserialize(&redis_key, &value_key, "SurchargeDetails") - .await -} - pub fn get_request_incremental_authorization_value( request_incremental_authorization: Option, capture_method: Option, diff --git a/crates/router/src/openapi.rs b/crates/router/src/openapi.rs index 82c98304c62b..d83117c59d76 100644 --- a/crates/router/src/openapi.rs +++ b/crates/router/src/openapi.rs @@ -319,6 +319,9 @@ Never share your secret api keys. Keep them guarded and secure. api_models::payments::IncrementalAuthorizationResponse, api_models::payment_methods::RequiredFieldInfo, api_models::payment_methods::MaskedBankDetails, + api_models::payment_methods::SurchargeDetailsResponse, + api_models::payment_methods::SurchargeResponse, + api_models::payment_methods::SurchargePercentage, api_models::refunds::RefundListRequest, api_models::refunds::RefundListResponse, api_models::payments::TimeRange, @@ -363,7 +366,7 @@ Never share your secret api keys. Keep them guarded and secure. api_models::payments::PaymentLinkResponse, api_models::payments::RetrievePaymentLinkResponse, api_models::payments::PaymentLinkInitiateRequest, - api_models::payments::PaymentLinkObject + api_models::payments::PaymentLinkObject, )), modifiers(&SecurityAddon) )] diff --git a/crates/router/src/types/api.rs b/crates/router/src/types/api.rs index ea2ea8b701da..c74608ea20a1 100644 --- a/crates/router/src/types/api.rs +++ b/crates/router/src/types/api.rs @@ -238,7 +238,11 @@ impl SessionSurchargeDetails { ) -> Option { match self { Self::Calculated(surcharge_metadata) => surcharge_metadata - .get_surcharge_details(payment_method, payment_method_type, card_network) + .get_surcharge_details(payments_types::SurchargeKey::PaymentMethodData( + *payment_method, + *payment_method_type, + card_network.cloned(), + )) .cloned(), Self::PreDetermined(surcharge_details) => Some(surcharge_details.clone()), } diff --git a/openapi/openapi_spec.json b/openapi/openapi_spec.json index 4f26589ba029..d67089aea35f 100644 --- a/openapi/openapi_spec.json +++ b/openapi/openapi_spec.json @@ -5116,6 +5116,14 @@ ], "nullable": true }, + "surcharge_details": { + "allOf": [ + { + "$ref": "#/components/schemas/SurchargeDetailsResponse" + } + ], + "nullable": true + }, "requires_cvv": { "type": "boolean", "description": "Whether this payment method requires CVV to be collected", @@ -12043,6 +12051,106 @@ } } }, + "SurchargeDetailsResponse": { + "type": "object", + "required": [ + "surcharge", + "display_surcharge_amount", + "display_tax_on_surcharge_amount", + "display_total_surcharge_amount", + "display_final_amount" + ], + "properties": { + "surcharge": { + "$ref": "#/components/schemas/SurchargeResponse" + }, + "tax_on_surcharge": { + "allOf": [ + { + "$ref": "#/components/schemas/SurchargePercentage" + } + ], + "nullable": true + }, + "display_surcharge_amount": { + "type": "number", + "format": "double", + "description": "surcharge amount for this payment" + }, + "display_tax_on_surcharge_amount": { + "type": "number", + "format": "double", + "description": "tax on surcharge amount for this payment" + }, + "display_total_surcharge_amount": { + "type": "number", + "format": "double", + "description": "sum of display_surcharge_amount and display_tax_on_surcharge_amount" + }, + "display_final_amount": { + "type": "number", + "format": "double", + "description": "sum of original amount," + } + } + }, + "SurchargePercentage": { + "type": "object", + "required": [ + "percentage" + ], + "properties": { + "percentage": { + "type": "number", + "format": "float" + } + } + }, + "SurchargeResponse": { + "oneOf": [ + { + "type": "object", + "required": [ + "type", + "value" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "fixed" + ] + }, + "value": { + "type": "integer", + "format": "int64", + "description": "Fixed Surcharge value" + } + } + }, + { + "type": "object", + "required": [ + "type", + "value" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "rate" + ] + }, + "value": { + "$ref": "#/components/schemas/SurchargePercentage" + } + } + } + ], + "discriminator": { + "propertyName": "type" + } + }, "SwishQrData": { "type": "object" }, From 298e3627c379de5acfcafb074036754661801f1e Mon Sep 17 00:00:00 2001 From: SamraatBansal <55536657+SamraatBansal@users.noreply.github.com> Date: Tue, 5 Dec 2023 01:47:55 +0530 Subject: [PATCH 2/5] fix: transform connector name to lowercase in connector integration script (#3048) --- scripts/add_connector.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/add_connector.sh b/scripts/add_connector.sh index 7ed5e65151e1..1246c51d8eb3 100755 --- a/scripts/add_connector.sh +++ b/scripts/add_connector.sh @@ -25,7 +25,7 @@ function find_prev_connector() { eval "$2='aci'" } -payment_gateway=$1; +payment_gateway=$(echo $1 | tr '[:upper:]' '[:lower:]') base_url=$2; payment_gateway_camelcase="$(tr '[:lower:]' '[:upper:]' <<< ${payment_gateway:0:1})${payment_gateway:1}" src="crates/router/src" @@ -49,7 +49,7 @@ git checkout $conn.rs $src/types/api.rs $src/configs/settings.rs config/developm # Add enum for this connector in required places previous_connector='' -find_prev_connector $1 previous_connector +find_prev_connector $payment_gateway previous_connector previous_connector_camelcase="$(tr '[:lower:]' '[:upper:]' <<< ${previous_connector:0:1})${previous_connector:1}" sed -i'' -e "s|pub mod $previous_connector;|pub mod $previous_connector;\npub mod ${payment_gateway};|" $conn.rs sed -i'' -e "s/};/${payment_gateway}::${payment_gateway_camelcase},\n};/" $conn.rs From ba392f58b2956d67e93a08853bcf2270a869be27 Mon Sep 17 00:00:00 2001 From: Kartikeya Hegde Date: Tue, 5 Dec 2023 12:47:37 +0530 Subject: [PATCH 3/5] fix: add fallback to reverselookup error (#3025) Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com> --- Cargo.lock | 1 + crates/common_utils/src/lib.rs | 1 + crates/common_utils/src/macros.rs | 92 +++++++++++ crates/data_models/Cargo.toml | 1 + crates/data_models/src/errors.rs | 4 +- crates/diesel_models/src/errors.rs | 16 +- crates/router/src/db/refund.rs | 147 +++++++++--------- crates/router/src/db/user/sample_data.rs | 8 +- crates/router/src/lib.rs | 1 + crates/router/src/macros.rs | 72 +-------- crates/storage_impl/src/errors.rs | 10 +- crates/storage_impl/src/lib.rs | 10 +- .../src/payments/payment_attempt.rs | 125 +++++++++------ .../src/payments/payment_intent.rs | 27 ++-- 14 files changed, 288 insertions(+), 227 deletions(-) create mode 100644 crates/common_utils/src/macros.rs diff --git a/Cargo.lock b/Cargo.lock index 4231c62d9499..cb38c0b70b59 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2052,6 +2052,7 @@ dependencies = [ "async-trait", "common_enums", "common_utils", + "diesel_models", "error-stack", "masking", "serde", diff --git a/crates/common_utils/src/lib.rs b/crates/common_utils/src/lib.rs index 62428dccfb6a..0ac8e886bc06 100644 --- a/crates/common_utils/src/lib.rs +++ b/crates/common_utils/src/lib.rs @@ -10,6 +10,7 @@ pub mod errors; pub mod events; pub mod ext_traits; pub mod fp_utils; +pub mod macros; pub mod pii; #[allow(missing_docs)] // Todo: add docs pub mod request; diff --git a/crates/common_utils/src/macros.rs b/crates/common_utils/src/macros.rs new file mode 100644 index 000000000000..9d41569384f1 --- /dev/null +++ b/crates/common_utils/src/macros.rs @@ -0,0 +1,92 @@ +#![allow(missing_docs)] + +#[macro_export] +macro_rules! newtype_impl { + ($is_pub:vis, $name:ident, $ty_path:path) => { + impl std::ops::Deref for $name { + type Target = $ty_path; + + fn deref(&self) -> &Self::Target { + &self.0 + } + } + + impl std::ops::DerefMut for $name { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } + } + + impl From<$ty_path> for $name { + fn from(ty: $ty_path) -> Self { + Self(ty) + } + } + + impl $name { + pub fn into_inner(self) -> $ty_path { + self.0 + } + } + }; +} + +#[macro_export] +macro_rules! newtype { + ($is_pub:vis $name:ident = $ty_path:path) => { + $is_pub struct $name(pub $ty_path); + + $crate::newtype_impl!($is_pub, $name, $ty_path); + }; + + ($is_pub:vis $name:ident = $ty_path:path, derives = ($($trt:path),*)) => { + #[derive($($trt),*)] + $is_pub struct $name(pub $ty_path); + + $crate::newtype_impl!($is_pub, $name, $ty_path); + }; +} + +#[macro_export] +macro_rules! async_spawn { + ($t:block) => { + tokio::spawn(async move { $t }); + }; +} + +#[macro_export] +macro_rules! fallback_reverse_lookup_not_found { + ($a:expr,$b:expr) => { + match $a { + Ok(res) => res, + Err(err) => { + router_env::logger::error!(reverse_lookup_fallback = %err); + match err.current_context() { + errors::StorageError::ValueNotFound(_) => return $b, + errors::StorageError::DatabaseError(data_err) => { + match data_err.current_context() { + diesel_models::errors::DatabaseError::NotFound => return $b, + _ => return Err(err) + } + } + _=> return Err(err) + } + } + }; + }; +} + +#[macro_export] +macro_rules! collect_missing_value_keys { + [$(($key:literal, $option:expr)),+] => { + { + let mut keys: Vec<&'static str> = Vec::new(); + $( + if $option.is_none() { + keys.push($key); + } + )* + keys + } + }; +} diff --git a/crates/data_models/Cargo.toml b/crates/data_models/Cargo.toml index 857d53b6999e..a86dc3070b4d 100644 --- a/crates/data_models/Cargo.toml +++ b/crates/data_models/Cargo.toml @@ -17,6 +17,7 @@ api_models = { version = "0.1.0", path = "../api_models" } common_enums = { version = "0.1.0", path = "../common_enums" } common_utils = { version = "0.1.0", path = "../common_utils" } masking = { version = "0.1.0", path = "../masking" } +diesel_models = { version = "0.1.0", path = "../diesel_models", features = ["kv_store"] } # Third party deps async-trait = "0.1.68" diff --git a/crates/data_models/src/errors.rs b/crates/data_models/src/errors.rs index 4f8229ea0c9b..9616a3a944ca 100644 --- a/crates/data_models/src/errors.rs +++ b/crates/data_models/src/errors.rs @@ -1,3 +1,5 @@ +use diesel_models::errors::DatabaseError; + pub type StorageResult = error_stack::Result; #[derive(Debug, thiserror::Error)] @@ -6,7 +8,7 @@ pub enum StorageError { InitializationError, // TODO: deprecate this error type to use a domain error instead #[error("DatabaseError: {0:?}")] - DatabaseError(String), + DatabaseError(error_stack::Report), #[error("ValueNotFound: {0}")] ValueNotFound(String), #[error("DuplicateValue: {entity} already exists {key:?}")] diff --git a/crates/diesel_models/src/errors.rs b/crates/diesel_models/src/errors.rs index 0a8422131ae2..4a536aad07e4 100644 --- a/crates/diesel_models/src/errors.rs +++ b/crates/diesel_models/src/errors.rs @@ -1,4 +1,4 @@ -#[derive(Debug, thiserror::Error)] +#[derive(Copy, Clone, Debug, thiserror::Error)] pub enum DatabaseError { #[error("An error occurred when obtaining database connection")] DatabaseConnectionError, @@ -14,3 +14,17 @@ pub enum DatabaseError { #[error("An unknown error occurred")] Others, } + +impl From for DatabaseError { + fn from(error: diesel::result::Error) -> Self { + match error { + diesel::result::Error::DatabaseError( + diesel::result::DatabaseErrorKind::UniqueViolation, + _, + ) => Self::UniqueViolation, + diesel::result::Error::NotFound => Self::NotFound, + diesel::result::Error::QueryBuilderError(_) => Self::QueryGenerationFailed, + _ => Self::Others, + } + } +} diff --git a/crates/router/src/db/refund.rs b/crates/router/src/db/refund.rs index 8ac8bd106eff..f385e1bc5a83 100644 --- a/crates/router/src/db/refund.rs +++ b/crates/router/src/db/refund.rs @@ -267,7 +267,7 @@ mod storage { #[cfg(feature = "kv_store")] mod storage { - use common_utils::date_time; + use common_utils::{date_time, fallback_reverse_lookup_not_found}; use error_stack::{IntoReport, ResultExt}; use redis_interface::HsetnxReply; use storage_impl::redis::kv_store::{kv_wrapper, KvOperation}; @@ -277,7 +277,6 @@ mod storage { connection, core::errors::{self, CustomResult}, db::reverse_lookup::ReverseLookupInterface, - logger, services::Store, types::storage::{self as storage_types, enums, kv}, utils::{self, db_utils}, @@ -304,10 +303,12 @@ mod storage { match storage_scheme { enums::MerchantStorageScheme::PostgresOnly => database_call().await, enums::MerchantStorageScheme::RedisKv => { - let lookup_id = format!("{merchant_id}_{internal_reference_id}"); - let lookup = self - .get_lookup_by_lookup_id(&lookup_id, storage_scheme) - .await?; + let lookup_id = format!("ref_inter_ref_{merchant_id}_{internal_reference_id}"); + let lookup = fallback_reverse_lookup_not_found!( + self.get_lookup_by_lookup_id(&lookup_id, storage_scheme) + .await, + database_call().await + ); let key = &lookup.pk_id; Box::pin(db_utils::try_redis_get_else_try_database_get( @@ -382,6 +383,50 @@ mod storage { }, }; + let mut reverse_lookups = vec![ + storage_types::ReverseLookupNew { + sk_id: field.clone(), + lookup_id: format!( + "ref_ref_id_{}_{}", + created_refund.merchant_id, created_refund.refund_id + ), + pk_id: key.clone(), + source: "refund".to_string(), + updated_by: storage_scheme.to_string(), + }, + // [#492]: A discussion is required on whether this is required? + storage_types::ReverseLookupNew { + sk_id: field.clone(), + lookup_id: format!( + "ref_inter_ref_{}_{}", + created_refund.merchant_id, created_refund.internal_reference_id + ), + pk_id: key.clone(), + source: "refund".to_string(), + updated_by: storage_scheme.to_string(), + }, + ]; + if let Some(connector_refund_id) = created_refund.to_owned().connector_refund_id + { + reverse_lookups.push(storage_types::ReverseLookupNew { + sk_id: field.clone(), + lookup_id: format!( + "ref_connector_{}_{}_{}", + created_refund.merchant_id, + connector_refund_id, + created_refund.connector + ), + pk_id: key.clone(), + source: "refund".to_string(), + updated_by: storage_scheme.to_string(), + }) + }; + let rev_look = reverse_lookups + .into_iter() + .map(|rev| self.insert_reverse_lookup(rev, storage_scheme)); + + futures::future::try_join_all(rev_look).await?; + match kv_wrapper::( self, KvOperation::::HSetNx( @@ -400,55 +445,7 @@ mod storage { key: Some(created_refund.refund_id), }) .into_report(), - Ok(HsetnxReply::KeySet) => { - let mut reverse_lookups = vec![ - storage_types::ReverseLookupNew { - sk_id: field.clone(), - lookup_id: format!( - "{}_{}", - created_refund.merchant_id, created_refund.refund_id - ), - pk_id: key.clone(), - source: "refund".to_string(), - updated_by: storage_scheme.to_string(), - }, - // [#492]: A discussion is required on whether this is required? - storage_types::ReverseLookupNew { - sk_id: field.clone(), - lookup_id: format!( - "{}_{}", - created_refund.merchant_id, - created_refund.internal_reference_id - ), - pk_id: key.clone(), - source: "refund".to_string(), - updated_by: storage_scheme.to_string(), - }, - ]; - if let Some(connector_refund_id) = - created_refund.to_owned().connector_refund_id - { - reverse_lookups.push(storage_types::ReverseLookupNew { - sk_id: field.clone(), - lookup_id: format!( - "{}_{}_{}", - created_refund.merchant_id, - connector_refund_id, - created_refund.connector - ), - pk_id: key, - source: "refund".to_string(), - updated_by: storage_scheme.to_string(), - }) - }; - let rev_look = reverse_lookups - .into_iter() - .map(|rev| self.insert_reverse_lookup(rev, storage_scheme)); - - futures::future::try_join_all(rev_look).await?; - - Ok(created_refund) - } + Ok(HsetnxReply::KeySet) => Ok(created_refund), Err(er) => Err(er).change_context(errors::StorageError::KVError), } } @@ -475,17 +472,14 @@ mod storage { match storage_scheme { enums::MerchantStorageScheme::PostgresOnly => database_call().await, enums::MerchantStorageScheme::RedisKv => { - let lookup_id = format!("{merchant_id}_{connector_transaction_id}"); - let lookup = match self - .get_lookup_by_lookup_id(&lookup_id, storage_scheme) - .await - { - Ok(l) => l, - Err(err) => { - logger::error!(?err); - return Ok(vec![]); - } - }; + let lookup_id = + format!("pa_conn_trans_{merchant_id}_{connector_transaction_id}"); + let lookup = fallback_reverse_lookup_not_found!( + self.get_lookup_by_lookup_id(&lookup_id, storage_scheme) + .await, + database_call().await + ); + let key = &lookup.pk_id; let pattern = db_utils::generate_hscan_pattern_for_refund(&lookup.sk_id); @@ -575,10 +569,12 @@ mod storage { match storage_scheme { enums::MerchantStorageScheme::PostgresOnly => database_call().await, enums::MerchantStorageScheme::RedisKv => { - let lookup_id = format!("{merchant_id}_{refund_id}"); - let lookup = self - .get_lookup_by_lookup_id(&lookup_id, storage_scheme) - .await?; + let lookup_id = format!("ref_ref_id_{merchant_id}_{refund_id}"); + let lookup = fallback_reverse_lookup_not_found!( + self.get_lookup_by_lookup_id(&lookup_id, storage_scheme) + .await, + database_call().await + ); let key = &lookup.pk_id; Box::pin(db_utils::try_redis_get_else_try_database_get( @@ -620,10 +616,13 @@ mod storage { match storage_scheme { enums::MerchantStorageScheme::PostgresOnly => database_call().await, enums::MerchantStorageScheme::RedisKv => { - let lookup_id = format!("{merchant_id}_{connector_refund_id}_{connector}"); - let lookup = self - .get_lookup_by_lookup_id(&lookup_id, storage_scheme) - .await?; + let lookup_id = + format!("ref_connector_{merchant_id}_{connector_refund_id}_{connector}"); + let lookup = fallback_reverse_lookup_not_found!( + self.get_lookup_by_lookup_id(&lookup_id, storage_scheme) + .await, + database_call().await + ); let key = &lookup.pk_id; Box::pin(db_utils::try_redis_get_else_try_database_get( diff --git a/crates/router/src/db/user/sample_data.rs b/crates/router/src/db/user/sample_data.rs index 11def9026854..ae98332cfc49 100644 --- a/crates/router/src/db/user/sample_data.rs +++ b/crates/router/src/db/user/sample_data.rs @@ -193,13 +193,7 @@ fn diesel_error_to_data_error(diesel_error: Report) -> Report { - StorageError::DatabaseError("No fields to update".to_string()) - } - DatabaseError::QueryGenerationFailed => { - StorageError::DatabaseError("Query generation failed".to_string()) - } - DatabaseError::Others => StorageError::DatabaseError("Others".to_string()), + err => StorageError::DatabaseError(error_stack::report!(*err)), }; diesel_error.change_context(new_err) } diff --git a/crates/router/src/lib.rs b/crates/router/src/lib.rs index 035314f71dfb..fb8be9636748 100644 --- a/crates/router/src/lib.rs +++ b/crates/router/src/lib.rs @@ -35,6 +35,7 @@ use storage_impl::errors::ApplicationResult; use tokio::sync::{mpsc, oneshot}; pub use self::env::logger; +pub(crate) use self::macros::*; use crate::{configs::settings, core::errors}; #[cfg(feature = "mimalloc")] diff --git a/crates/router/src/macros.rs b/crates/router/src/macros.rs index 33ed43fcc7ab..e6c9dba7d6e2 100644 --- a/crates/router/src/macros.rs +++ b/crates/router/src/macros.rs @@ -1,68 +1,4 @@ -#[macro_export] -macro_rules! newtype_impl { - ($is_pub:vis, $name:ident, $ty_path:path) => { - impl std::ops::Deref for $name { - type Target = $ty_path; - - fn deref(&self) -> &Self::Target { - &self.0 - } - } - - impl std::ops::DerefMut for $name { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.0 - } - } - - impl From<$ty_path> for $name { - fn from(ty: $ty_path) -> Self { - Self(ty) - } - } - - impl $name { - pub fn into_inner(self) -> $ty_path { - self.0 - } - } - }; -} - -#[macro_export] -macro_rules! newtype { - ($is_pub:vis $name:ident = $ty_path:path) => { - $is_pub struct $name(pub $ty_path); - - $crate::newtype_impl!($is_pub, $name, $ty_path); - }; - - ($is_pub:vis $name:ident = $ty_path:path, derives = ($($trt:path),*)) => { - #[derive($($trt),*)] - $is_pub struct $name(pub $ty_path); - - $crate::newtype_impl!($is_pub, $name, $ty_path); - }; -} - -#[macro_export] -macro_rules! async_spawn { - ($t:block) => { - tokio::spawn(async move { $t }); - }; -} - -#[macro_export] -macro_rules! collect_missing_value_keys { - [$(($key:literal, $option:expr)),+] => { - { - let mut keys: Vec<&'static str> = Vec::new(); - $( - if $option.is_none() { - keys.push($key); - } - )* - keys - } - }; -} +pub use common_utils::{ + async_spawn, collect_missing_value_keys, fallback_reverse_lookup_not_found, newtype, + newtype_impl, +}; diff --git a/crates/storage_impl/src/errors.rs b/crates/storage_impl/src/errors.rs index bc68986cb8ea..105a93d4beae 100644 --- a/crates/storage_impl/src/errors.rs +++ b/crates/storage_impl/src/errors.rs @@ -92,15 +92,7 @@ impl Into for &StorageError { key: None, } } - storage_errors::DatabaseError::NoFieldsToUpdate => { - DataStorageError::DatabaseError("No fields to update".to_string()) - } - storage_errors::DatabaseError::QueryGenerationFailed => { - DataStorageError::DatabaseError("Query generation failed".to_string()) - } - storage_errors::DatabaseError::Others => { - DataStorageError::DatabaseError("Unknown database error".to_string()) - } + err => DataStorageError::DatabaseError(error_stack::report!(*err)), }, StorageError::ValueNotFound(i) => DataStorageError::ValueNotFound(i.clone()), StorageError::DuplicateValue { entity, key } => DataStorageError::DuplicateValue { diff --git a/crates/storage_impl/src/lib.rs b/crates/storage_impl/src/lib.rs index dc0dea4bb59c..7e2c7f2fc3c5 100644 --- a/crates/storage_impl/src/lib.rs +++ b/crates/storage_impl/src/lib.rs @@ -251,14 +251,6 @@ pub(crate) fn diesel_error_to_data_error( entity: "entity ", key: None, }, - diesel_models::errors::DatabaseError::NoFieldsToUpdate => { - StorageError::DatabaseError("No fields to update".to_string()) - } - diesel_models::errors::DatabaseError::QueryGenerationFailed => { - StorageError::DatabaseError("Query generation failed".to_string()) - } - diesel_models::errors::DatabaseError::Others => { - StorageError::DatabaseError("Others".to_string()) - } + _ => StorageError::DatabaseError(error_stack::report!(*diesel_error)), } } diff --git a/crates/storage_impl/src/payments/payment_attempt.rs b/crates/storage_impl/src/payments/payment_attempt.rs index 9f351979f289..b524ff1aaa71 100644 --- a/crates/storage_impl/src/payments/payment_attempt.rs +++ b/crates/storage_impl/src/payments/payment_attempt.rs @@ -1,5 +1,5 @@ use api_models::enums::{AuthenticationType, Connector, PaymentMethod, PaymentMethodType}; -use common_utils::errors::CustomResult; +use common_utils::{errors::CustomResult, fallback_reverse_lookup_not_found}; use data_models::{ errors, mandates::{MandateAmountData, MandateDataType}, @@ -399,6 +399,20 @@ impl PaymentAttemptInterface for KVRouterStore { }, }; + //Reverse lookup for attempt_id + let reverse_lookup = ReverseLookupNew { + lookup_id: format!( + "pa_{}_{}", + &created_attempt.merchant_id, &created_attempt.attempt_id, + ), + pk_id: key.clone(), + sk_id: field.clone(), + source: "payment_attempt".to_string(), + updated_by: storage_scheme.to_string(), + }; + self.insert_reverse_lookup(reverse_lookup, storage_scheme) + .await?; + match kv_wrapper::( self, KvOperation::HSetNx( @@ -417,23 +431,7 @@ impl PaymentAttemptInterface for KVRouterStore { key: Some(key), }) .into_report(), - Ok(HsetnxReply::KeySet) => { - //Reverse lookup for attempt_id - let reverse_lookup = ReverseLookupNew { - lookup_id: format!( - "{}_{}", - &created_attempt.merchant_id, &created_attempt.attempt_id, - ), - pk_id: key, - sk_id: field, - source: "payment_attempt".to_string(), - updated_by: storage_scheme.to_string(), - }; - self.insert_reverse_lookup(reverse_lookup, storage_scheme) - .await?; - - Ok(created_attempt) - } + Ok(HsetnxReply::KeySet) => Ok(created_attempt), Err(error) => Err(error.change_context(errors::StorageError::KVError)), } } @@ -480,16 +478,6 @@ impl PaymentAttemptInterface for KVRouterStore { }, }; - kv_wrapper::<(), _, _>( - self, - KvOperation::Hset::((&field, redis_value), redis_entry), - &key, - ) - .await - .change_context(errors::StorageError::KVError)? - .try_into_hset() - .change_context(errors::StorageError::KVError)?; - match ( old_connector_transaction_id, &updated_attempt.connector_transaction_id, @@ -549,6 +537,16 @@ impl PaymentAttemptInterface for KVRouterStore { (_, _) => {} } + kv_wrapper::<(), _, _>( + self, + KvOperation::Hset::((&field, redis_value), redis_entry), + &key, + ) + .await + .change_context(errors::StorageError::KVError)? + .try_into_hset() + .change_context(errors::StorageError::KVError)?; + Ok(updated_attempt) } } @@ -574,10 +572,20 @@ impl PaymentAttemptInterface for KVRouterStore { } MerchantStorageScheme::RedisKv => { // We assume that PaymentAttempt <=> PaymentIntent is a one-to-one relation for now - let lookup_id = format!("conn_trans_{merchant_id}_{connector_transaction_id}"); - let lookup = self - .get_lookup_by_lookup_id(&lookup_id, storage_scheme) - .await?; + let lookup_id = format!("pa_conn_trans_{merchant_id}_{connector_transaction_id}"); + let lookup = fallback_reverse_lookup_not_found!( + self.get_lookup_by_lookup_id(&lookup_id, storage_scheme) + .await, + self.router_store + .find_payment_attempt_by_connector_transaction_id_payment_id_merchant_id( + connector_transaction_id, + payment_id, + merchant_id, + storage_scheme, + ) + .await + ); + let key = &lookup.pk_id; Box::pin(try_redis_get_else_try_database_get( @@ -707,10 +715,18 @@ impl PaymentAttemptInterface for KVRouterStore { .await } MerchantStorageScheme::RedisKv => { - let lookup_id = format!("{merchant_id}_{connector_txn_id}"); - let lookup = self - .get_lookup_by_lookup_id(&lookup_id, storage_scheme) - .await?; + let lookup_id = format!("pa_conn_trans_{merchant_id}_{connector_txn_id}"); + let lookup = fallback_reverse_lookup_not_found!( + self.get_lookup_by_lookup_id(&lookup_id, storage_scheme) + .await, + self.router_store + .find_payment_attempt_by_merchant_id_connector_txn_id( + merchant_id, + connector_txn_id, + storage_scheme, + ) + .await + ); let key = &lookup.pk_id; Box::pin(try_redis_get_else_try_database_get( @@ -799,10 +815,19 @@ impl PaymentAttemptInterface for KVRouterStore { .await } MerchantStorageScheme::RedisKv => { - let lookup_id = format!("{merchant_id}_{attempt_id}"); - let lookup = self - .get_lookup_by_lookup_id(&lookup_id, storage_scheme) - .await?; + let lookup_id = format!("pa_{merchant_id}_{attempt_id}"); + let lookup = fallback_reverse_lookup_not_found!( + self.get_lookup_by_lookup_id(&lookup_id, storage_scheme) + .await, + self.router_store + .find_payment_attempt_by_attempt_id_merchant_id( + attempt_id, + merchant_id, + storage_scheme, + ) + .await + ); + let key = &lookup.pk_id; Box::pin(try_redis_get_else_try_database_get( async { @@ -846,10 +871,18 @@ impl PaymentAttemptInterface for KVRouterStore { .await } MerchantStorageScheme::RedisKv => { - let lookup_id = format!("preprocessing_{merchant_id}_{preprocessing_id}"); - let lookup = self - .get_lookup_by_lookup_id(&lookup_id, storage_scheme) - .await?; + let lookup_id = format!("pa_preprocessing_{merchant_id}_{preprocessing_id}"); + let lookup = fallback_reverse_lookup_not_found!( + self.get_lookup_by_lookup_id(&lookup_id, storage_scheme) + .await, + self.router_store + .find_payment_attempt_by_preprocessing_id_merchant_id( + preprocessing_id, + merchant_id, + storage_scheme, + ) + .await + ); let key = &lookup.pk_id; Box::pin(try_redis_get_else_try_database_get( @@ -1757,7 +1790,7 @@ async fn add_connector_txn_id_to_reverse_lookup( ) -> CustomResult { let field = format!("pa_{}", updated_attempt_attempt_id); let reverse_lookup_new = ReverseLookupNew { - lookup_id: format!("conn_trans_{}_{}", merchant_id, connector_transaction_id), + lookup_id: format!("pa_conn_trans_{}_{}", merchant_id, connector_transaction_id), pk_id: key.to_owned(), sk_id: field.clone(), source: "payment_attempt".to_string(), @@ -1779,7 +1812,7 @@ async fn add_preprocessing_id_to_reverse_lookup( ) -> CustomResult { let field = format!("pa_{}", updated_attempt_attempt_id); let reverse_lookup_new = ReverseLookupNew { - lookup_id: format!("preprocessing_{}_{}", merchant_id, preprocessing_id), + lookup_id: format!("pa_preprocessing_{}_{}", merchant_id, preprocessing_id), pk_id: key.to_owned(), sk_id: field.clone(), source: "payment_attempt".to_string(), diff --git a/crates/storage_impl/src/payments/payment_intent.rs b/crates/storage_impl/src/payments/payment_intent.rs index 90bb21190c39..3e695947b8bf 100644 --- a/crates/storage_impl/src/payments/payment_intent.rs +++ b/crates/storage_impl/src/payments/payment_intent.rs @@ -494,12 +494,13 @@ impl PaymentIntentInterface for crate::RouterStore { .map(PaymentIntent::from_storage_model) .collect::>() }) - .into_report() .map_err(|er| { - let new_err = StorageError::DatabaseError(format!("{er:?}")); - er.change_context(new_err) + StorageError::DatabaseError( + error_stack::report!(diesel_models::errors::DatabaseError::from(er)) + .attach_printable("Error filtering payment records"), + ) }) - .attach_printable_lazy(|| "Error filtering records by predicate") + .into_report() } #[cfg(feature = "olap")] @@ -646,12 +647,13 @@ impl PaymentIntentInterface for crate::RouterStore { }) .collect() }) - .into_report() .map_err(|er| { - let new_er = StorageError::DatabaseError(format!("{er:?}")); - er.change_context(new_er) + StorageError::DatabaseError( + error_stack::report!(diesel_models::errors::DatabaseError::from(er)) + .attach_printable("Error filtering payment records"), + ) }) - .attach_printable("Error filtering payment records") + .into_report() } #[cfg(feature = "olap")] @@ -712,12 +714,13 @@ impl PaymentIntentInterface for crate::RouterStore { db_metrics::DatabaseOperation::Filter, ) .await - .into_report() .map_err(|er| { - let new_err = StorageError::DatabaseError(format!("{er:?}")); - er.change_context(new_err) + StorageError::DatabaseError( + error_stack::report!(diesel_models::errors::DatabaseError::from(er)) + .attach_printable("Error filtering payment records"), + ) }) - .attach_printable_lazy(|| "Error filtering records by predicate") + .into_report() } } From 6e09bc9e2c4bbe14dcb70da4a438850b03b3254c Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 5 Dec 2023 07:48:22 +0000 Subject: [PATCH 4/5] test(postman): update postman collection files --- postman/collection-json/adyen_uk.postman_collection.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/postman/collection-json/adyen_uk.postman_collection.json b/postman/collection-json/adyen_uk.postman_collection.json index 400f04241c27..04a7e39f15e7 100644 --- a/postman/collection-json/adyen_uk.postman_collection.json +++ b/postman/collection-json/adyen_uk.postman_collection.json @@ -13959,10 +13959,10 @@ "// Response body should have value \"invalid_request\" for \"error type\"", "if (jsonData?.error?.message) {", " pm.test(", - " \"[POST]::/payments - Content check if value for 'error.message' matches 'The payment has not succeeded yet. Please pass a successful payment to initiate refund'\",", + " \"[POST]::/payments - Content check if value for 'error.message' matches 'This Payment could not be refund because it has a status of requires_confirmation. The expected state is succeeded, partially_captured'\",", " function () {", " pm.expect(jsonData.error.message).to.eql(", - " \"The payment has not succeeded yet. Please pass a successful payment to initiate refund\",", + " \"This Payment could not be refund because it has a status of requires_confirmation. The expected state is succeeded, partially_captured\",", " );", " },", " );", From 5b62731399c8d5b8bfd923fb61706b3a9c4f5ffe Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 5 Dec 2023 07:48:22 +0000 Subject: [PATCH 5/5] chore(version): v1.95.0 --- CHANGELOG.md | 39 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3bbdac921fd7..3cd968293c4b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,45 @@ All notable changes to HyperSwitch will be documented here. - - - +## 1.95.0 (2023-12-05) + +### Features + +- **connector:** [BOA/CYBERSOURCE] Fix Status Mapping for Terminal St… ([#3031](https://github.com/juspay/hyperswitch/pull/3031)) ([`95876b0`](https://github.com/juspay/hyperswitch/commit/95876b0ce03e024edf77909502c53eb4e63a9855)) +- **pm_list:** Add required field for open_banking_uk for Adyen and Volt Connector ([#3032](https://github.com/juspay/hyperswitch/pull/3032)) ([`9d93533`](https://github.com/juspay/hyperswitch/commit/9d935332193dcc9f191a0a5a9e7405316794a418)) +- **router:** + - Add key_value to locker metrics ([#2995](https://github.com/juspay/hyperswitch/pull/2995)) ([`83fcd1a`](https://github.com/juspay/hyperswitch/commit/83fcd1a9deb106a44c8262923c7f1660b0c46bf2)) + - Add payments incremental authorization api ([#3038](https://github.com/juspay/hyperswitch/pull/3038)) ([`a0cfdd3`](https://github.com/juspay/hyperswitch/commit/a0cfdd3fb12f04b603f65551eac985c31e08da85)) +- **types:** Add email types for sending emails ([#3020](https://github.com/juspay/hyperswitch/pull/3020)) ([`c4bd47e`](https://github.com/juspay/hyperswitch/commit/c4bd47eca93a158c9daeeeb18afb1e735eea8c94)) +- **user:** + - Generate and delete sample data ([#2987](https://github.com/juspay/hyperswitch/pull/2987)) ([`092ec73`](https://github.com/juspay/hyperswitch/commit/092ec73b3c65ce6048d379383b078d643f0f35fc)) + - Add user_list and switch_list apis ([#3033](https://github.com/juspay/hyperswitch/pull/3033)) ([`ec15ddd`](https://github.com/juspay/hyperswitch/commit/ec15ddd0d0ed942fedec525406df3005d494b8d4)) +- Calculate surcharge for customer saved card list ([#3039](https://github.com/juspay/hyperswitch/pull/3039)) ([`daf0f09`](https://github.com/juspay/hyperswitch/commit/daf0f09f8e3293ee6a3599a25362d9171fc5b2e7)) + +### Bug Fixes + +- **connector:** [Paypal] Parse response for Cards with no 3DS check ([#3021](https://github.com/juspay/hyperswitch/pull/3021)) ([`d883cd1`](https://github.com/juspay/hyperswitch/commit/d883cd18972c5f9e8350e9a3f4e5cd56ec2c0787)) +- **pm_list:** [Trustpay]Update dynamic fields for trustpay blik ([#3042](https://github.com/juspay/hyperswitch/pull/3042)) ([`9274cef`](https://github.com/juspay/hyperswitch/commit/9274cefbdd29d2ac64baeea2fe504dff2472cb47)) +- **wasm:** Fix wasm function to return the categories for keys with their description respectively ([#3023](https://github.com/juspay/hyperswitch/pull/3023)) ([`2ac5b2c`](https://github.com/juspay/hyperswitch/commit/2ac5b2cd764c0aad53ac7c672dfcc9132fa5668f)) +- Use card bin to get additional card details ([#3036](https://github.com/juspay/hyperswitch/pull/3036)) ([`6c7d3a2`](https://github.com/juspay/hyperswitch/commit/6c7d3a2e8a047ff23b52b76792fe8f28d3b952a4)) +- Transform connector name to lowercase in connector integration script ([#3048](https://github.com/juspay/hyperswitch/pull/3048)) ([`298e362`](https://github.com/juspay/hyperswitch/commit/298e3627c379de5acfcafb074036754661801f1e)) +- Add fallback to reverselookup error ([#3025](https://github.com/juspay/hyperswitch/pull/3025)) ([`ba392f5`](https://github.com/juspay/hyperswitch/commit/ba392f58b2956d67e93a08853bcf2270a869be27)) + +### Refactors + +- **payment_methods:** Add support for passing card_cvc in payment_method_data object along with token ([#3024](https://github.com/juspay/hyperswitch/pull/3024)) ([`3ce04ab`](https://github.com/juspay/hyperswitch/commit/3ce04abae4eddfa27025368f5ef28987cccea43d)) +- **users:** Separate signup and signin ([#2921](https://github.com/juspay/hyperswitch/pull/2921)) ([`80efeb7`](https://github.com/juspay/hyperswitch/commit/80efeb76b1801529766978af1c06e2d2c7de66c0)) +- Create separate struct for surcharge details response ([#3027](https://github.com/juspay/hyperswitch/pull/3027)) ([`57591f8`](https://github.com/juspay/hyperswitch/commit/57591f819c7994099e76cff1affc7bcf3e45a031)) + +### Testing + +- **postman:** Update postman collection files ([`6e09bc9`](https://github.com/juspay/hyperswitch/commit/6e09bc9e2c4bbe14dcb70da4a438850b03b3254c)) + +**Full Changelog:** [`v1.94.0...v1.95.0`](https://github.com/juspay/hyperswitch/compare/v1.94.0...v1.95.0) + +- - - + + ## 1.94.0 (2023-12-01) ### Features