Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(router): add card_holder_name pre-validator #6734

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 49 additions & 0 deletions crates/cards/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,48 @@ impl<'de> Deserialize<'de> for CardExpirationMonth {
}
}

#[derive(Serialize)]
pub struct CardHolderName(StrongSecret<String>);

impl TryFrom<String> for CardHolderName {
type Error = error_stack::Report<errors::ValidationError>;
fn try_from(card_holder_name: String) -> Result<Self, Self::Error> {
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<errors::ValidationError>> {
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<D>(deserializer: D) -> Result<Self, D::Error>
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<u16>);

Expand Down Expand Up @@ -188,3 +230,10 @@ impl Deref for CardExpirationYear {
&self.0
}
}

impl Deref for CardHolderName {
type Target = StrongSecret<String>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
29 changes: 26 additions & 3 deletions crates/cards/tests/basic.rs
Original file line number Diff line number Diff line change
@@ -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;

Expand Down Expand Up @@ -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::<CardHolderName>(&serialized).unwrap();
assert_eq!(*derialized.peek(), "Sakil O'Neil".to_string());

let invalid_deserialization = serde_json::from_str::<CardHolderName>("$@k!l M*$t@k");
assert!(invalid_deserialization.is_err());
}

#[test]
fn test_card_expiration_year() {
let curr_date = date_time::now();
Expand Down Expand Up @@ -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::<CardExpiration>(&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}"#;
Expand Down
58 changes: 58 additions & 0 deletions crates/router/src/core/payments/helpers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<String>,
Expand Down Expand Up @@ -1048,6 +1086,26 @@ pub fn validate_card_expiry(
Ok(())
}

pub fn validate_card_holder_name(
optional_card_holder_name: &Option<masking::Secret<String>>,
) -> 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>,
Expand Down
15 changes: 14 additions & 1 deletion crates/router/src/core/payments/operations/payment_confirm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down Expand Up @@ -710,6 +712,17 @@ impl<F: Send + Clone> GetTracker<F, PaymentData<F>, api::PaymentsRequest> for Pa
payment_method_data_billing.get_billing_address()
});

// validate billing name for card holder name
helpers::validate_billing_name(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

create a domain type which will have these validations, use the domain type wherever validations are required

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);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -985,6 +985,13 @@ impl<F: Send + Clone> ValidateRequest<F, api::PaymentsRequest, PaymentData<F>> 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)?;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,13 @@ impl<F: Send + Clone> GetTracker<F, PaymentData<F>, 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,
Expand Down
Loading