From 6f045d84f1b5d40251de66c8d41a707db75c60e4 Mon Sep 17 00:00:00 2001 From: Ayush Anand <114248859+ayush22667@users.noreply.github.com> Date: Thu, 16 Oct 2025 15:11:40 +0530 Subject: [PATCH] fix(payments): add connector metadata parsing and order category to payment router data (#9825) --- crates/api_models/src/payments.rs | 24 ++++++++-------- .../hyperswitch_domain_models/src/payments.rs | 26 +++-------------- .../src/payments/payment_intent.rs | 28 +++++++++++++++++-- .../operations/payment_update_intent.rs | 8 +++++- .../router/src/core/payments/transformers.rs | 20 ++++++++++++- crates/router/src/core/utils.rs | 7 ++--- .../src/services/kafka/payment_intent.rs | 2 +- .../services/kafka/payment_intent_event.rs | 2 +- 8 files changed, 72 insertions(+), 45 deletions(-) diff --git a/crates/api_models/src/payments.rs b/crates/api_models/src/payments.rs index 937cdaf98d..485aa0e8de 100644 --- a/crates/api_models/src/payments.rs +++ b/crates/api_models/src/payments.rs @@ -459,7 +459,7 @@ pub struct PaymentsUpdateIntentRequest { /// Some connectors like Apple pay, Airwallex and Noon might require some additional information, find specific details in the child attributes below. #[schema(value_type = Option)] - pub connector_metadata: Option, + pub connector_metadata: Option, /// Additional data that might be required by hyperswitch based on the requested features by the merchants. #[schema(value_type = Option)] @@ -634,7 +634,7 @@ pub struct PaymentsIntentResponse { /// Some connectors like Apple pay, Airwallex and Noon might require some additional information, find specific details in the child attributes below. #[schema(value_type = Option)] - pub connector_metadata: Option, + pub connector_metadata: Option, /// Additional data that might be required by hyperswitch based on the requested features by the merchants. #[schema(value_type = Option)] @@ -7558,7 +7558,7 @@ pub struct ApplepaySessionRequest { } /// Some connectors like Apple Pay, Airwallex and Noon might require some additional information, find specific details in the child attributes below. -#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, ToSchema)] +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize, ToSchema)] pub struct ConnectorMetadata { pub apple_pay: Option, pub airwallex: Option, @@ -7593,18 +7593,18 @@ impl ConnectorMetadata { } } -#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, ToSchema)] +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize, ToSchema)] pub struct AirwallexData { /// payload required by airwallex payload: Option, } -#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, ToSchema)] +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize, ToSchema)] pub struct NoonData { /// Information about the order category that merchant wants to specify at connector level. (e.g. In Noon Payments it can take values like "pay", "food", or any other custom string set by the merchant in Noon's Dashboard) pub order_category: Option, } -#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, ToSchema)] +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize, ToSchema)] pub struct BraintreeData { /// Information about the merchant_account_id that merchant wants to specify at connector level. #[schema(value_type = String)] @@ -7614,19 +7614,19 @@ pub struct BraintreeData { pub merchant_config_currency: Option, } -#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, ToSchema)] +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize, ToSchema)] pub struct AdyenConnectorMetadata { pub testing: AdyenTestingData, } -#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, ToSchema)] +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize, ToSchema)] pub struct AdyenTestingData { /// Holder name to be sent to Adyen for a card payment(CIT) or a generic payment(MIT). This value overrides the values for card.card_holder_name and applies during both CIT and MIT payment transactions. #[schema(value_type = String)] pub holder_name: Option>, } -#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, ToSchema)] +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize, ToSchema)] pub struct ApplepayConnectorMetadataRequest { pub session_token_data: Option, } @@ -7674,7 +7674,7 @@ pub struct PaymentRequestMetadata { pub label: String, } -#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, ToSchema)] +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize, ToSchema)] pub struct SessionTokenInfo { #[schema(value_type = String)] pub certificate: Secret, @@ -7690,14 +7690,14 @@ pub struct SessionTokenInfo { pub payment_processing_details_at: Option, } -#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Display, ToSchema)] +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize, Display, ToSchema)] #[serde(rename_all = "snake_case")] pub enum ApplepayInitiative { Web, Ios, } -#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, ToSchema)] +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize, ToSchema)] #[serde(tag = "payment_processing_details_at")] pub enum PaymentProcessingDetailsAt { Hyperswitch(PaymentProcessingDetails), diff --git a/crates/hyperswitch_domain_models/src/payments.rs b/crates/hyperswitch_domain_models/src/payments.rs index 957cd671d1..67b2b56d9b 100644 --- a/crates/hyperswitch_domain_models/src/payments.rs +++ b/crates/hyperswitch_domain_models/src/payments.rs @@ -2,7 +2,7 @@ use std::marker::PhantomData; #[cfg(feature = "v2")] -use api_models::payments::{SessionToken, VaultSessionDetails}; +use api_models::payments::{ConnectorMetadata, SessionToken, VaultSessionDetails}; use common_types::primitive_wrappers; #[cfg(feature = "v1")] use common_types::primitive_wrappers::{ @@ -478,8 +478,8 @@ pub struct PaymentIntent { /// 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>, - /// This metadata contains details about - pub connector_metadata: Option, + /// This metadata contains connector-specific details like Apple Pay certificates, Airwallex data, Noon order category, Braintree merchant account ID, and Adyen testing data + pub connector_metadata: Option, pub feature_metadata: Option, /// Number of attempts that have been made for the order pub attempt_count: i16, @@ -635,10 +635,6 @@ impl PaymentIntent { request: api_models::payments::PaymentsCreateIntentRequest, decrypted_payment_intent: DecryptedPaymentIntent, ) -> CustomResult { - 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 request_incremental_authorization = Self::get_request_incremental_authorization_value(&request)?; let allowed_payment_method_types = request.allowed_payment_method_types; @@ -679,7 +675,7 @@ impl PaymentIntent { active_attempts_group_id: None, order_details, allowed_payment_method_types, - connector_metadata, + connector_metadata: request.connector_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, @@ -829,20 +825,6 @@ impl PaymentIntent { .transpose() } - pub fn get_optional_connector_metadata( - &self, - ) -> CustomResult< - Option, - common_utils::errors::ParsingError, - > { - self.connector_metadata - .clone() - .map(|cm| { - cm.parse_value::("ConnectorMetadata") - }) - .transpose() - } - pub fn get_currency(&self) -> storage_enums::Currency { self.amount_details.currency } diff --git a/crates/hyperswitch_domain_models/src/payments/payment_intent.rs b/crates/hyperswitch_domain_models/src/payments/payment_intent.rs index d3d4f891e6..ecb8445768 100644 --- a/crates/hyperswitch_domain_models/src/payments/payment_intent.rs +++ b/crates/hyperswitch_domain_models/src/payments/payment_intent.rs @@ -1790,7 +1790,15 @@ impl behaviour::Conversion for PaymentIntent { }) .transpose()? .map(Secret::new), - connector_metadata, + connector_metadata: connector_metadata + .map(|cm| { + cm.encode_to_value() + .change_context(ValidationError::InvalidValue { + message: "Failed to serialize connector_metadata".to_string(), + }) + }) + .transpose()? + .map(Secret::new), feature_metadata, attempt_count, profile_id, @@ -1947,7 +1955,12 @@ impl behaviour::Conversion for PaymentIntent { .collect::>() }), allowed_payment_method_types, - connector_metadata: storage_model.connector_metadata, + connector_metadata: storage_model + .connector_metadata + .map(|cm| cm.parse_value("ConnectorMetadata")) + .transpose() + .change_context(common_utils::errors::CryptoError::DecodingFailed) + .attach_printable("Failed to deserialize connector_metadata")?, feature_metadata: storage_model.feature_metadata, attempt_count: storage_model.attempt_count, profile_id: storage_model.profile_id, @@ -2038,7 +2051,16 @@ impl behaviour::Conversion for PaymentIntent { }) .transpose()? .map(Secret::new), - connector_metadata: self.connector_metadata, + connector_metadata: self + .connector_metadata + .map(|cm| { + cm.encode_to_value() + .change_context(ValidationError::InvalidValue { + message: "Failed to serialize connector_metadata".to_string(), + }) + }) + .transpose()? + .map(Secret::new), feature_metadata: self.feature_metadata, attempt_count: self.attempt_count, profile_id: self.profile_id, diff --git a/crates/router/src/core/payments/operations/payment_update_intent.rs b/crates/router/src/core/payments/operations/payment_update_intent.rs index 2af8ee3a7e..b7d7b77c78 100644 --- a/crates/router/src/core/payments/operations/payment_update_intent.rs +++ b/crates/router/src/core/payments/operations/payment_update_intent.rs @@ -370,7 +370,13 @@ impl UpdateTracker, PaymentsUpdateIn order_details: intent.order_details, allowed_payment_method_types: intent.allowed_payment_method_types, metadata: intent.metadata, - connector_metadata: intent.connector_metadata, + connector_metadata: intent + .connector_metadata + .map(|cm| cm.encode_to_value()) + .transpose() + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed to serialize connector_metadata")? + .map(masking::Secret::new), feature_metadata: intent.feature_metadata, payment_link_config: intent.payment_link_config, request_incremental_authorization: Some(intent.request_incremental_authorization), diff --git a/crates/router/src/core/payments/transformers.rs b/crates/router/src/core/payments/transformers.rs index 51730d485f..01fb260b23 100644 --- a/crates/router/src/core/payments/transformers.rs +++ b/crates/router/src/core/payments/transformers.rs @@ -7,6 +7,7 @@ use api_models::payments::{ MandateIds, NetworkDetails, RequestSurchargeDetails, }; use common_enums::{Currency, RequestIncrementalAuthorization}; +#[cfg(feature = "v1")] use common_utils::{ consts::X_HS_LATENCY, fp_utils, pii, @@ -15,6 +16,15 @@ use common_utils::{ StringMajorUnitForConnector, }, }; +#[cfg(feature = "v2")] +use common_utils::{ + ext_traits::Encode, + fp_utils, pii, + types::{ + self as common_utils_type, AmountConvertor, MinorUnit, StringMajorUnit, + StringMajorUnitForConnector, + }, +}; use diesel_models::{ ephemeral_key, payment_attempt::{ @@ -377,6 +387,14 @@ pub async fn construct_payment_router_data_for_authorize<'a>( .change_context(errors::ApiErrorResponse::InternalServerError) .attach_printable("Failed to parse AdditionalPaymentData from payment_data.payment_attempt.payment_method_data")?; + let connector_metadata = payment_data.payment_intent.connector_metadata.clone(); + + let order_category = connector_metadata.as_ref().and_then(|cm| { + cm.noon + .as_ref() + .and_then(|noon| noon.order_category.clone()) + }); + // TODO: few fields are repeated in both routerdata and request let request = types::PaymentsAuthorizeData { payment_method_data: payment_data @@ -403,7 +421,7 @@ pub async fn construct_payment_router_data_for_authorize<'a>( customer_name: None, payment_experience: None, order_details: None, - order_category: None, + order_category, session_token: None, enrolled_for_3ds: true, related_transaction_id: None, diff --git a/crates/router/src/core/utils.rs b/crates/router/src/core/utils.rs index 80b522b7a1..176b49eb14 100644 --- a/crates/router/src/core/utils.rs +++ b/crates/router/src/core/utils.rs @@ -304,10 +304,9 @@ pub async fn construct_refund_router_data<'a, F>( .attach_printable("Failed to get optional customer id")?; let braintree_metadata = payment_intent - .get_optional_connector_metadata() - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Failed parsing ConnectorMetadata")? - .and_then(|cm| cm.braintree); + .connector_metadata + .as_ref() + .and_then(|cm| cm.braintree.clone()); let merchant_account_id = braintree_metadata .as_ref() diff --git a/crates/router/src/services/kafka/payment_intent.rs b/crates/router/src/services/kafka/payment_intent.rs index 366c3a9f96..c396705f35 100644 --- a/crates/router/src/services/kafka/payment_intent.rs +++ b/crates/router/src/services/kafka/payment_intent.rs @@ -143,7 +143,7 @@ pub struct KafkaPaymentIntent<'a> { pub order_details: Option<&'a Vec>>, pub allowed_payment_method_types: Option<&'a Vec>, - pub connector_metadata: Option<&'a Secret>, + pub connector_metadata: Option<&'a api_models::payments::ConnectorMetadata>, pub payment_link_id: Option<&'a String>, pub updated_by: &'a String, pub surcharge_applicable: Option, diff --git a/crates/router/src/services/kafka/payment_intent_event.rs b/crates/router/src/services/kafka/payment_intent_event.rs index 9492ae23cf..ef1989a116 100644 --- a/crates/router/src/services/kafka/payment_intent_event.rs +++ b/crates/router/src/services/kafka/payment_intent_event.rs @@ -96,7 +96,7 @@ pub struct KafkaPaymentIntentEvent<'a> { pub order_details: Option<&'a Vec>>, pub allowed_payment_method_types: Option<&'a Vec>, - pub connector_metadata: Option<&'a Secret>, + pub connector_metadata: Option<&'a api_models::payments::ConnectorMetadata>, pub payment_link_id: Option<&'a String>, pub updated_by: &'a String, pub surcharge_applicable: Option,