diff --git a/crates/cards/src/lib.rs b/crates/cards/src/lib.rs index 91cb93301a9..f231234f6ce 100644 --- a/crates/cards/src/lib.rs +++ b/crates/cards/src/lib.rs @@ -67,6 +67,48 @@ impl<'de> Deserialize<'de> for CardExpirationMonth { } } +#[derive(Serialize)] +pub struct CardHolderName(StrongSecret); + +impl TryFrom for CardHolderName { + type Error = error_stack::Report; + fn try_from(card_holder_name: String) -> Result { + for char in card_holder_name.chars() { + validate_character_in_card_holder_name(char)?; + } + Ok(Self(StrongSecret::new(card_holder_name))) + } +} + +fn validate_character_in_card_holder_name( + character: char, +) -> Result<(), error_stack::Report> { + if character.is_alphabetic() + || character == ' ' + || character == '.' + || character == '-' + || character == '\'' + || character == '~' + || character == '`' + { + Ok(()) + } else { + Err(report!(errors::ValidationError::InvalidValue { + message: "invalid character in card holder name".to_string() + })) + } +} + +impl<'de> Deserialize<'de> for CardHolderName { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + let card_holder_name = String::deserialize(deserializer)?; + card_holder_name.try_into().map_err(de::Error::custom) + } +} + #[derive(Serialize)] pub struct CardExpirationYear(StrongSecret); @@ -188,3 +230,10 @@ impl Deref for CardExpirationYear { &self.0 } } + +impl Deref for CardHolderName { + type Target = StrongSecret; + fn deref(&self) -> &Self::Target { + &self.0 + } +} diff --git a/crates/cards/tests/basic.rs b/crates/cards/tests/basic.rs index 0f27c9e0288..22239aaaacf 100644 --- a/crates/cards/tests/basic.rs +++ b/crates/cards/tests/basic.rs @@ -1,6 +1,8 @@ #![allow(clippy::unwrap_used, clippy::expect_used)] -use cards::{CardExpiration, CardExpirationMonth, CardExpirationYear, CardSecurityCode}; +use cards::{ + CardExpiration, CardExpirationMonth, CardExpirationYear, CardHolderName, CardSecurityCode, +}; use common_utils::date_time; use masking::PeekInterface; @@ -42,6 +44,27 @@ fn test_card_expiration_month() { assert!(invalid_deserialization.is_err()); } +#[test] +fn test_card_holder_name() { + // no panic + let card_holder_name = CardHolderName::try_from("Sakil O'Neil".to_string()).unwrap(); + + // will panic on unwrap + let invalid_card_holder_name = CardHolderName::try_from("$@k!l M*$t@k".to_string()); + + assert_eq!(*card_holder_name.peek(), "Sakil O'Neil"); + assert!(invalid_card_holder_name.is_err()); + + let serialized = serde_json::to_string(&card_holder_name).unwrap(); + assert_eq!(&serialized, "\"Sakil O'Neil\""); + + let derialized = serde_json::from_str::(&serialized).unwrap(); + assert_eq!(*derialized.peek(), "Sakil O'Neil".to_string()); + + let invalid_deserialization = serde_json::from_str::("$@k!l M*$t@k"); + assert!(invalid_deserialization.is_err()); +} + #[test] fn test_card_expiration_year() { let curr_date = date_time::now(); @@ -85,11 +108,11 @@ fn test_card_expiration() { assert!(invalid_card_exp.is_err()); let serialized = serde_json::to_string(&card_exp).unwrap(); - let expected_string = format!(r#"{{"month":{},"year":{}}}"#, 3, curr_year); + let expected_string = format!(r#"{{"month":{},"year":{}}}"#, curr_month, curr_year); assert_eq!(serialized, expected_string); let derialized = serde_json::from_str::(&serialized).unwrap(); - assert_eq!(*derialized.get_month().peek(), 3); + assert_eq!(*derialized.get_month().peek(), curr_month); assert_eq!(*derialized.get_year().peek(), curr_year); let invalid_serialized_string = r#"{"month":13,"year":123}"#; diff --git a/crates/router/src/core/payments/helpers.rs b/crates/router/src/core/payments/helpers.rs index e7de03804d1..f15ba01c2ef 100644 --- a/crates/router/src/core/payments/helpers.rs +++ b/crates/router/src/core/payments/helpers.rs @@ -995,10 +995,48 @@ pub fn validate_card_data( )?; validate_card_expiry(&card.card_exp_month, &card.card_exp_year)?; + validate_card_holder_name(&card.card_holder_name)?; } Ok(()) } +pub fn validate_billing_name( + billing_address: Option<&api_models::payments::AddressDetails>, +) -> CustomResult<(), errors::ApiErrorResponse> { + let optional_first_name = billing_address.and_then(|address| address.first_name.clone()); + let optional_last_name = billing_address.and_then(|address| address.last_name.clone()); + + if let Some(first_name) = optional_first_name { + let first_name = first_name.peek().to_string(); + if first_name.len() > 256 { + Err(report!(errors::ApiErrorResponse::PreconditionFailed { + message: "Invalid First Name Length".to_string() + }))? + } + ::cards::CardHolderName::try_from(first_name).change_context( + errors::ApiErrorResponse::PreconditionFailed { + message: "Invalid First Name".to_string(), + }, + )?; + } + + if let Some(last_name) = optional_last_name { + let last_name = last_name.peek().to_string(); + if last_name.len() > 256 { + Err(report!(errors::ApiErrorResponse::PreconditionFailed { + message: "Invalid Last Name Length".to_string() + }))? + } + ::cards::CardHolderName::try_from(last_name).change_context( + errors::ApiErrorResponse::PreconditionFailed { + message: "Invalid Last Name".to_string(), + }, + )?; + } + + Ok(()) +} + #[instrument(skip_all)] pub fn validate_card_expiry( card_exp_month: &masking::Secret, @@ -1048,6 +1086,26 @@ pub fn validate_card_expiry( Ok(()) } +pub fn validate_card_holder_name( + optional_card_holder_name: &Option>, +) -> CustomResult<(), errors::ApiErrorResponse> { + if let Some(masked_card_holder_name) = optional_card_holder_name { + let card_holder_name = masked_card_holder_name.peek().to_string(); + if card_holder_name.len() > 256 { + Err(report!(errors::ApiErrorResponse::PreconditionFailed { + message: "Invalid Card Holder Name Length".to_string() + }))? + } + // validate card holder name + ::cards::CardHolderName::try_from(card_holder_name).change_context( + errors::ApiErrorResponse::PreconditionFailed { + message: "Invalid Card Holder Name".to_string(), + }, + )?; + } + Ok(()) +} + pub fn infer_payment_type( amount: &api::Amount, mandate_type: Option<&api::MandateTransactionType>, diff --git a/crates/router/src/core/payments/operations/payment_confirm.rs b/crates/router/src/core/payments/operations/payment_confirm.rs index b56260b407e..dfc15bdc040 100644 --- a/crates/router/src/core/payments/operations/payment_confirm.rs +++ b/crates/router/src/core/payments/operations/payment_confirm.rs @@ -7,7 +7,9 @@ use api_models::{ admin::ExtendedCardInfoConfig, enums::FrmSuggestion, // payment_methods::PaymentMethodsData, - payments::{ConnectorMandateReferenceId, ExtendedCardInfo, GetAddressFromPaymentMethodData}, + payments::{ + Address, ConnectorMandateReferenceId, ExtendedCardInfo, GetAddressFromPaymentMethodData, + }, }; use async_trait::async_trait; use common_utils::ext_traits::{AsyncExt, Encode, StringExt, ValueExt}; @@ -710,6 +712,17 @@ impl GetTracker, api::PaymentsRequest> for Pa payment_method_data_billing.get_billing_address() }); + // validate billing name for card holder name + helpers::validate_billing_name( + payment_method_data_billing + .as_ref() + .and_then(|billing: &Address| billing.address.as_ref()) + .or(request + .billing + .as_ref() + .and_then(|billing: &Address| billing.address.as_ref())), + )?; + let unified_address = address.unify_with_payment_method_data_billing(payment_method_data_billing); diff --git a/crates/router/src/core/payments/operations/payment_create.rs b/crates/router/src/core/payments/operations/payment_create.rs index 697c06603da..bcdd5fe17cc 100644 --- a/crates/router/src/core/payments/operations/payment_create.rs +++ b/crates/router/src/core/payments/operations/payment_create.rs @@ -985,6 +985,13 @@ impl ValidateRequest> f .as_ref() .and_then(|pmd| pmd.payment_method_data.clone()), )?; + // validate billing name for card holder name + helpers::validate_billing_name( + request + .billing + .as_ref() + .and_then(|billing| billing.address.as_ref()), + )?; helpers::validate_payment_method_fields_present(request)?; diff --git a/crates/router/src/core/payments/operations/payment_update.rs b/crates/router/src/core/payments/operations/payment_update.rs index 98491cab1db..0a9f5666dcb 100644 --- a/crates/router/src/core/payments/operations/payment_update.rs +++ b/crates/router/src/core/payments/operations/payment_update.rs @@ -100,6 +100,13 @@ impl GetTracker, api::PaymentsRequest> for Pa .as_ref() .and_then(|pmd| pmd.payment_method_data.clone()), )?; + // validate billing name for card holder name + helpers::validate_billing_name( + request + .billing + .as_ref() + .and_then(|billing| billing.address.as_ref()), + )?; helpers::validate_payment_status_against_allowed_statuses( &payment_intent.status,