WARNING: THIS SITE IS A MIRROR OF GITHUB.COM / IT CANNOT LOGIN OR REGISTER ACCOUNTS / THE CONTENTS ARE PROVIDED AS-IS / THIS SITE ASSUMES NO RESPONSIBILITY FOR ANY DISPLAYED CONTENT OR LINKS / IF YOU FOUND SOMETHING MAY NOT GOOD FOR EVERYONE, CONTACT ADMIN AT ilovescratch@foxmail.com
Skip to content

Commit 2c9e630

Browse files
fix(core): Merchant Category Code Enum to strict Struct (#10423)
Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com>
1 parent c0d13ed commit 2c9e630

File tree

5 files changed

+176
-102
lines changed

5 files changed

+176
-102
lines changed

api-reference/v1/openapi_spec_v1.json

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -21465,15 +21465,8 @@
2146521465
},
2146621466
"MerchantCategoryCode": {
2146721467
"type": "string",
21468-
"enum": [
21469-
"5411",
21470-
"7011",
21471-
"0763",
21472-
"8111",
21473-
"5021",
21474-
"4816",
21475-
"5661"
21476-
]
21468+
"title": "4 digit Merchant category code (MCC)",
21469+
"example": "5411"
2147721470
},
2147821471
"MerchantConnectorAccountId": {
2147921472
"type": "string",

api-reference/v2/openapi_spec_v2.json

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -14743,15 +14743,8 @@
1474314743
},
1474414744
"MerchantCategoryCode": {
1474514745
"type": "string",
14746-
"enum": [
14747-
"5411",
14748-
"7011",
14749-
"0763",
14750-
"8111",
14751-
"5021",
14752-
"4816",
14753-
"5661"
14754-
]
14746+
"title": "4 digit Merchant category code (MCC)",
14747+
"example": "5411"
1475514748
},
1475614749
"MerchantConnectorAccountFeatureMetadata": {
1475714750
"type": "object",

crates/common_enums/src/enums.rs

Lines changed: 139 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,23 @@ mod payments;
33
mod ui;
44
use std::{
55
collections::HashSet,
6+
fmt,
67
num::{ParseFloatError, TryFromIntError},
8+
str::FromStr,
79
};
810

911
pub use accounts::{
1012
MerchantAccountRequestType, MerchantAccountType, MerchantProductType, OrganizationType,
1113
};
14+
use diesel::{
15+
backend::Backend,
16+
deserialize::FromSql,
17+
expression::AsExpression,
18+
serialize::{Output, ToSql},
19+
sql_types::Text,
20+
};
1221
pub use payments::ProductType;
13-
use serde::{Deserialize, Serialize};
22+
use serde::{de, Deserialize, Deserializer, Serialize, Serializer};
1423
use smithy::SmithyModel;
1524
pub use ui::*;
1625
use utoipa::ToSchema;
@@ -3081,96 +3090,147 @@ pub enum DisputeStatus {
30813090
DisputeLost,
30823091
}
30833092

3084-
#[derive(
3085-
Clone,
3086-
Copy,
3087-
Debug,
3088-
Eq,
3089-
Hash,
3090-
PartialEq,
3091-
serde::Deserialize,
3092-
serde::Serialize,
3093-
strum::Display,
3094-
strum::EnumString,
3095-
strum::EnumIter,
3096-
strum::VariantNames,
3097-
ToSchema,
3093+
#[derive(Debug, Clone, AsExpression, PartialEq, ToSchema)]
3094+
#[schema(
3095+
value_type = String,
3096+
title = "4 digit Merchant category code (MCC)",
3097+
example = "5411"
30983098
)]
3099-
pub enum MerchantCategory {
3100-
#[serde(rename = "Grocery Stores, Supermarkets (5411)")]
3101-
GroceryStoresSupermarkets,
3102-
#[serde(rename = "Lodging-Hotels, Motels, Resorts-not elsewhere classified (7011)")]
3103-
LodgingHotelsMotelsResorts,
3104-
#[serde(rename = "Agricultural Cooperatives (0763)")]
3105-
AgriculturalCooperatives,
3106-
#[serde(rename = "Attorneys, Legal Services (8111)")]
3107-
AttorneysLegalServices,
3108-
#[serde(rename = "Office and Commercial Furniture (5021)")]
3109-
OfficeAndCommercialFurniture,
3110-
#[serde(rename = "Computer Network/Information Services (4816)")]
3111-
ComputerNetworkInformationServices,
3112-
#[serde(rename = "Shoe Stores (5661)")]
3113-
ShoeStores,
3099+
#[diesel(sql_type = Text)]
3100+
pub struct MerchantCategoryCode(String);
3101+
3102+
impl MerchantCategoryCode {
3103+
pub fn get_code(&self) -> Result<u16, InvalidMccError> {
3104+
// since self.0 is private field we can safely ensure self.0 is string "0001"-"9999"
3105+
self.0.parse::<u16>().map_err(|_| InvalidMccError {
3106+
message: "Invalid MCC code found".to_string(),
3107+
})
3108+
}
3109+
3110+
pub fn new(code: u16) -> Result<Self, InvalidMccError> {
3111+
if code >= 10000 {
3112+
return Err(InvalidMccError {
3113+
message: "MCC should be in range 0001 to 9999".to_string(),
3114+
});
3115+
}
3116+
let formatted = format!("{code:04}");
3117+
3118+
Ok(Self(formatted))
3119+
}
3120+
3121+
pub fn get_category_name(&self) -> Result<&str, InvalidMccError> {
3122+
let code = self.get_code()?;
3123+
match code {
3124+
// specific mapping needs to be depricated
3125+
5411 => Ok("Grocery Stores, Supermarkets (5411)"),
3126+
7011 => Ok("Lodging-Hotels, Motels, Resorts-not elsewhere classified (7011)"),
3127+
763 => Ok("Agricultural Cooperatives (0763)"),
3128+
8111 => Ok("Attorneys, Legal Services (8111)"),
3129+
5021 => Ok("Office and Commercial Furniture (5021)"),
3130+
4816 => Ok("Computer Network/Information Services (4816)"),
3131+
5661 => Ok("Shoe Stores (5661)"),
3132+
3133+
_ => Err(InvalidMccError {
3134+
message: format!("Category name not found for {}", code),
3135+
}),
3136+
}
3137+
}
3138+
3139+
/// Returns a reference to the 4-digit zero-padded code string.
3140+
pub fn get_string_code(&self) -> &str {
3141+
&self.0
3142+
}
31143143
}
31153144

3116-
#[derive(
3117-
Clone,
3118-
Copy,
3119-
Debug,
3120-
Eq,
3121-
Hash,
3122-
PartialEq,
3123-
serde::Deserialize,
3124-
serde::Serialize,
3125-
strum::Display,
3126-
strum::EnumString,
3127-
strum::EnumIter,
3128-
strum::VariantNames,
3129-
ToSchema,
3130-
)]
3131-
#[router_derive::diesel_enum(storage_type = "text")]
3132-
pub enum MerchantCategoryCode {
3133-
#[serde(rename = "5411")]
3134-
#[strum(serialize = "5411")]
3135-
Mcc5411,
3136-
#[serde(rename = "7011")]
3137-
#[strum(serialize = "7011")]
3138-
Mcc7011,
3139-
#[serde(rename = "0763")]
3140-
#[strum(serialize = "0763")]
3141-
Mcc0763,
3142-
#[serde(rename = "8111")]
3143-
#[strum(serialize = "8111")]
3144-
Mcc8111,
3145-
#[serde(rename = "5021")]
3146-
#[strum(serialize = "5021")]
3147-
Mcc5021,
3148-
#[serde(rename = "4816")]
3149-
#[strum(serialize = "4816")]
3150-
Mcc4816,
3151-
#[serde(rename = "5661")]
3152-
#[strum(serialize = "5661")]
3153-
Mcc5661,
3145+
impl<DB> ToSql<Text, DB> for MerchantCategoryCode
3146+
where
3147+
DB: Backend,
3148+
String: ToSql<Text, DB>,
3149+
{
3150+
fn to_sql<'b>(&'b self, out: &mut Output<'b, '_, DB>) -> diesel::serialize::Result {
3151+
self.0.to_sql(out)
3152+
}
31543153
}
3154+
impl<DB: Backend> FromSql<Text, DB> for MerchantCategoryCode
3155+
where
3156+
String: FromSql<Text, DB>,
3157+
{
3158+
fn from_sql(bytes: DB::RawValue<'_>) -> diesel::deserialize::Result<Self> {
3159+
let code = <String as FromSql<Text, DB>>::from_sql(bytes)?;
31553160

3156-
impl MerchantCategoryCode {
3157-
pub fn to_merchant_category_name(&self) -> MerchantCategory {
3158-
match self {
3159-
Self::Mcc5411 => MerchantCategory::GroceryStoresSupermarkets,
3160-
Self::Mcc7011 => MerchantCategory::LodgingHotelsMotelsResorts,
3161-
Self::Mcc0763 => MerchantCategory::AgriculturalCooperatives,
3162-
Self::Mcc8111 => MerchantCategory::AttorneysLegalServices,
3163-
Self::Mcc5021 => MerchantCategory::OfficeAndCommercialFurniture,
3164-
Self::Mcc4816 => MerchantCategory::ComputerNetworkInformationServices,
3165-
Self::Mcc5661 => MerchantCategory::ShoeStores,
3161+
Self::from_str(&code).map_err(Into::into)
3162+
}
3163+
}
3164+
3165+
impl Serialize for MerchantCategoryCode {
3166+
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
3167+
where
3168+
S: Serializer,
3169+
{
3170+
serializer.serialize_str(&self.0)
3171+
}
3172+
}
3173+
3174+
#[derive(Debug, Clone)]
3175+
pub struct InvalidMccError {
3176+
pub message: String,
3177+
}
3178+
3179+
impl fmt::Display for InvalidMccError {
3180+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3181+
write!(f, "Invalid MCC: {}", self.message)
3182+
}
3183+
}
3184+
3185+
impl std::error::Error for InvalidMccError {}
3186+
3187+
impl From<std::num::ParseIntError> for InvalidMccError {
3188+
fn from(err: std::num::ParseIntError) -> Self {
3189+
Self {
3190+
message: format!("MCC must be a number: {}", err),
31663191
}
31673192
}
31683193
}
31693194

3195+
impl FromStr for MerchantCategoryCode {
3196+
type Err = InvalidMccError;
3197+
fn from_str(s: &str) -> Result<Self, Self::Err> {
3198+
let _code: u16 = s.parse()?;
3199+
3200+
if s.len() != 4 {
3201+
return Err(InvalidMccError {
3202+
message: format!(
3203+
"MCC must be exactly 4 characters long (e.g., '0001'), but got '{}'",
3204+
s
3205+
),
3206+
});
3207+
}
3208+
Ok(Self(s.to_string()))
3209+
}
3210+
}
3211+
3212+
impl<'de> Deserialize<'de> for MerchantCategoryCode {
3213+
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
3214+
where
3215+
D: Deserializer<'de>,
3216+
{
3217+
let code = String::deserialize(deserializer)?;
3218+
3219+
Self::from_str(&code).map_err(de::Error::custom)
3220+
}
3221+
}
3222+
3223+
impl fmt::Display for MerchantCategoryCode {
3224+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3225+
// Format as a zero-padded 4-digit string, which matches the expected enum serialization.
3226+
write!(f, "{:04}", self.0)
3227+
}
3228+
}
3229+
31703230
#[derive(serde::Serialize, serde::Deserialize, Clone, Debug, PartialEq)]
31713231
pub struct MerchantCategoryCodeWithName {
31723232
pub code: MerchantCategoryCode,
3173-
pub name: MerchantCategory,
3233+
pub name: String,
31743234
}
31753235

31763236
#[derive(

crates/euclid_wasm/src/lib.rs

Lines changed: 32 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,15 @@ struct SeedData {
5050
static SEED_DATA: OnceLock<SeedData> = OnceLock::new();
5151
static SEED_FOREX: OnceLock<currency_conversion_types::ExchangeRates> = OnceLock::new();
5252

53+
#[wasm_bindgen]
54+
extern "C" {
55+
#[wasm_bindgen(js_namespace = console, js_name = log)]
56+
fn console_log(s: &str);
57+
58+
#[wasm_bindgen(js_namespace = console, js_name = error)]
59+
fn console_error(s: &str);
60+
}
61+
5362
/// This function can be used by the frontend to educate wasm about the forex rates data.
5463
/// The input argument is a struct fields base_currency and conversion where later is all the conversions associated with the base_currency
5564
/// to all different currencies present.
@@ -100,10 +109,29 @@ pub fn get_two_letter_country_code() -> JsResult {
100109
/// along with their names.
101110
#[wasm_bindgen(js_name=getMerchantCategoryCodeWithName)]
102111
pub fn get_merchant_category_code_with_name() -> JsResult {
103-
let merchant_category_codes_with_name = MerchantCategoryCode::iter()
104-
.map(|mcc_value| MerchantCategoryCodeWithName {
105-
code: mcc_value,
106-
name: mcc_value.to_merchant_category_name(),
112+
let merchant_category_codes_with_name = vec![5411, 7011, 763, 8111, 5021, 4816, 5661]
113+
.into_iter()
114+
.filter_map(|mcc_value| match MerchantCategoryCode::new(mcc_value) {
115+
Ok(mcc) => match mcc.get_category_name() {
116+
Ok(mcc_name) => Some(MerchantCategoryCodeWithName {
117+
code: mcc.clone(),
118+
name: mcc_name.to_string(),
119+
}),
120+
Err(err) => {
121+
console_error(&format!(
122+
"Failed to get category name for MCC {}: {:?}",
123+
mcc_value, err
124+
));
125+
None
126+
}
127+
},
128+
Err(err) => {
129+
console_error(&format!(
130+
"Failed to create MCC for value {}: {:?}",
131+
mcc_value, err
132+
));
133+
None
134+
}
107135
})
108136
.collect::<Vec<_>>();
109137

crates/router/src/core/payments/operations/payment_confirm.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1463,7 +1463,7 @@ impl<F: Clone + Send + Sync> Domain<F, api::PaymentsRequest, PaymentData<F>> for
14631463
let merchant_details = Some(unified_authentication_service::MerchantDetails {
14641464
merchant_id: Some(authentication.merchant_id.get_string_repr().to_string()),
14651465
merchant_name: acquirer_configs.clone().map(|detail| detail.merchant_name.clone()).or(metadata.clone().and_then(|metadata| metadata.merchant_name)),
1466-
merchant_category_code: business_profile.merchant_category_code.or(metadata.clone().and_then(|metadata| metadata.merchant_category_code)),
1466+
merchant_category_code: business_profile.merchant_category_code.clone().or(metadata.clone().and_then(|metadata| metadata.merchant_category_code)),
14671467
endpoint_prefix: metadata.clone().and_then(|metadata| metadata.endpoint_prefix),
14681468
three_ds_requestor_url: business_profile.authentication_connector_details.clone().map(|details| details.three_ds_requestor_url),
14691469
three_ds_requestor_id: metadata.clone().and_then(|metadata| metadata.three_ds_requestor_id),

0 commit comments

Comments
 (0)