From f1bb4a09edeb1a23a63b96592c808482f876b905 Mon Sep 17 00:00:00 2001 From: Kashif Date: Thu, 24 Apr 2025 20:30:10 +0530 Subject: [PATCH] feat(payments): add support for connector testing (Adyen) (#7874) Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com> --- api-reference-v2/openapi_spec.json | 31 +++++ api-reference/openapi_spec.json | 31 +++++ crates/api_models/src/payments.rs | 13 ++ .../src/connectors/adyen/transformers.rs | 131 +++++++++++++----- crates/hyperswitch_connectors/src/utils.rs | 5 + .../src/router_request_types.rs | 2 + crates/openapi/src/openapi.rs | 2 + crates/openapi/src/openapi_v2.rs | 2 + .../router/src/core/payments/transformers.rs | 87 ++++++++++-- crates/router/src/types.rs | 1 + .../router/src/types/api/verify_connector.rs | 1 + crates/router/tests/connectors/utils.rs | 1 + 12 files changed, 256 insertions(+), 51 deletions(-) diff --git a/api-reference-v2/openapi_spec.json b/api-reference-v2/openapi_spec.json index 45f95f01da..6f84c486b5 100644 --- a/api-reference-v2/openapi_spec.json +++ b/api-reference-v2/openapi_spec.json @@ -3775,6 +3775,17 @@ }, "additionalProperties": false }, + "AdyenConnectorMetadata": { + "type": "object", + "required": [ + "testing" + ], + "properties": { + "testing": { + "$ref": "#/components/schemas/AdyenTestingData" + } + } + }, "AdyenSplitData": { "type": "object", "description": "Fee information for Split Payments to be charged on the payment being collected for Adyen", @@ -3848,6 +3859,18 @@ "Vat" ] }, + "AdyenTestingData": { + "type": "object", + "required": [ + "holder_name" + ], + "properties": { + "holder_name": { + "type": "string", + "description": "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." + } + } + }, "AirwallexData": { "type": "object", "properties": { @@ -7974,6 +7997,14 @@ } ], "nullable": true + }, + "adyen": { + "allOf": [ + { + "$ref": "#/components/schemas/AdyenConnectorMetadata" + } + ], + "nullable": true } } }, diff --git a/api-reference/openapi_spec.json b/api-reference/openapi_spec.json index 90bbf70fd6..8c70afc104 100644 --- a/api-reference/openapi_spec.json +++ b/api-reference/openapi_spec.json @@ -6069,6 +6069,17 @@ }, "additionalProperties": false }, + "AdyenConnectorMetadata": { + "type": "object", + "required": [ + "testing" + ], + "properties": { + "testing": { + "$ref": "#/components/schemas/AdyenTestingData" + } + } + }, "AdyenSplitData": { "type": "object", "description": "Fee information for Split Payments to be charged on the payment being collected for Adyen", @@ -6142,6 +6153,18 @@ "Vat" ] }, + "AdyenTestingData": { + "type": "object", + "required": [ + "holder_name" + ], + "properties": { + "holder_name": { + "type": "string", + "description": "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." + } + } + }, "AirwallexData": { "type": "object", "properties": { @@ -10024,6 +10047,14 @@ } ], "nullable": true + }, + "adyen": { + "allOf": [ + { + "$ref": "#/components/schemas/AdyenConnectorMetadata" + } + ], + "nullable": true } } }, diff --git a/crates/api_models/src/payments.rs b/crates/api_models/src/payments.rs index 1f7b40de7e..b83aa00490 100644 --- a/crates/api_models/src/payments.rs +++ b/crates/api_models/src/payments.rs @@ -6729,6 +6729,7 @@ pub struct ConnectorMetadata { pub airwallex: Option, pub noon: Option, pub braintree: Option, + pub adyen: Option, } impl ConnectorMetadata { @@ -6779,6 +6780,18 @@ pub struct BraintreeData { pub merchant_config_currency: Option, } +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, ToSchema)] +pub struct AdyenConnectorMetadata { + pub testing: AdyenTestingData, +} + +#[derive(Debug, Clone, 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)] pub struct ApplepayConnectorMetadataRequest { pub session_token_data: Option, diff --git a/crates/hyperswitch_connectors/src/connectors/adyen/transformers.rs b/crates/hyperswitch_connectors/src/connectors/adyen/transformers.rs index be6543d8b1..c15affcc8a 100644 --- a/crates/hyperswitch_connectors/src/connectors/adyen/transformers.rs +++ b/crates/hyperswitch_connectors/src/connectors/adyen/transformers.rs @@ -10,7 +10,7 @@ use common_enums::enums as storage_enums; use common_utils::ext_traits::OptionExt; use common_utils::{ errors::{CustomResult, ParsingError}, - ext_traits::Encode, + ext_traits::{Encode, ValueExt}, pii::Email, request::Method, types::MinorUnit, @@ -1222,6 +1222,7 @@ pub struct AdyenMandate { #[serde(rename = "type")] payment_type: PaymentType, stored_payment_method_id: Secret, + holder_name: Option>, } #[serde_with::skip_serializing_none] @@ -2008,13 +2009,21 @@ impl TryFrom<(&BankDebitData, &PaymentsAuthorizeRouterData)> for AdyenPaymentMet account_number, sort_code, .. - } => Ok(AdyenPaymentMethod::BacsDirectDebit(Box::new( - BacsDirectDebitData { - bank_account_number: account_number.clone(), - bank_location_id: sort_code.clone(), - holder_name: item.get_billing_full_name()?, - }, - ))), + } => { + let testing_data = item + .request + .get_connector_testing_data() + .map(AdyenTestingData::try_from) + .transpose()?; + let test_holder_name = testing_data.and_then(|test_data| test_data.holder_name); + Ok(AdyenPaymentMethod::BacsDirectDebit(Box::new( + BacsDirectDebitData { + bank_account_number: account_number.clone(), + bank_location_id: sort_code.clone(), + holder_name: test_holder_name.unwrap_or(item.get_billing_full_name()?), + }, + ))) + } BankDebitData::BecsBankDebit { .. } => Err(errors::ConnectorError::NotImplemented( utils::get_unimplemented_payment_method_error_message("Adyen"), @@ -2396,30 +2405,38 @@ impl TryFrom<(&BankRedirectData, &PaymentsAuthorizeRouterData)> for AdyenPayment card_exp_month, card_exp_year, .. - } => Ok(AdyenPaymentMethod::BancontactCard(Box::new(AdyenCard { - brand: Some(CardBrand::Bcmc), - number: card_number - .as_ref() - .ok_or(errors::ConnectorError::MissingRequiredField { - field_name: "bancontact_card.card_number", - })? - .clone(), - expiry_month: card_exp_month - .as_ref() - .ok_or(errors::ConnectorError::MissingRequiredField { - field_name: "bancontact_card.card_exp_month", - })? - .clone(), - expiry_year: card_exp_year - .as_ref() - .ok_or(errors::ConnectorError::MissingRequiredField { - field_name: "bancontact_card.card_exp_year", - })? - .clone(), - holder_name: Some(item.get_billing_full_name()?), - cvc: None, - network_payment_reference: None, - }))), + } => { + let testing_data = item + .request + .get_connector_testing_data() + .map(AdyenTestingData::try_from) + .transpose()?; + let test_holder_name = testing_data.and_then(|test_data| test_data.holder_name); + Ok(AdyenPaymentMethod::BancontactCard(Box::new(AdyenCard { + brand: Some(CardBrand::Bcmc), + number: card_number + .as_ref() + .ok_or(errors::ConnectorError::MissingRequiredField { + field_name: "bancontact_card.card_number", + })? + .clone(), + expiry_month: card_exp_month + .as_ref() + .ok_or(errors::ConnectorError::MissingRequiredField { + field_name: "bancontact_card.card_exp_month", + })? + .clone(), + expiry_year: card_exp_year + .as_ref() + .ok_or(errors::ConnectorError::MissingRequiredField { + field_name: "bancontact_card.card_exp_year", + })? + .clone(), + holder_name: test_holder_name.or(Some(item.get_billing_full_name()?)), + cvc: None, + network_payment_reference: None, + }))) + } BankRedirectData::Bizum { .. } => Ok(AdyenPaymentMethod::Bizum), BankRedirectData::Blik { blik_code } => { Ok(AdyenPaymentMethod::Blik(Box::new(BlikRedirectionData { @@ -2684,6 +2701,13 @@ impl let additional_data = get_additional_data(item.router_data); let return_url = item.router_data.request.get_router_return_url()?; let payment_method_type = item.router_data.request.payment_method_type; + let testing_data = item + .router_data + .request + .get_connector_testing_data() + .map(AdyenTestingData::try_from) + .transpose()?; + let test_holder_name = testing_data.and_then(|test_data| test_data.holder_name); let payment_method = match mandate_ref_id { payments::MandateReferenceId::ConnectorMandateId(connector_mandate_ids) => { let adyen_mandate = AdyenMandate { @@ -2696,6 +2720,7 @@ impl .get_connector_mandate_id() .ok_or_else(missing_field_err("mandate_id"))?, ), + holder_name: test_holder_name, }; Ok::, Self::Error>(PaymentMethod::AdyenMandatePaymentMethod( Box::new(adyen_mandate), @@ -2727,7 +2752,7 @@ impl .get_expiry_year_4_digit() .clone(), cvc: None, - holder_name: card_holder_name, + holder_name: test_holder_name.or(card_holder_name), brand: Some(brand), network_payment_reference: Some(Secret::new(network_mandate_id)), }; @@ -2768,7 +2793,7 @@ impl number: token_data.get_network_token(), expiry_month: token_data.get_network_token_expiry_month(), expiry_year: token_data.get_expiry_year_4_digit(), - holder_name: card_holder_name, + holder_name: test_holder_name.or(card_holder_name), brand: Some(brand), // FIXME: Remove hardcoding network_payment_reference: Some(Secret::new( network_mandate_id.network_transaction_id, @@ -2879,7 +2904,15 @@ impl TryFrom<(&AdyenRouterData<&PaymentsAuthorizeRouterData>, &Card)> for AdyenP let country_code = get_country_code(item.router_data.get_optional_billing()); let additional_data = get_additional_data(item.router_data); let return_url = item.router_data.request.get_router_return_url()?; - let card_holder_name = item.router_data.get_optional_billing_full_name(); + let testing_data = item + .router_data + .request + .get_connector_testing_data() + .map(AdyenTestingData::try_from) + .transpose()?; + let test_holder_name = testing_data.and_then(|test_data| test_data.holder_name); + let card_holder_name = + test_holder_name.or(item.router_data.get_optional_billing_full_name()); let payment_method = PaymentMethod::AdyenPaymentMethod(Box::new( AdyenPaymentMethod::try_from((card_data, card_holder_name))?, )); @@ -5505,6 +5538,24 @@ pub struct DefenseDocuments { defense_document_type_code: String, } +#[derive(Debug, Deserialize)] +pub struct AdyenTestingData { + holder_name: Option>, +} + +impl TryFrom for AdyenTestingData { + type Error = error_stack::Report; + fn try_from(testing_data: common_utils::pii::SecretSerdeValue) -> Result { + let testing_data = testing_data + .expose() + .parse_value::("AdyenTestingData") + .change_context(errors::ConnectorError::InvalidDataFormat { + field_name: "connector_metadata.adyen.testing", + })?; + Ok(testing_data) + } +} + impl TryFrom<&SubmitEvidenceRouterData> for Evidence { type Error = error_stack::Report; fn try_from(item: &SubmitEvidenceRouterData) -> Result { @@ -5782,7 +5833,15 @@ impl let country_code = get_country_code(item.router_data.get_optional_billing()); let additional_data = get_additional_data(item.router_data); let return_url = item.router_data.request.get_router_return_url()?; - let card_holder_name = item.router_data.get_optional_billing_full_name(); + let testing_data = item + .router_data + .request + .get_connector_testing_data() + .map(AdyenTestingData::try_from) + .transpose()?; + let test_holder_name = testing_data.and_then(|test_data| test_data.holder_name); + let card_holder_name = + test_holder_name.or(item.router_data.get_optional_billing_full_name()); let payment_method = PaymentMethod::AdyenPaymentMethod(Box::new( AdyenPaymentMethod::try_from((token_data, card_holder_name))?, )); diff --git a/crates/hyperswitch_connectors/src/utils.rs b/crates/hyperswitch_connectors/src/utils.rs index 8c441d741d..a3ccc73c18 100644 --- a/crates/hyperswitch_connectors/src/utils.rs +++ b/crates/hyperswitch_connectors/src/utils.rs @@ -1691,6 +1691,7 @@ pub trait PaymentsAuthorizeRequestData { fn get_card_network_from_additional_payment_method_data( &self, ) -> Result; + fn get_connector_testing_data(&self) -> Option; } impl PaymentsAuthorizeRequestData for PaymentsAuthorizeData { @@ -1911,6 +1912,9 @@ impl PaymentsAuthorizeRequestData for PaymentsAuthorizeData { .into()), } } + fn get_connector_testing_data(&self) -> Option { + self.connector_testing_data.clone() + } } pub trait PaymentsCaptureRequestData { @@ -6059,6 +6063,7 @@ pub(crate) fn convert_setup_mandate_router_data_to_authorize_router_data( shipping_cost: data.request.shipping_cost, merchant_account_id: None, merchant_config_currency: None, + connector_testing_data: data.request.connector_testing_data.clone(), } } diff --git a/crates/hyperswitch_domain_models/src/router_request_types.rs b/crates/hyperswitch_domain_models/src/router_request_types.rs index 7a91f82f30..1dc88ed95a 100644 --- a/crates/hyperswitch_domain_models/src/router_request_types.rs +++ b/crates/hyperswitch_domain_models/src/router_request_types.rs @@ -76,6 +76,7 @@ pub struct PaymentsAuthorizeData { pub additional_payment_method_data: Option, pub merchant_account_id: Option>, pub merchant_config_currency: Option, + pub connector_testing_data: Option, } #[derive(Debug, Clone)] pub struct PaymentsPostSessionTokensData { @@ -935,4 +936,5 @@ pub struct SetupMandateRequestData { // MinorUnit for amount framework pub minor_amount: Option, pub shipping_cost: Option, + pub connector_testing_data: Option, } diff --git a/crates/openapi/src/openapi.rs b/crates/openapi/src/openapi.rs index 5d9ccfab2a..6dc61d4bf4 100644 --- a/crates/openapi/src/openapi.rs +++ b/crates/openapi/src/openapi.rs @@ -734,6 +734,8 @@ Never share your secret api keys. Keep them guarded and secure. api_models::payments::PaymentsUpdateMetadataRequest, api_models::payments::PaymentsUpdateMetadataResponse, api_models::payments::CtpServiceDetails, + api_models::payments::AdyenConnectorMetadata, + api_models::payments::AdyenTestingData, api_models::feature_matrix::FeatureMatrixListResponse, api_models::feature_matrix::FeatureMatrixRequest, api_models::feature_matrix::ConnectorFeatureMatrixResponse, diff --git a/crates/openapi/src/openapi_v2.rs b/crates/openapi/src/openapi_v2.rs index 2629b1b8b7..b7f8834548 100644 --- a/crates/openapi/src/openapi_v2.rs +++ b/crates/openapi/src/openapi_v2.rs @@ -701,6 +701,8 @@ Never share your secret api keys. Keep them guarded and secure. api_models::payments::DisplayAmountOnSdk, api_models::payments::ErrorDetails, api_models::payments::CtpServiceDetails, + api_models::payments::AdyenConnectorMetadata, + api_models::payments::AdyenTestingData, api_models::feature_matrix::FeatureMatrixListResponse, api_models::feature_matrix::FeatureMatrixRequest, api_models::feature_matrix::ConnectorFeatureMatrixResponse, diff --git a/crates/router/src/core/payments/transformers.rs b/crates/router/src/core/payments/transformers.rs index 0628a7281f..6ae4cf4ec7 100644 --- a/crates/router/src/core/payments/transformers.rs +++ b/crates/router/src/core/payments/transformers.rs @@ -307,6 +307,7 @@ pub async fn construct_payment_router_data_for_authorize<'a>( additional_payment_method_data: None, merchant_account_id: None, merchant_config_currency: None, + connector_testing_data: None, }; let connector_mandate_request_reference_id = payment_data .payment_attempt @@ -960,6 +961,7 @@ pub async fn construct_payment_router_data_for_setup_mandate<'a>( shipping_cost: payment_data.payment_intent.amount_details.shipping_cost, capture_method: Some(payment_data.payment_intent.capture_method), complete_authorize_url, + connector_testing_data: None, }; let connector_mandate_request_reference_id = payment_data .payment_attempt @@ -3183,7 +3185,7 @@ impl TryFrom> for types::PaymentsAuthoriz field_name: "browser_info", })?; - let order_category = additional_data + let connector_metadata = additional_data .payment_data .payment_intent .connector_metadata @@ -3193,21 +3195,16 @@ impl TryFrom> for types::PaymentsAuthoriz .change_context(errors::ApiErrorResponse::InternalServerError) .attach_printable("Failed parsing ConnectorMetadata") }) - .transpose()? - .and_then(|cm| cm.noon.and_then(|noon| noon.order_category)); + .transpose()?; - let braintree_metadata = additional_data - .payment_data - .payment_intent - .connector_metadata - .clone() - .map(|cm| { - cm.parse_value::("ConnectorMetadata") - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Failed parsing ConnectorMetadata") - }) - .transpose()? - .and_then(|cm| cm.braintree); + let order_category = connector_metadata.as_ref().and_then(|cm| { + cm.noon + .as_ref() + .and_then(|noon| noon.order_category.clone()) + }); + let braintree_metadata = connector_metadata + .as_ref() + .and_then(|cm| cm.braintree.clone()); let merchant_account_id = braintree_metadata .as_ref() @@ -3301,6 +3298,30 @@ impl TryFrom> for types::PaymentsAuthoriz .clone(); let shipping_cost = payment_data.payment_intent.shipping_cost; + let connector = api_models::enums::Connector::from_str(connector_name) + .change_context(errors::ConnectorError::InvalidConnectorName) + .change_context(errors::ApiErrorResponse::InvalidDataValue { + field_name: "connector", + }) + .attach_printable_lazy(|| { + format!("unable to parse connector name {connector_name:?}") + })?; + + let connector_testing_data = connector_metadata + .and_then(|cm| match connector { + api_models::enums::Connector::Adyen => cm + .adyen + .map(|adyen_cm| adyen_cm.testing) + .map(|testing_data| { + serde_json::to_value(testing_data) + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed to parse Adyen testing data") + }), + _ => None, + }) + .transpose()? + .map(pii::SecretSerdeValue::new); + Ok(Self { payment_method_data: (payment_method_data.get_required_value("payment_method_data")?), setup_future_usage: payment_data.payment_attempt.setup_future_usage_applied, @@ -3355,6 +3376,7 @@ impl TryFrom> for types::PaymentsAuthoriz shipping_cost, merchant_account_id, merchant_config_currency, + connector_testing_data, }) } } @@ -4106,6 +4128,40 @@ impl TryFrom> for types::SetupMandateRequ payment_data.creds_identifier.as_deref(), )); + let connector = api_models::enums::Connector::from_str(connector_name) + .change_context(errors::ConnectorError::InvalidConnectorName) + .change_context(errors::ApiErrorResponse::InvalidDataValue { + field_name: "connector", + }) + .attach_printable_lazy(|| { + format!("unable to parse connector name {connector_name:?}") + })?; + + let connector_testing_data = payment_data + .payment_intent + .connector_metadata + .as_ref() + .map(|cm| { + cm.clone() + .parse_value::("ConnectorMetadata") + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed parsing ConnectorMetadata") + }) + .transpose()? + .and_then(|cm| match connector { + api_models::enums::Connector::Adyen => cm + .adyen + .map(|adyen_cm| adyen_cm.testing) + .map(|testing_data| { + serde_json::to_value(testing_data) + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed to parse Adyen testing data") + }), + _ => None, + }) + .transpose()? + .map(pii::SecretSerdeValue::new); + Ok(Self { currency: payment_data.currency, confirm: true, @@ -4138,6 +4194,7 @@ impl TryFrom> for types::SetupMandateRequ webhook_url, complete_authorize_url, capture_method: payment_data.payment_attempt.capture_method, + connector_testing_data, }) } } diff --git a/crates/router/src/types.rs b/crates/router/src/types.rs index 2927bc4c8f..057543d963 100644 --- a/crates/router/src/types.rs +++ b/crates/router/src/types.rs @@ -923,6 +923,7 @@ impl ForeignFrom<&SetupMandateRouterData> for PaymentsAuthorizeData { shipping_cost: data.request.shipping_cost, merchant_account_id: None, merchant_config_currency: None, + connector_testing_data: data.request.connector_testing_data.clone(), } } } diff --git a/crates/router/src/types/api/verify_connector.rs b/crates/router/src/types/api/verify_connector.rs index a2a4cf88f6..223c3e5638 100644 --- a/crates/router/src/types/api/verify_connector.rs +++ b/crates/router/src/types/api/verify_connector.rs @@ -63,6 +63,7 @@ impl VerifyConnectorData { shipping_cost: None, merchant_account_id: None, merchant_config_currency: None, + connector_testing_data: None, } } diff --git a/crates/router/tests/connectors/utils.rs b/crates/router/tests/connectors/utils.rs index 6a7239b5b4..2bc4c9f24e 100644 --- a/crates/router/tests/connectors/utils.rs +++ b/crates/router/tests/connectors/utils.rs @@ -987,6 +987,7 @@ impl Default for PaymentAuthorizeType { shipping_cost: None, merchant_account_id: None, merchant_config_currency: None, + connector_testing_data: None, }; Self(data) }