diff --git a/api-reference/openapi_spec.json b/api-reference/openapi_spec.json index eeb68e9d04..4bb9346942 100644 --- a/api-reference/openapi_spec.json +++ b/api-reference/openapi_spec.json @@ -6821,6 +6821,15 @@ "properties": { "domain_name": { "type": "string", + "description": "Custom domain name to be used for hosting the link in your own domain", + "nullable": true + }, + "business_specific_configs": { + "type": "object", + "description": "list of configs for multi theme setup", + "additionalProperties": { + "$ref": "#/components/schemas/PaymentLinkConfigRequest" + }, "nullable": true } } @@ -14978,6 +14987,11 @@ ], "nullable": true }, + "payment_link_config_id": { + "type": "string", + "description": "custom payment link config id set at business profile send only if business_specific_configs is configured", + "nullable": true + }, "payment_type": { "allOf": [ { @@ -15311,6 +15325,11 @@ ], "nullable": true }, + "payment_link_config_id": { + "type": "string", + "description": "custom payment link config id set at business profile send only if business_specific_configs is configured", + "nullable": true + }, "profile_id": { "type": "string", "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 @@ ], "nullable": true }, + "payment_link_config_id": { + "type": "string", + "description": "custom payment link config id set at business profile send only if business_specific_configs is configured", + "nullable": true + }, "profile_id": { "type": "string", "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 @@ ], "nullable": true }, + "payment_link_config_id": { + "type": "string", + "description": "custom payment link config id set at business profile send only if business_specific_configs is configured", + "nullable": true + }, "surcharge_details": { "allOf": [ { diff --git a/crates/api_models/src/admin.rs b/crates/api_models/src/admin.rs index e1ebb547f0..81761fac70 100644 --- a/crates/api_models/src/admin.rs +++ b/crates/api_models/src/admin.rs @@ -1159,9 +1159,14 @@ pub struct BusinessGenericLinkConfig { #[derive(Clone, Debug, serde::Deserialize, serde::Serialize, PartialEq, ToSchema)] pub struct BusinessPaymentLinkConfig { + /// Custom domain name to be used for hosting the link in your own domain pub domain_name: Option, + /// Default payment link config for all future payment link #[serde(flatten)] - pub config: PaymentLinkConfigRequest, + #[schema(value_type = PaymentLinkConfigRequest)] + pub default_config: Option, + /// list of configs for multi theme setup + pub business_specific_configs: Option>, } #[derive(Clone, Debug, serde::Deserialize, serde::Serialize, PartialEq, ToSchema)] diff --git a/crates/api_models/src/payments.rs b/crates/api_models/src/payments.rs index 08388f8db2..cd36c53a12 100644 --- a/crates/api_models/src/payments.rs +++ b/crates/api_models/src/payments.rs @@ -469,6 +469,9 @@ pub struct PaymentsRequest { #[schema(value_type = Option)] pub payment_link_config: Option, + /// custom payment link config id set at business profile send only if business_specific_configs is configured + pub payment_link_config_id: Option, + /// The business profile to use for this payment, if not passed the default business profile /// associated with the merchant account will be used. #[remove_in(PaymentsUpdateRequest, PaymentsConfirmRequest)] @@ -5034,7 +5037,7 @@ pub enum PaymentLinkData<'a> { #[derive(Debug, serde::Serialize, Clone)] pub struct PaymentLinkDetails { - pub amount: String, + pub amount: StringMajorUnit, pub currency: api_enums::Currency, pub pub_key: String, pub client_secret: String, @@ -5055,7 +5058,7 @@ pub struct PaymentLinkDetails { #[derive(Debug, serde::Serialize)] pub struct PaymentLinkStatusDetails { - pub amount: String, + pub amount: StringMajorUnit, pub currency: api_enums::Currency, pub payment_id: String, pub merchant_logo: String, @@ -5129,7 +5132,8 @@ pub struct PaymentLinkListResponse { pub struct PaymentCreatePaymentLinkConfig { #[serde(flatten)] #[schema(value_type = Option)] - pub config: admin::PaymentLinkConfigRequest, + /// Theme config for the particular payment + pub theme_config: admin::PaymentLinkConfigRequest, } #[derive(Debug, Default, Eq, PartialEq, serde::Deserialize, serde::Serialize, Clone, ToSchema)] @@ -5141,7 +5145,7 @@ pub struct OrderDetailsWithStringAmount { #[schema(example = 1)] pub quantity: u16, /// the amount per quantity of product - pub amount: String, + pub amount: StringMajorUnit, /// Product Image link pub product_img_link: Option, } diff --git a/crates/common_utils/src/types.rs b/crates/common_utils/src/types.rs index 36f40da2dc..f469a0299f 100644 --- a/crates/common_utils/src/types.rs +++ b/crates/common_utils/src/types.rs @@ -251,6 +251,28 @@ impl AmountConvertor for StringMinorUnitForConnector { } } +/// Core required conversion type +#[derive(Default, Debug, serde::Deserialize, serde::Serialize, Clone, Copy, PartialEq)] +pub struct StringMajorUnitForCore; +impl AmountConvertor for StringMajorUnitForCore { + type Output = StringMajorUnit; + fn convert( + &self, + amount: MinorUnit, + currency: enums::Currency, + ) -> Result> { + amount.to_major_unit_as_string(currency) + } + + fn convert_back( + &self, + amount: StringMajorUnit, + currency: enums::Currency, + ) -> Result> { + amount.to_minor_unit_as_i64(currency) + } +} + /// Connector required amount type #[derive(Default, Debug, serde::Deserialize, serde::Serialize, Clone, Copy, PartialEq)] pub struct StringMajorUnitForConnector; 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 053eb493ce..faca8cd7bb 100644 --- a/crates/hyperswitch_domain_models/src/errors/api_error_response.rs +++ b/crates/hyperswitch_domain_models/src/errors/api_error_response.rs @@ -275,6 +275,8 @@ pub enum ApiErrorResponse { MissingTenantId, #[error(error_type = ErrorType::ProcessingError, code = "HE_06", message = "Invalid tenant id: {tenant_id}")] InvalidTenant { tenant_id: String }, + #[error(error_type = ErrorType::ValidationError, code = "HE_01", message = "Failed to convert amount to {amount_type} type")] + AmountConversionFailed { amount_type: &'static str }, } #[derive(Clone)] @@ -613,6 +615,9 @@ impl ErrorSwitch for ApiErrorRespon Self::InvalidTenant { tenant_id } => { AER::InternalServerError(ApiError::new("HE", 6, format!("Invalid Tenant {tenant_id}"), None)) } + Self::AmountConversionFailed { amount_type } => { + AER::InternalServerError(ApiError::new("HE", 6, format!("Failed to convert amount to {amount_type} type"), None)) + } } } } diff --git a/crates/router/src/compatibility/stripe/errors.rs b/crates/router/src/compatibility/stripe/errors.rs index 17fcea9b82..0e538bb338 100644 --- a/crates/router/src/compatibility/stripe/errors.rs +++ b/crates/router/src/compatibility/stripe/errors.rs @@ -266,6 +266,8 @@ pub enum StripeErrorCode { ExtendedCardInfoNotFound, #[error(error_type = StripeErrorType::InvalidRequestError, code = "IR_28", message = "Invalid tenant")] InvalidTenant, + #[error(error_type = StripeErrorType::HyperswitchError, code = "HE_01", message = "Failed to convert amount to {amount_type} type")] + AmountConversionFailed { amount_type: &'static str }, // [#216]: https://github.com/juspay/hyperswitch/issues/216 // Implement the remaining stripe error codes @@ -650,6 +652,9 @@ impl From for StripeErrorCode { errors::ApiErrorResponse::ExtendedCardInfoNotFound => Self::ExtendedCardInfoNotFound, errors::ApiErrorResponse::InvalidTenant { tenant_id: _ } | errors::ApiErrorResponse::MissingTenantId => Self::InvalidTenant, + errors::ApiErrorResponse::AmountConversionFailed { amount_type } => { + Self::AmountConversionFailed { amount_type } + } } } } @@ -730,7 +735,8 @@ impl actix_web::ResponseError for StripeErrorCode { | Self::MandateActive | Self::CustomerRedacted | Self::WebhookProcessingError - | Self::InvalidTenant => StatusCode::INTERNAL_SERVER_ERROR, + | Self::InvalidTenant + | Self::AmountConversionFailed { .. } => StatusCode::INTERNAL_SERVER_ERROR, Self::ReturnUrlUnavailable => StatusCode::SERVICE_UNAVAILABLE, Self::ExternalConnectorError { status_code, .. } => { StatusCode::from_u16(*status_code).unwrap_or(StatusCode::INTERNAL_SERVER_ERROR) diff --git a/crates/router/src/core/payment_link.rs b/crates/router/src/core/payment_link.rs index 99bff1fd69..566f9d86aa 100644 --- a/crates/router/src/core/payment_link.rs +++ b/crates/router/src/core/payment_link.rs @@ -5,6 +5,7 @@ use common_utils::{ DEFAULT_MERCHANT_LOGO, DEFAULT_PRODUCT_IMG, DEFAULT_SDK_LAYOUT, DEFAULT_SESSION_EXPIRY, }, ext_traits::{OptionExt, ValueExt}, + types::{AmountConvertor, MinorUnit, StringMajorUnitForCore}, }; use error_stack::ResultExt; use futures::future; @@ -14,6 +15,7 @@ use time::PrimitiveDateTime; use super::errors::{self, RouterResult, StorageErrorExt}; use crate::{ errors::RouterResponse, + get_payment_link_config_value, get_payment_link_config_value_based_on_priority, routes::SessionState, services, types::{ @@ -121,9 +123,15 @@ pub async fn initiate_payment_link_flow( payment_intent.currency, payment_intent.client_secret.clone(), )?; - let amount = currency - .to_currency_base_unit(payment_intent.amount.get_amount_as_i64()) - .change_context(errors::ApiErrorResponse::CurrencyConversionFailed)?; + + let required_conversion_type = StringMajorUnitForCore; + + let amount = required_conversion_type + .convert(payment_intent.amount, currency) + .change_context(errors::ApiErrorResponse::AmountConversionFailed { + amount_type: "StringMajorUnit", + })?; + let order_details = validate_order_details(payment_intent.order_details.clone(), currency)?; let session_expiry = payment_link.fulfilment_time.unwrap_or_else(|| { @@ -325,6 +333,7 @@ fn validate_order_details( Option>, error_stack::Report, > { + let required_conversion_type = StringMajorUnitForCore; let order_details = order_details .map(|order_details| { order_details @@ -356,10 +365,11 @@ fn validate_order_details( .product_img_link .clone_from(&order.product_img_link) }; - order_details_amount_string.amount = - currency - .to_currency_base_unit(order.amount) - .change_context(errors::ApiErrorResponse::CurrencyConversionFailed)?; + order_details_amount_string.amount = required_conversion_type + .convert(MinorUnit::new(order.amount), currency) + .change_context(errors::ApiErrorResponse::AmountConversionFailed { + amount_type: "StringMajorUnit", + })?; order_details_amount_string.product_name = capitalize_first_char(&order.product_name.clone()); order_details_amount_string.quantity = order.quantity; @@ -386,9 +396,11 @@ pub fn get_payment_link_config_based_on_priority( business_link_config: Option, merchant_name: String, default_domain_name: String, + payment_link_config_id: Option, ) -> Result<(admin_types::PaymentLinkConfig, String), error_stack::Report> { - let (domain_name, business_config) = if let Some(business_config) = business_link_config { + let (domain_name, business_theme_configs) = if let Some(business_config) = business_link_config + { let extracted_value: api_models::admin::BusinessPaymentLinkConfig = business_config .parse_value("BusinessPaymentLinkConfig") .change_context(errors::ApiErrorResponse::InvalidDataValue { @@ -402,73 +414,32 @@ pub fn get_payment_link_config_based_on_priority( .clone() .map(|d_name| format!("https://{}", d_name)) .unwrap_or_else(|| default_domain_name.clone()), - Some(extracted_value.config), + payment_link_config_id + .and_then(|id| { + extracted_value + .business_specific_configs + .as_ref() + .and_then(|specific_configs| specific_configs.get(&id).cloned()) + }) + .or(extracted_value.default_config), ) } else { (default_domain_name, None) }; - let theme = payment_create_link_config - .as_ref() - .and_then(|pc_config| pc_config.config.theme.clone()) - .or_else(|| { - business_config - .as_ref() - .and_then(|business_config| business_config.theme.clone()) - }) - .unwrap_or(DEFAULT_BACKGROUND_COLOR.to_string()); - - let logo = payment_create_link_config - .as_ref() - .and_then(|pc_config| pc_config.config.logo.clone()) - .or_else(|| { - business_config - .as_ref() - .and_then(|business_config| business_config.logo.clone()) - }) - .unwrap_or(DEFAULT_MERCHANT_LOGO.to_string()); - - let seller_name = payment_create_link_config - .as_ref() - .and_then(|pc_config| pc_config.config.seller_name.clone()) - .or_else(|| { - business_config - .as_ref() - .and_then(|business_config| business_config.seller_name.clone()) - }) - .unwrap_or(merchant_name.clone()); - - let sdk_layout = payment_create_link_config - .as_ref() - .and_then(|pc_config| pc_config.config.sdk_layout.clone()) - .or_else(|| { - business_config - .as_ref() - .and_then(|business_config| business_config.sdk_layout.clone()) - }) - .unwrap_or(DEFAULT_SDK_LAYOUT.to_owned()); - - let display_sdk_only = payment_create_link_config - .as_ref() - .and_then(|pc_config| { - pc_config.config.display_sdk_only.or_else(|| { - business_config - .as_ref() - .and_then(|business_config| business_config.display_sdk_only) - }) - }) - .unwrap_or(DEFAULT_DISPLAY_SDK_ONLY); - - let enabled_saved_payment_method = payment_create_link_config - .as_ref() - .and_then(|pc_config| { - pc_config.config.enabled_saved_payment_method.or_else(|| { - business_config - .as_ref() - .and_then(|business_config| business_config.enabled_saved_payment_method) - }) - }) - .unwrap_or(DEFAULT_ENABLE_SAVED_PAYMENT_METHOD); + let (theme, logo, seller_name, sdk_layout, display_sdk_only, enabled_saved_payment_method) = get_payment_link_config_value!( + payment_create_link_config, + business_theme_configs, + (theme, DEFAULT_BACKGROUND_COLOR.to_string()), + (logo, DEFAULT_MERCHANT_LOGO.to_string()), + (seller_name, merchant_name.clone()), + (sdk_layout, DEFAULT_SDK_LAYOUT.to_owned()), + (display_sdk_only, DEFAULT_DISPLAY_SDK_ONLY), + ( + enabled_saved_payment_method, + DEFAULT_ENABLE_SAVED_PAYMENT_METHOD + ) + ); let payment_link_config = admin_types::PaymentLinkConfig { theme, @@ -567,9 +538,13 @@ pub async fn get_payment_link_status( field_name: "currency", })?; - let amount = currency - .to_currency_base_unit(payment_attempt.net_amount.get_amount_as_i64()) - .change_context(errors::ApiErrorResponse::CurrencyConversionFailed)?; + let required_conversion_type = StringMajorUnitForCore; + + let amount = required_conversion_type + .convert(payment_attempt.net_amount, currency) + .change_context(errors::ApiErrorResponse::AmountConversionFailed { + amount_type: "StringMajorUnit", + })?; // converting first letter of merchant name to upperCase let merchant_name = capitalize_first_char(&payment_link_config.seller_name); diff --git a/crates/router/src/core/payment_link/payment_link_initiate/payment_link.js b/crates/router/src/core/payment_link/payment_link_initiate/payment_link.js index 7ab10e4d27..1a28b6ecf8 100644 --- a/crates/router/src/core/payment_link/payment_link_initiate/payment_link.js +++ b/crates/router/src/core/payment_link/payment_link_initiate/payment_link.js @@ -216,18 +216,18 @@ function boot() { "quantity": null }); } + } - if (paymentDetails.merchant_name) { - document.title = "Payment requested by " + paymentDetails.merchant_name; - } + if (paymentDetails.merchant_name) { + document.title = "Payment requested by " + paymentDetails.merchant_name; + } - if (paymentDetails.merchant_logo) { - var link = document.createElement("link"); - link.rel = "icon"; - link.href = paymentDetails.merchant_logo; - link.type = "image/x-icon"; - document.head.appendChild(link); - } + if (paymentDetails.merchant_logo) { + var link = document.createElement("link"); + link.rel = "icon"; + link.href = paymentDetails.merchant_logo; + link.type = "image/x-icon"; + document.head.appendChild(link); } // Render UI diff --git a/crates/router/src/core/payments/operations/payment_create.rs b/crates/router/src/core/payments/operations/payment_create.rs index 63ae07868a..718d12f1c0 100644 --- a/crates/router/src/core/payments/operations/payment_create.rs +++ b/crates/router/src/core/payments/operations/payment_create.rs @@ -209,12 +209,12 @@ impl GetTracker, api::PaymentsRequest> for Pa ), )); - let payment_link_data = if let Some(payment_link_create) = request.payment_link { - if payment_link_create { + let payment_link_data = match request.payment_link { + Some(true) => { let merchant_name = merchant_account .merchant_name .clone() - .map(|merchant_name| merchant_name.into_inner().peek().to_owned()) + .map(|name| name.into_inner().peek().to_owned()) .unwrap_or_default(); let default_domain_name = state.base_url.clone(); @@ -225,7 +225,9 @@ impl GetTracker, api::PaymentsRequest> for Pa business_profile.payment_link_config.clone(), merchant_name, default_domain_name, + request.payment_link_config_id.clone(), )?; + create_payment_link( request, payment_link_config, @@ -239,11 +241,8 @@ impl GetTracker, api::PaymentsRequest> for Pa session_expiry, ) .await? - } else { - None } - } else { - None + _ => None, }; let payment_intent_new = Self::make_payment_intent( diff --git a/crates/router/src/macros.rs b/crates/router/src/macros.rs index f7a5c645d2..962bdb9652 100644 --- a/crates/router/src/macros.rs +++ b/crates/router/src/macros.rs @@ -9,3 +9,27 @@ macro_rules! get_formatted_date_time { .change_context($crate::core::errors::ConnectorError::InvalidDateFormat) }}; } + +#[macro_export] +macro_rules! get_payment_link_config_value_based_on_priority { + ($config:expr, $business_config:expr, $field:ident, $default:expr) => { + $config + .as_ref() + .and_then(|pc_config| pc_config.theme_config.$field.clone()) + .or_else(|| { + $business_config + .as_ref() + .and_then(|business_config| business_config.$field.clone()) + }) + .unwrap_or($default) + }; +} + +#[macro_export] +macro_rules! get_payment_link_config_value { + ($config:expr, $business_config:expr, $(($field:ident, $default:expr)),*) => { + ( + $(get_payment_link_config_value_based_on_priority!($config, $business_config, $field, $default)),* + ) + }; +}