Skip to content

Commit ecc6c00

Browse files
feat(payment_link): add multiple custom css support in business level (#5137)
Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com>
1 parent 1b89463 commit ecc6c00

File tree

10 files changed

+165
-96
lines changed

10 files changed

+165
-96
lines changed

api-reference/openapi_spec.json

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6821,6 +6821,15 @@
68216821
"properties": {
68226822
"domain_name": {
68236823
"type": "string",
6824+
"description": "Custom domain name to be used for hosting the link in your own domain",
6825+
"nullable": true
6826+
},
6827+
"business_specific_configs": {
6828+
"type": "object",
6829+
"description": "list of configs for multi theme setup",
6830+
"additionalProperties": {
6831+
"$ref": "#/components/schemas/PaymentLinkConfigRequest"
6832+
},
68246833
"nullable": true
68256834
}
68266835
}
@@ -14978,6 +14987,11 @@
1497814987
],
1497914988
"nullable": true
1498014989
},
14990+
"payment_link_config_id": {
14991+
"type": "string",
14992+
"description": "custom payment link config id set at business profile send only if business_specific_configs is configured",
14993+
"nullable": true
14994+
},
1498114995
"payment_type": {
1498214996
"allOf": [
1498314997
{
@@ -15311,6 +15325,11 @@
1531115325
],
1531215326
"nullable": true
1531315327
},
15328+
"payment_link_config_id": {
15329+
"type": "string",
15330+
"description": "custom payment link config id set at business profile send only if business_specific_configs is configured",
15331+
"nullable": true
15332+
},
1531415333
"profile_id": {
1531515334
"type": "string",
1531615335
"description": "The business profile to use for this payment, if not passed the default business profile\nassociated with the merchant account will be used.",
@@ -16351,6 +16370,11 @@
1635116370
],
1635216371
"nullable": true
1635316372
},
16373+
"payment_link_config_id": {
16374+
"type": "string",
16375+
"description": "custom payment link config id set at business profile send only if business_specific_configs is configured",
16376+
"nullable": true
16377+
},
1635416378
"profile_id": {
1635516379
"type": "string",
1635616380
"description": "The business profile to use for this payment, if not passed the default business profile\nassociated with the merchant account will be used.",
@@ -17359,6 +17383,11 @@
1735917383
],
1736017384
"nullable": true
1736117385
},
17386+
"payment_link_config_id": {
17387+
"type": "string",
17388+
"description": "custom payment link config id set at business profile send only if business_specific_configs is configured",
17389+
"nullable": true
17390+
},
1736217391
"surcharge_details": {
1736317392
"allOf": [
1736417393
{

crates/api_models/src/admin.rs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1159,9 +1159,14 @@ pub struct BusinessGenericLinkConfig {
11591159

11601160
#[derive(Clone, Debug, serde::Deserialize, serde::Serialize, PartialEq, ToSchema)]
11611161
pub struct BusinessPaymentLinkConfig {
1162+
/// Custom domain name to be used for hosting the link in your own domain
11621163
pub domain_name: Option<String>,
1164+
/// Default payment link config for all future payment link
11631165
#[serde(flatten)]
1164-
pub config: PaymentLinkConfigRequest,
1166+
#[schema(value_type = PaymentLinkConfigRequest)]
1167+
pub default_config: Option<PaymentLinkConfigRequest>,
1168+
/// list of configs for multi theme setup
1169+
pub business_specific_configs: Option<HashMap<String, PaymentLinkConfigRequest>>,
11651170
}
11661171

11671172
#[derive(Clone, Debug, serde::Deserialize, serde::Serialize, PartialEq, ToSchema)]

crates/api_models/src/payments.rs

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -469,6 +469,9 @@ pub struct PaymentsRequest {
469469
#[schema(value_type = Option<PaymentCreatePaymentLinkConfig>)]
470470
pub payment_link_config: Option<PaymentCreatePaymentLinkConfig>,
471471

472+
/// custom payment link config id set at business profile send only if business_specific_configs is configured
473+
pub payment_link_config_id: Option<String>,
474+
472475
/// The business profile to use for this payment, if not passed the default business profile
473476
/// associated with the merchant account will be used.
474477
#[remove_in(PaymentsUpdateRequest, PaymentsConfirmRequest)]
@@ -5034,7 +5037,7 @@ pub enum PaymentLinkData<'a> {
50345037

50355038
#[derive(Debug, serde::Serialize, Clone)]
50365039
pub struct PaymentLinkDetails {
5037-
pub amount: String,
5040+
pub amount: StringMajorUnit,
50385041
pub currency: api_enums::Currency,
50395042
pub pub_key: String,
50405043
pub client_secret: String,
@@ -5055,7 +5058,7 @@ pub struct PaymentLinkDetails {
50555058

50565059
#[derive(Debug, serde::Serialize)]
50575060
pub struct PaymentLinkStatusDetails {
5058-
pub amount: String,
5061+
pub amount: StringMajorUnit,
50595062
pub currency: api_enums::Currency,
50605063
pub payment_id: String,
50615064
pub merchant_logo: String,
@@ -5129,7 +5132,8 @@ pub struct PaymentLinkListResponse {
51295132
pub struct PaymentCreatePaymentLinkConfig {
51305133
#[serde(flatten)]
51315134
#[schema(value_type = Option<PaymentLinkConfigRequest>)]
5132-
pub config: admin::PaymentLinkConfigRequest,
5135+
/// Theme config for the particular payment
5136+
pub theme_config: admin::PaymentLinkConfigRequest,
51335137
}
51345138

51355139
#[derive(Debug, Default, Eq, PartialEq, serde::Deserialize, serde::Serialize, Clone, ToSchema)]
@@ -5141,7 +5145,7 @@ pub struct OrderDetailsWithStringAmount {
51415145
#[schema(example = 1)]
51425146
pub quantity: u16,
51435147
/// the amount per quantity of product
5144-
pub amount: String,
5148+
pub amount: StringMajorUnit,
51455149
/// Product Image link
51465150
pub product_img_link: Option<String>,
51475151
}

crates/common_utils/src/types.rs

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -251,6 +251,28 @@ impl AmountConvertor for StringMinorUnitForConnector {
251251
}
252252
}
253253

254+
/// Core required conversion type
255+
#[derive(Default, Debug, serde::Deserialize, serde::Serialize, Clone, Copy, PartialEq)]
256+
pub struct StringMajorUnitForCore;
257+
impl AmountConvertor for StringMajorUnitForCore {
258+
type Output = StringMajorUnit;
259+
fn convert(
260+
&self,
261+
amount: MinorUnit,
262+
currency: enums::Currency,
263+
) -> Result<Self::Output, error_stack::Report<ParsingError>> {
264+
amount.to_major_unit_as_string(currency)
265+
}
266+
267+
fn convert_back(
268+
&self,
269+
amount: StringMajorUnit,
270+
currency: enums::Currency,
271+
) -> Result<MinorUnit, error_stack::Report<ParsingError>> {
272+
amount.to_minor_unit_as_i64(currency)
273+
}
274+
}
275+
254276
/// Connector required amount type
255277
#[derive(Default, Debug, serde::Deserialize, serde::Serialize, Clone, Copy, PartialEq)]
256278
pub struct StringMajorUnitForConnector;

crates/hyperswitch_domain_models/src/errors/api_error_response.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -275,6 +275,8 @@ pub enum ApiErrorResponse {
275275
MissingTenantId,
276276
#[error(error_type = ErrorType::ProcessingError, code = "HE_06", message = "Invalid tenant id: {tenant_id}")]
277277
InvalidTenant { tenant_id: String },
278+
#[error(error_type = ErrorType::ValidationError, code = "HE_01", message = "Failed to convert amount to {amount_type} type")]
279+
AmountConversionFailed { amount_type: &'static str },
278280
}
279281

280282
#[derive(Clone)]
@@ -613,6 +615,9 @@ impl ErrorSwitch<api_models::errors::types::ApiErrorResponse> for ApiErrorRespon
613615
Self::InvalidTenant { tenant_id } => {
614616
AER::InternalServerError(ApiError::new("HE", 6, format!("Invalid Tenant {tenant_id}"), None))
615617
}
618+
Self::AmountConversionFailed { amount_type } => {
619+
AER::InternalServerError(ApiError::new("HE", 6, format!("Failed to convert amount to {amount_type} type"), None))
620+
}
616621
}
617622
}
618623
}

crates/router/src/compatibility/stripe/errors.rs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -266,6 +266,8 @@ pub enum StripeErrorCode {
266266
ExtendedCardInfoNotFound,
267267
#[error(error_type = StripeErrorType::InvalidRequestError, code = "IR_28", message = "Invalid tenant")]
268268
InvalidTenant,
269+
#[error(error_type = StripeErrorType::HyperswitchError, code = "HE_01", message = "Failed to convert amount to {amount_type} type")]
270+
AmountConversionFailed { amount_type: &'static str },
269271
// [#216]: https://github.com/juspay/hyperswitch/issues/216
270272
// Implement the remaining stripe error codes
271273

@@ -650,6 +652,9 @@ impl From<errors::ApiErrorResponse> for StripeErrorCode {
650652
errors::ApiErrorResponse::ExtendedCardInfoNotFound => Self::ExtendedCardInfoNotFound,
651653
errors::ApiErrorResponse::InvalidTenant { tenant_id: _ }
652654
| errors::ApiErrorResponse::MissingTenantId => Self::InvalidTenant,
655+
errors::ApiErrorResponse::AmountConversionFailed { amount_type } => {
656+
Self::AmountConversionFailed { amount_type }
657+
}
653658
}
654659
}
655660
}
@@ -730,7 +735,8 @@ impl actix_web::ResponseError for StripeErrorCode {
730735
| Self::MandateActive
731736
| Self::CustomerRedacted
732737
| Self::WebhookProcessingError
733-
| Self::InvalidTenant => StatusCode::INTERNAL_SERVER_ERROR,
738+
| Self::InvalidTenant
739+
| Self::AmountConversionFailed { .. } => StatusCode::INTERNAL_SERVER_ERROR,
734740
Self::ReturnUrlUnavailable => StatusCode::SERVICE_UNAVAILABLE,
735741
Self::ExternalConnectorError { status_code, .. } => {
736742
StatusCode::from_u16(*status_code).unwrap_or(StatusCode::INTERNAL_SERVER_ERROR)

crates/router/src/core/payment_link.rs

Lines changed: 48 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ use common_utils::{
55
DEFAULT_MERCHANT_LOGO, DEFAULT_PRODUCT_IMG, DEFAULT_SDK_LAYOUT, DEFAULT_SESSION_EXPIRY,
66
},
77
ext_traits::{OptionExt, ValueExt},
8+
types::{AmountConvertor, MinorUnit, StringMajorUnitForCore},
89
};
910
use error_stack::ResultExt;
1011
use futures::future;
@@ -14,6 +15,7 @@ use time::PrimitiveDateTime;
1415
use super::errors::{self, RouterResult, StorageErrorExt};
1516
use crate::{
1617
errors::RouterResponse,
18+
get_payment_link_config_value, get_payment_link_config_value_based_on_priority,
1719
routes::SessionState,
1820
services,
1921
types::{
@@ -121,9 +123,15 @@ pub async fn initiate_payment_link_flow(
121123
payment_intent.currency,
122124
payment_intent.client_secret.clone(),
123125
)?;
124-
let amount = currency
125-
.to_currency_base_unit(payment_intent.amount.get_amount_as_i64())
126-
.change_context(errors::ApiErrorResponse::CurrencyConversionFailed)?;
126+
127+
let required_conversion_type = StringMajorUnitForCore;
128+
129+
let amount = required_conversion_type
130+
.convert(payment_intent.amount, currency)
131+
.change_context(errors::ApiErrorResponse::AmountConversionFailed {
132+
amount_type: "StringMajorUnit",
133+
})?;
134+
127135
let order_details = validate_order_details(payment_intent.order_details.clone(), currency)?;
128136

129137
let session_expiry = payment_link.fulfilment_time.unwrap_or_else(|| {
@@ -325,6 +333,7 @@ fn validate_order_details(
325333
Option<Vec<api_models::payments::OrderDetailsWithStringAmount>>,
326334
error_stack::Report<errors::ApiErrorResponse>,
327335
> {
336+
let required_conversion_type = StringMajorUnitForCore;
328337
let order_details = order_details
329338
.map(|order_details| {
330339
order_details
@@ -356,10 +365,11 @@ fn validate_order_details(
356365
.product_img_link
357366
.clone_from(&order.product_img_link)
358367
};
359-
order_details_amount_string.amount =
360-
currency
361-
.to_currency_base_unit(order.amount)
362-
.change_context(errors::ApiErrorResponse::CurrencyConversionFailed)?;
368+
order_details_amount_string.amount = required_conversion_type
369+
.convert(MinorUnit::new(order.amount), currency)
370+
.change_context(errors::ApiErrorResponse::AmountConversionFailed {
371+
amount_type: "StringMajorUnit",
372+
})?;
363373
order_details_amount_string.product_name =
364374
capitalize_first_char(&order.product_name.clone());
365375
order_details_amount_string.quantity = order.quantity;
@@ -386,9 +396,11 @@ pub fn get_payment_link_config_based_on_priority(
386396
business_link_config: Option<serde_json::Value>,
387397
merchant_name: String,
388398
default_domain_name: String,
399+
payment_link_config_id: Option<String>,
389400
) -> Result<(admin_types::PaymentLinkConfig, String), error_stack::Report<errors::ApiErrorResponse>>
390401
{
391-
let (domain_name, business_config) = if let Some(business_config) = business_link_config {
402+
let (domain_name, business_theme_configs) = if let Some(business_config) = business_link_config
403+
{
392404
let extracted_value: api_models::admin::BusinessPaymentLinkConfig = business_config
393405
.parse_value("BusinessPaymentLinkConfig")
394406
.change_context(errors::ApiErrorResponse::InvalidDataValue {
@@ -402,73 +414,32 @@ pub fn get_payment_link_config_based_on_priority(
402414
.clone()
403415
.map(|d_name| format!("https://{}", d_name))
404416
.unwrap_or_else(|| default_domain_name.clone()),
405-
Some(extracted_value.config),
417+
payment_link_config_id
418+
.and_then(|id| {
419+
extracted_value
420+
.business_specific_configs
421+
.as_ref()
422+
.and_then(|specific_configs| specific_configs.get(&id).cloned())
423+
})
424+
.or(extracted_value.default_config),
406425
)
407426
} else {
408427
(default_domain_name, None)
409428
};
410429

411-
let theme = payment_create_link_config
412-
.as_ref()
413-
.and_then(|pc_config| pc_config.config.theme.clone())
414-
.or_else(|| {
415-
business_config
416-
.as_ref()
417-
.and_then(|business_config| business_config.theme.clone())
418-
})
419-
.unwrap_or(DEFAULT_BACKGROUND_COLOR.to_string());
420-
421-
let logo = payment_create_link_config
422-
.as_ref()
423-
.and_then(|pc_config| pc_config.config.logo.clone())
424-
.or_else(|| {
425-
business_config
426-
.as_ref()
427-
.and_then(|business_config| business_config.logo.clone())
428-
})
429-
.unwrap_or(DEFAULT_MERCHANT_LOGO.to_string());
430-
431-
let seller_name = payment_create_link_config
432-
.as_ref()
433-
.and_then(|pc_config| pc_config.config.seller_name.clone())
434-
.or_else(|| {
435-
business_config
436-
.as_ref()
437-
.and_then(|business_config| business_config.seller_name.clone())
438-
})
439-
.unwrap_or(merchant_name.clone());
440-
441-
let sdk_layout = payment_create_link_config
442-
.as_ref()
443-
.and_then(|pc_config| pc_config.config.sdk_layout.clone())
444-
.or_else(|| {
445-
business_config
446-
.as_ref()
447-
.and_then(|business_config| business_config.sdk_layout.clone())
448-
})
449-
.unwrap_or(DEFAULT_SDK_LAYOUT.to_owned());
450-
451-
let display_sdk_only = payment_create_link_config
452-
.as_ref()
453-
.and_then(|pc_config| {
454-
pc_config.config.display_sdk_only.or_else(|| {
455-
business_config
456-
.as_ref()
457-
.and_then(|business_config| business_config.display_sdk_only)
458-
})
459-
})
460-
.unwrap_or(DEFAULT_DISPLAY_SDK_ONLY);
461-
462-
let enabled_saved_payment_method = payment_create_link_config
463-
.as_ref()
464-
.and_then(|pc_config| {
465-
pc_config.config.enabled_saved_payment_method.or_else(|| {
466-
business_config
467-
.as_ref()
468-
.and_then(|business_config| business_config.enabled_saved_payment_method)
469-
})
470-
})
471-
.unwrap_or(DEFAULT_ENABLE_SAVED_PAYMENT_METHOD);
430+
let (theme, logo, seller_name, sdk_layout, display_sdk_only, enabled_saved_payment_method) = get_payment_link_config_value!(
431+
payment_create_link_config,
432+
business_theme_configs,
433+
(theme, DEFAULT_BACKGROUND_COLOR.to_string()),
434+
(logo, DEFAULT_MERCHANT_LOGO.to_string()),
435+
(seller_name, merchant_name.clone()),
436+
(sdk_layout, DEFAULT_SDK_LAYOUT.to_owned()),
437+
(display_sdk_only, DEFAULT_DISPLAY_SDK_ONLY),
438+
(
439+
enabled_saved_payment_method,
440+
DEFAULT_ENABLE_SAVED_PAYMENT_METHOD
441+
)
442+
);
472443

473444
let payment_link_config = admin_types::PaymentLinkConfig {
474445
theme,
@@ -567,9 +538,13 @@ pub async fn get_payment_link_status(
567538
field_name: "currency",
568539
})?;
569540

570-
let amount = currency
571-
.to_currency_base_unit(payment_attempt.net_amount.get_amount_as_i64())
572-
.change_context(errors::ApiErrorResponse::CurrencyConversionFailed)?;
541+
let required_conversion_type = StringMajorUnitForCore;
542+
543+
let amount = required_conversion_type
544+
.convert(payment_attempt.net_amount, currency)
545+
.change_context(errors::ApiErrorResponse::AmountConversionFailed {
546+
amount_type: "StringMajorUnit",
547+
})?;
573548

574549
// converting first letter of merchant name to upperCase
575550
let merchant_name = capitalize_first_char(&payment_link_config.seller_name);

0 commit comments

Comments
 (0)