diff --git a/crates/hyperswitch_connectors/src/connectors/nexixpay/transformers.rs b/crates/hyperswitch_connectors/src/connectors/nexixpay/transformers.rs index 32e630a2be..4e30b1a6ef 100644 --- a/crates/hyperswitch_connectors/src/connectors/nexixpay/transformers.rs +++ b/crates/hyperswitch_connectors/src/connectors/nexixpay/transformers.rs @@ -27,6 +27,7 @@ use hyperswitch_domain_models::{ }; use hyperswitch_interfaces::{consts::NO_ERROR_CODE, errors}; use masking::{ExposeInterface, Secret}; +use rand::distributions::{Alphanumeric, DistString}; use serde::{Deserialize, Serialize}; use strum::Display; @@ -40,6 +41,224 @@ use crate::{ }, }; +fn get_random_string() -> String { + Alphanumeric.sample_string(&mut rand::thread_rng(), MAX_ORDER_ID_LENGTH) +} + +#[derive(Clone, Copy, Debug)] +enum AddressKind { + Billing, + Shipping, +} + +trait AddressConstructor { + fn new( + name: Option>, + street: Option>, + city: Option, + post_code: Option>, + country: Option, + ) -> Self; +} + +impl AddressConstructor for BillingAddress { + fn new( + name: Option>, + street: Option>, + city: Option, + post_code: Option>, + country: Option, + ) -> Self { + Self { + name, + street, + city, + post_code, + country, + } + } +} + +impl AddressConstructor for ShippingAddress { + fn new( + name: Option>, + street: Option>, + city: Option, + post_code: Option>, + country: Option, + ) -> Self { + Self { + name, + street, + city, + post_code, + country, + } + } +} + +fn get_validated_address_details_generic( + data: &RouterContextDataAlias, + address_kind: AddressKind, +) -> Result, error_stack::Report> +where + RouterContextDataAlias: crate::utils::RouterData, + AddressOutput: AddressConstructor + Sized, +{ + let ( + opt_line1, + opt_line2, + opt_full_name, + opt_city, + opt_zip, + opt_country, + has_address_details_check, + address_type_str, + max_name_len, + max_street_len, + max_city_len, + max_post_code_len, + max_country_len, + ) = match address_kind { + AddressKind::Billing => ( + data.get_optional_billing_line1(), + data.get_optional_billing_line2(), + data.get_optional_billing_full_name(), + data.get_optional_billing_city(), + data.get_optional_billing_zip(), + data.get_optional_billing_country(), + data.get_optional_billing().is_some(), + "billing", + MAX_BILLING_ADDRESS_NAME_LENGTH, + MAX_BILLING_ADDRESS_STREET_LENGTH, + MAX_BILLING_ADDRESS_CITY_LENGTH, + MAX_BILLING_ADDRESS_POST_CODE_LENGTH, + MAX_BILLING_ADDRESS_COUNTRY_LENGTH, + ), + AddressKind::Shipping => ( + data.get_optional_shipping_line1(), + data.get_optional_shipping_line2(), + data.get_optional_shipping_full_name(), + data.get_optional_shipping_city(), + data.get_optional_shipping_zip(), + data.get_optional_shipping_country(), + data.get_optional_shipping().is_some(), + "shipping", + MAX_BILLING_ADDRESS_NAME_LENGTH, + MAX_BILLING_ADDRESS_STREET_LENGTH, + MAX_BILLING_ADDRESS_CITY_LENGTH, + MAX_BILLING_ADDRESS_POST_CODE_LENGTH, + MAX_BILLING_ADDRESS_COUNTRY_LENGTH, + ), + }; + + let street_val = match (opt_line1.clone(), opt_line2.clone()) { + (Some(l1), Some(l2)) => Some(Secret::new(format!("{}, {}", l1.expose(), l2.expose()))), + (Some(l1), None) => Some(l1), + (None, Some(l2)) => Some(l2), + (None, None) => None, + }; + + if has_address_details_check { + let name_val = opt_full_name; + if let Some(ref val) = name_val { + let length = val.clone().expose().len(); + if length > max_name_len { + return Err(error_stack::Report::from( + errors::ConnectorError::MaxFieldLengthViolated { + field_name: format!( + "{0}.address.first_name & {0}.address.last_name", + address_type_str + ), + connector: "Nexixpay".to_string(), + max_length: max_name_len, + received_length: length, + }, + )); + } + } + + if let Some(ref val) = street_val { + let length = val.clone().expose().len(); + if length > max_street_len { + return Err(error_stack::Report::from( + errors::ConnectorError::MaxFieldLengthViolated { + field_name: format!( + "{0}.address.line1 & {0}.address.line2", + address_type_str + ), + connector: "Nexixpay".to_string(), + max_length: max_street_len, + received_length: length, + }, + )); + } + } + + let city_val = opt_city; + if let Some(ref val) = city_val { + let length = val.len(); + if length > max_city_len { + return Err(error_stack::Report::from( + errors::ConnectorError::MaxFieldLengthViolated { + field_name: format!("{}.address.city", address_type_str), + connector: "Nexixpay".to_string(), + max_length: max_city_len, + received_length: length, + }, + )); + } + } + + let post_code_val = opt_zip; + if let Some(ref val) = post_code_val { + let length = val.clone().expose().len(); + if length > max_post_code_len { + return Err(error_stack::Report::from( + errors::ConnectorError::MaxFieldLengthViolated { + field_name: format!("{}.address.zip", address_type_str), + connector: "Nexixpay".to_string(), + max_length: max_post_code_len, + received_length: length, + }, + )); + } + } + + let country_val = opt_country; + if let Some(ref val) = country_val { + let length = val.to_string().len(); + if length > max_country_len { + return Err(error_stack::Report::from( + errors::ConnectorError::MaxFieldLengthViolated { + field_name: format!("{}.address.country", address_type_str), + connector: "Nexixpay".to_string(), + max_length: max_country_len, + received_length: length, + }, + )); + } + } + Ok(Some(AddressOutput::new( + name_val, + street_val, + city_val, + post_code_val, + country_val, + ))) + } else { + Ok(None) + } +} + +const MAX_ORDER_ID_LENGTH: usize = 18; +const MAX_CARD_HOLDER_LENGTH: usize = 255; +const MAX_BILLING_ADDRESS_NAME_LENGTH: usize = 50; +const MAX_BILLING_ADDRESS_STREET_LENGTH: usize = 50; +const MAX_BILLING_ADDRESS_CITY_LENGTH: usize = 40; +const MAX_BILLING_ADDRESS_POST_CODE_LENGTH: usize = 16; +const MAX_BILLING_ADDRESS_COUNTRY_LENGTH: usize = 3; + pub struct NexixpayRouterData { pub amount: StringMinorUnit, pub router_data: T, @@ -480,60 +699,50 @@ impl TryFrom<&NexixpayRouterData<&PaymentsAuthorizeRouterData>> for NexixpayPaym fn try_from( item: &NexixpayRouterData<&PaymentsAuthorizeRouterData>, ) -> Result { - let billing_address_street = match ( - item.router_data.get_optional_billing_line1(), - item.router_data.get_optional_billing_line2(), - ) { - (Some(line1), Some(line2)) => Some(Secret::new(format!( - "{}, {}", - line1.expose(), - line2.expose() - ))), - (Some(line1), None) => Some(line1), - (None, Some(line2)) => Some(line2), - (None, None) => None, - }; - let billing_address = item - .router_data - .get_optional_billing() - .map(|_| BillingAddress { - name: item.router_data.get_optional_billing_full_name(), - street: billing_address_street, - city: item.router_data.get_optional_billing_city(), - post_code: item.router_data.get_optional_billing_zip(), - country: item.router_data.get_optional_billing_country(), - }); - let shipping_address_street = match ( - item.router_data.get_optional_shipping_line1(), - item.router_data.get_optional_shipping_line2(), - ) { - (Some(line1), Some(line2)) => Some(Secret::new(format!( - "{}, {}", - line1.expose(), - line2.expose() - ))), - (Some(line1), None) => Some(Secret::new(line1.expose())), - (None, Some(line2)) => Some(Secret::new(line2.expose())), - (None, None) => None, + let order_id = if item.router_data.payment_id.len() > MAX_ORDER_ID_LENGTH { + if item.router_data.payment_id.starts_with("pay_") { + get_random_string() + } else { + return Err(error_stack::Report::from( + errors::ConnectorError::MaxFieldLengthViolated { + field_name: "payment_id".to_string(), + connector: "Nexixpay".to_string(), + max_length: MAX_ORDER_ID_LENGTH, + received_length: item.router_data.payment_id.len(), + }, + )); + } + } else { + item.router_data.payment_id.clone() }; - let shipping_address = item - .router_data - .get_optional_shipping() - .map(|_| ShippingAddress { - name: item.router_data.get_optional_shipping_full_name(), - street: shipping_address_street, - city: item.router_data.get_optional_shipping_city(), - post_code: item.router_data.get_optional_shipping_zip(), - country: item.router_data.get_optional_shipping_country(), - }); + let billing_address = get_validated_billing_address(item.router_data)?; + let shipping_address = get_validated_shipping_address(item.router_data)?; + let customer_info = CustomerInfo { - card_holder_name: item.router_data.get_billing_full_name()?, + card_holder_name: match item.router_data.get_billing_full_name()? { + name if name.clone().expose().len() <= MAX_CARD_HOLDER_LENGTH => name, + _ => { + return Err(error_stack::Report::from( + errors::ConnectorError::MaxFieldLengthViolated { + field_name: "billing.address.first_name & billing.address.last_name" + .to_string(), + connector: "Nexixpay".to_string(), + max_length: MAX_CARD_HOLDER_LENGTH, + received_length: item + .router_data + .get_billing_full_name()? + .expose() + .len(), + }, + )) + } + }, billing_address: billing_address.clone(), shipping_address: shipping_address.clone(), }; let order = Order { - order_id: item.router_data.connector_request_reference_id.clone(), + order_id, amount: item.amount.clone(), currency: item.router_data.request.currency, description: item.router_data.description.clone(), @@ -1089,55 +1298,27 @@ impl TryFrom<&NexixpayRouterData<&PaymentsCompleteAuthorizeRouterData>> )?; let capture_type = get_nexixpay_capture_type(item.router_data.request.capture_method)?; - let order_id = item.router_data.connector_request_reference_id.clone(); + let order_id = if item.router_data.payment_id.len() > MAX_ORDER_ID_LENGTH { + if item.router_data.payment_id.starts_with("pay_") { + get_random_string() + } else { + return Err(error_stack::Report::from( + errors::ConnectorError::MaxFieldLengthViolated { + field_name: "payment_id".to_string(), + connector: "Nexixpay".to_string(), + max_length: MAX_ORDER_ID_LENGTH, + received_length: item.router_data.payment_id.len(), + }, + )); + } + } else { + item.router_data.payment_id.clone() + }; let amount = item.amount.clone(); - let billing_address_street = match ( - item.router_data.get_optional_billing_line1(), - item.router_data.get_optional_billing_line2(), - ) { - (Some(line1), Some(line2)) => Some(Secret::new(format!( - "{}, {}", - line1.expose(), - line2.expose() - ))), - (Some(line1), None) => Some(line1), - (None, Some(line2)) => Some(line2), - (None, None) => None, - }; - let billing_address = item - .router_data - .get_optional_billing() - .map(|_| BillingAddress { - name: item.router_data.get_optional_billing_full_name(), - street: billing_address_street, - city: item.router_data.get_optional_billing_city(), - post_code: item.router_data.get_optional_billing_zip(), - country: item.router_data.get_optional_billing_country(), - }); - let shipping_address_street = match ( - item.router_data.get_optional_shipping_line1(), - item.router_data.get_optional_shipping_line2(), - ) { - (Some(line1), Some(line2)) => Some(Secret::new(format!( - "{}, {}", - line1.expose(), - line2.expose() - ))), - (Some(line1), None) => Some(Secret::new(line1.expose())), - (None, Some(line2)) => Some(Secret::new(line2.expose())), - (None, None) => None, - }; - let shipping_address = item - .router_data - .get_optional_shipping() - .map(|_| ShippingAddress { - name: item.router_data.get_optional_shipping_full_name(), - street: shipping_address_street, - city: item.router_data.get_optional_shipping_city(), - post_code: item.router_data.get_optional_shipping_zip(), - country: item.router_data.get_optional_shipping_country(), - }); + let billing_address = get_validated_billing_address(item.router_data)?; + let shipping_address = get_validated_shipping_address(item.router_data)?; + let customer_info = CustomerInfo { card_holder_name: item.router_data.get_billing_full_name()?, billing_address: billing_address.clone(), @@ -1227,6 +1408,24 @@ impl TryFrom<&NexixpayRouterData<&PaymentsCompleteAuthorizeRouterData>> } } +fn get_validated_shipping_address( + data: &RouterContextDataAlias, +) -> Result, error_stack::Report> +where + RouterContextDataAlias: crate::utils::RouterData, +{ + get_validated_address_details_generic(data, AddressKind::Shipping) +} + +fn get_validated_billing_address( + data: &RouterContextDataAlias, +) -> Result, error_stack::Report> +where + RouterContextDataAlias: crate::utils::RouterData, +{ + get_validated_address_details_generic(data, AddressKind::Billing) +} + impl TryFrom< ResponseRouterData, diff --git a/crates/hyperswitch_domain_models/src/errors/api_error_response.rs b/crates/hyperswitch_domain_models/src/errors/api_error_response.rs index 3acabaca12..21accfa22c 100644 --- a/crates/hyperswitch_domain_models/src/errors/api_error_response.rs +++ b/crates/hyperswitch_domain_models/src/errors/api_error_response.rs @@ -292,6 +292,13 @@ pub enum ApiErrorResponse { ExternalVaultFailed, #[error(error_type = ErrorType::InvalidRequestError, code = "IR_46", message = "Field {fields} doesn't match with the ones used during mandate creation")] MandatePaymentDataMismatch { fields: String }, + #[error(error_type = ErrorType::InvalidRequestError, code = "IR_47", message = "Connector '{connector}' rejected field '{field_name}': length {received_length} exceeds maximum of {max_length}")] + MaxFieldLengthViolated { + connector: String, + field_name: String, + max_length: usize, + received_length: usize, + }, #[error(error_type = ErrorType::InvalidRequestError, code = "WE_01", message = "Failed to authenticate the webhook")] WebhookAuthenticationFailed, #[error(error_type = ErrorType::InvalidRequestError, code = "WE_02", message = "Bad request received in webhook")] @@ -656,7 +663,9 @@ impl ErrorSwitch for ApiErrorRespon Self::MandatePaymentDataMismatch { fields} => { AER::BadRequest(ApiError::new("IR", 46, format!("Field {fields} doesn't match with the ones used during mandate creation"), Some(Extra {fields: Some(fields.to_owned()), ..Default::default()}))) //FIXME: error message } - + Self::MaxFieldLengthViolated { connector, field_name, max_length, received_length} => { + AER::BadRequest(ApiError::new("IR", 47, format!("Connector '{connector}' rejected field '{field_name}': length {received_length} exceeds maximum of {max_length}"), Some(Extra {connector: Some(connector.to_string()), ..Default::default()}))) + } Self::WebhookAuthenticationFailed => { AER::Unauthorized(ApiError::new("WE", 1, "Webhook authentication failed", None)) } diff --git a/crates/hyperswitch_interfaces/src/errors.rs b/crates/hyperswitch_interfaces/src/errors.rs index be67daaa09..60880c1130 100644 --- a/crates/hyperswitch_interfaces/src/errors.rs +++ b/crates/hyperswitch_interfaces/src/errors.rs @@ -127,6 +127,13 @@ pub enum ConnectorError { }, #[error("Field {fields} doesn't match with the ones used during mandate creation")] MandatePaymentDataMismatch { fields: String }, + #[error("Field '{field_name}' is too long for connector '{connector}'")] + MaxFieldLengthViolated { + connector: String, + field_name: String, + max_length: usize, + received_length: usize, + }, } impl ConnectorError { diff --git a/crates/router/src/compatibility/stripe/errors.rs b/crates/router/src/compatibility/stripe/errors.rs index 39d5d7040d..58f707b2db 100644 --- a/crates/router/src/compatibility/stripe/errors.rs +++ b/crates/router/src/compatibility/stripe/errors.rs @@ -601,6 +601,7 @@ impl From for StripeErrorCode { errors::ApiErrorResponse::NotImplemented { .. } => Self::Unauthorized, errors::ApiErrorResponse::FlowNotSupported { .. } => Self::InternalServerError, errors::ApiErrorResponse::MandatePaymentDataMismatch { .. } => Self::PlatformBadRequest, + errors::ApiErrorResponse::MaxFieldLengthViolated { .. } => Self::PlatformBadRequest, errors::ApiErrorResponse::PaymentUnexpectedState { current_flow, field_name, diff --git a/crates/router/src/core/errors/utils.rs b/crates/router/src/core/errors/utils.rs index 4302dba446..53ea2e83fd 100644 --- a/crates/router/src/core/errors/utils.rs +++ b/crates/router/src/core/errors/utils.rs @@ -156,6 +156,7 @@ impl ConnectorErrorExt for error_stack::Result | errors::ConnectorError::NoConnectorMetaData | errors::ConnectorError::NoConnectorWalletDetails | errors::ConnectorError::FailedToObtainCertificateKey + | errors::ConnectorError::MaxFieldLengthViolated { .. } | errors::ConnectorError::FlowNotSupported { .. } | errors::ConnectorError::MissingConnectorMandateID | errors::ConnectorError::MissingConnectorMandateMetadata @@ -242,6 +243,9 @@ impl ConnectorErrorExt for error_stack::Result errors::ConnectorError::FlowNotSupported{ flow, connector } => { errors::ApiErrorResponse::FlowNotSupported { flow: flow.to_owned(), connector: connector.to_owned() } }, + errors::ConnectorError::MaxFieldLengthViolated{ connector, field_name, max_length, received_length} => { + errors::ApiErrorResponse::MaxFieldLengthViolated { connector: connector.to_string(), field_name: field_name.to_string(), max_length: *max_length, received_length: *received_length } + }, errors::ConnectorError::InvalidDataFormat { field_name } => { errors::ApiErrorResponse::InvalidDataValue { field_name } }, @@ -362,6 +366,7 @@ impl ConnectorErrorExt for error_stack::Result | errors::ConnectorError::FailedToObtainCertificateKey | errors::ConnectorError::NotImplemented(_) | errors::ConnectorError::NotSupported { .. } + | errors::ConnectorError::MaxFieldLengthViolated { .. } | errors::ConnectorError::FlowNotSupported { .. } | errors::ConnectorError::MissingConnectorMandateID | errors::ConnectorError::MissingConnectorMandateMetadata diff --git a/crates/storage_impl/src/errors.rs b/crates/storage_impl/src/errors.rs index 9a14320a85..2260a0528f 100644 --- a/crates/storage_impl/src/errors.rs +++ b/crates/storage_impl/src/errors.rs @@ -161,6 +161,13 @@ pub enum ConnectorError { }, #[error("{flow} flow not supported by {connector} connector")] FlowNotSupported { flow: String, connector: String }, + #[error("Connector '{connector}' rejected field '{field_name}': length {received_length} exceeds maximum of {max_length}'")] + MaxFieldLengthViolated { + connector: String, + field_name: String, + max_length: usize, + received_length: usize, + }, #[error("Capture method not supported")] CaptureMethodNotSupported, #[error("Missing connector transaction ID")]