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>
This commit is contained in:
Sahkal Poddar
2024-07-01 16:29:06 +05:30
committed by GitHub
parent 1b8946321b
commit ecc6c00d4a
10 changed files with 165 additions and 96 deletions

View File

@ -6821,6 +6821,15 @@
"properties": { "properties": {
"domain_name": { "domain_name": {
"type": "string", "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 "nullable": true
} }
} }
@ -14978,6 +14987,11 @@
], ],
"nullable": true "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": { "payment_type": {
"allOf": [ "allOf": [
{ {
@ -15311,6 +15325,11 @@
], ],
"nullable": true "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": { "profile_id": {
"type": "string", "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.", "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 "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": { "profile_id": {
"type": "string", "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.", "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 "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": { "surcharge_details": {
"allOf": [ "allOf": [
{ {

View File

@ -1159,9 +1159,14 @@ pub struct BusinessGenericLinkConfig {
#[derive(Clone, Debug, serde::Deserialize, serde::Serialize, PartialEq, ToSchema)] #[derive(Clone, Debug, serde::Deserialize, serde::Serialize, PartialEq, ToSchema)]
pub struct BusinessPaymentLinkConfig { pub struct BusinessPaymentLinkConfig {
/// Custom domain name to be used for hosting the link in your own domain
pub domain_name: Option<String>, pub domain_name: Option<String>,
/// Default payment link config for all future payment link
#[serde(flatten)] #[serde(flatten)]
pub config: PaymentLinkConfigRequest, #[schema(value_type = PaymentLinkConfigRequest)]
pub default_config: Option<PaymentLinkConfigRequest>,
/// list of configs for multi theme setup
pub business_specific_configs: Option<HashMap<String, PaymentLinkConfigRequest>>,
} }
#[derive(Clone, Debug, serde::Deserialize, serde::Serialize, PartialEq, ToSchema)] #[derive(Clone, Debug, serde::Deserialize, serde::Serialize, PartialEq, ToSchema)]

View File

@ -469,6 +469,9 @@ pub struct PaymentsRequest {
#[schema(value_type = Option<PaymentCreatePaymentLinkConfig>)] #[schema(value_type = Option<PaymentCreatePaymentLinkConfig>)]
pub payment_link_config: Option<PaymentCreatePaymentLinkConfig>, pub payment_link_config: Option<PaymentCreatePaymentLinkConfig>,
/// custom payment link config id set at business profile send only if business_specific_configs is configured
pub payment_link_config_id: Option<String>,
/// The business profile to use for this payment, if not passed the default business profile /// The business profile to use for this payment, if not passed the default business profile
/// associated with the merchant account will be used. /// associated with the merchant account will be used.
#[remove_in(PaymentsUpdateRequest, PaymentsConfirmRequest)] #[remove_in(PaymentsUpdateRequest, PaymentsConfirmRequest)]
@ -5034,7 +5037,7 @@ pub enum PaymentLinkData<'a> {
#[derive(Debug, serde::Serialize, Clone)] #[derive(Debug, serde::Serialize, Clone)]
pub struct PaymentLinkDetails { pub struct PaymentLinkDetails {
pub amount: String, pub amount: StringMajorUnit,
pub currency: api_enums::Currency, pub currency: api_enums::Currency,
pub pub_key: String, pub pub_key: String,
pub client_secret: String, pub client_secret: String,
@ -5055,7 +5058,7 @@ pub struct PaymentLinkDetails {
#[derive(Debug, serde::Serialize)] #[derive(Debug, serde::Serialize)]
pub struct PaymentLinkStatusDetails { pub struct PaymentLinkStatusDetails {
pub amount: String, pub amount: StringMajorUnit,
pub currency: api_enums::Currency, pub currency: api_enums::Currency,
pub payment_id: String, pub payment_id: String,
pub merchant_logo: String, pub merchant_logo: String,
@ -5129,7 +5132,8 @@ pub struct PaymentLinkListResponse {
pub struct PaymentCreatePaymentLinkConfig { pub struct PaymentCreatePaymentLinkConfig {
#[serde(flatten)] #[serde(flatten)]
#[schema(value_type = Option<PaymentLinkConfigRequest>)] #[schema(value_type = Option<PaymentLinkConfigRequest>)]
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)] #[derive(Debug, Default, Eq, PartialEq, serde::Deserialize, serde::Serialize, Clone, ToSchema)]
@ -5141,7 +5145,7 @@ pub struct OrderDetailsWithStringAmount {
#[schema(example = 1)] #[schema(example = 1)]
pub quantity: u16, pub quantity: u16,
/// the amount per quantity of product /// the amount per quantity of product
pub amount: String, pub amount: StringMajorUnit,
/// Product Image link /// Product Image link
pub product_img_link: Option<String>, pub product_img_link: Option<String>,
} }

View File

@ -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<Self::Output, error_stack::Report<ParsingError>> {
amount.to_major_unit_as_string(currency)
}
fn convert_back(
&self,
amount: StringMajorUnit,
currency: enums::Currency,
) -> Result<MinorUnit, error_stack::Report<ParsingError>> {
amount.to_minor_unit_as_i64(currency)
}
}
/// Connector required amount type /// Connector required amount type
#[derive(Default, Debug, serde::Deserialize, serde::Serialize, Clone, Copy, PartialEq)] #[derive(Default, Debug, serde::Deserialize, serde::Serialize, Clone, Copy, PartialEq)]
pub struct StringMajorUnitForConnector; pub struct StringMajorUnitForConnector;

View File

@ -275,6 +275,8 @@ pub enum ApiErrorResponse {
MissingTenantId, MissingTenantId,
#[error(error_type = ErrorType::ProcessingError, code = "HE_06", message = "Invalid tenant id: {tenant_id}")] #[error(error_type = ErrorType::ProcessingError, code = "HE_06", message = "Invalid tenant id: {tenant_id}")]
InvalidTenant { tenant_id: String }, 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)] #[derive(Clone)]
@ -613,6 +615,9 @@ impl ErrorSwitch<api_models::errors::types::ApiErrorResponse> for ApiErrorRespon
Self::InvalidTenant { tenant_id } => { Self::InvalidTenant { tenant_id } => {
AER::InternalServerError(ApiError::new("HE", 6, format!("Invalid Tenant {tenant_id}"), None)) 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))
}
} }
} }
} }

View File

@ -266,6 +266,8 @@ pub enum StripeErrorCode {
ExtendedCardInfoNotFound, ExtendedCardInfoNotFound,
#[error(error_type = StripeErrorType::InvalidRequestError, code = "IR_28", message = "Invalid tenant")] #[error(error_type = StripeErrorType::InvalidRequestError, code = "IR_28", message = "Invalid tenant")]
InvalidTenant, 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 // [#216]: https://github.com/juspay/hyperswitch/issues/216
// Implement the remaining stripe error codes // Implement the remaining stripe error codes
@ -650,6 +652,9 @@ impl From<errors::ApiErrorResponse> for StripeErrorCode {
errors::ApiErrorResponse::ExtendedCardInfoNotFound => Self::ExtendedCardInfoNotFound, errors::ApiErrorResponse::ExtendedCardInfoNotFound => Self::ExtendedCardInfoNotFound,
errors::ApiErrorResponse::InvalidTenant { tenant_id: _ } errors::ApiErrorResponse::InvalidTenant { tenant_id: _ }
| errors::ApiErrorResponse::MissingTenantId => Self::InvalidTenant, | 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::MandateActive
| Self::CustomerRedacted | Self::CustomerRedacted
| Self::WebhookProcessingError | Self::WebhookProcessingError
| Self::InvalidTenant => StatusCode::INTERNAL_SERVER_ERROR, | Self::InvalidTenant
| Self::AmountConversionFailed { .. } => StatusCode::INTERNAL_SERVER_ERROR,
Self::ReturnUrlUnavailable => StatusCode::SERVICE_UNAVAILABLE, Self::ReturnUrlUnavailable => StatusCode::SERVICE_UNAVAILABLE,
Self::ExternalConnectorError { status_code, .. } => { Self::ExternalConnectorError { status_code, .. } => {
StatusCode::from_u16(*status_code).unwrap_or(StatusCode::INTERNAL_SERVER_ERROR) StatusCode::from_u16(*status_code).unwrap_or(StatusCode::INTERNAL_SERVER_ERROR)

View File

@ -5,6 +5,7 @@ use common_utils::{
DEFAULT_MERCHANT_LOGO, DEFAULT_PRODUCT_IMG, DEFAULT_SDK_LAYOUT, DEFAULT_SESSION_EXPIRY, DEFAULT_MERCHANT_LOGO, DEFAULT_PRODUCT_IMG, DEFAULT_SDK_LAYOUT, DEFAULT_SESSION_EXPIRY,
}, },
ext_traits::{OptionExt, ValueExt}, ext_traits::{OptionExt, ValueExt},
types::{AmountConvertor, MinorUnit, StringMajorUnitForCore},
}; };
use error_stack::ResultExt; use error_stack::ResultExt;
use futures::future; use futures::future;
@ -14,6 +15,7 @@ use time::PrimitiveDateTime;
use super::errors::{self, RouterResult, StorageErrorExt}; use super::errors::{self, RouterResult, StorageErrorExt};
use crate::{ use crate::{
errors::RouterResponse, errors::RouterResponse,
get_payment_link_config_value, get_payment_link_config_value_based_on_priority,
routes::SessionState, routes::SessionState,
services, services,
types::{ types::{
@ -121,9 +123,15 @@ pub async fn initiate_payment_link_flow(
payment_intent.currency, payment_intent.currency,
payment_intent.client_secret.clone(), payment_intent.client_secret.clone(),
)?; )?;
let amount = currency
.to_currency_base_unit(payment_intent.amount.get_amount_as_i64()) let required_conversion_type = StringMajorUnitForCore;
.change_context(errors::ApiErrorResponse::CurrencyConversionFailed)?;
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 order_details = validate_order_details(payment_intent.order_details.clone(), currency)?;
let session_expiry = payment_link.fulfilment_time.unwrap_or_else(|| { let session_expiry = payment_link.fulfilment_time.unwrap_or_else(|| {
@ -325,6 +333,7 @@ fn validate_order_details(
Option<Vec<api_models::payments::OrderDetailsWithStringAmount>>, Option<Vec<api_models::payments::OrderDetailsWithStringAmount>>,
error_stack::Report<errors::ApiErrorResponse>, error_stack::Report<errors::ApiErrorResponse>,
> { > {
let required_conversion_type = StringMajorUnitForCore;
let order_details = order_details let order_details = order_details
.map(|order_details| { .map(|order_details| {
order_details order_details
@ -356,10 +365,11 @@ fn validate_order_details(
.product_img_link .product_img_link
.clone_from(&order.product_img_link) .clone_from(&order.product_img_link)
}; };
order_details_amount_string.amount = order_details_amount_string.amount = required_conversion_type
currency .convert(MinorUnit::new(order.amount), currency)
.to_currency_base_unit(order.amount) .change_context(errors::ApiErrorResponse::AmountConversionFailed {
.change_context(errors::ApiErrorResponse::CurrencyConversionFailed)?; amount_type: "StringMajorUnit",
})?;
order_details_amount_string.product_name = order_details_amount_string.product_name =
capitalize_first_char(&order.product_name.clone()); capitalize_first_char(&order.product_name.clone());
order_details_amount_string.quantity = order.quantity; order_details_amount_string.quantity = order.quantity;
@ -386,9 +396,11 @@ pub fn get_payment_link_config_based_on_priority(
business_link_config: Option<serde_json::Value>, business_link_config: Option<serde_json::Value>,
merchant_name: String, merchant_name: String,
default_domain_name: String, default_domain_name: String,
payment_link_config_id: Option<String>,
) -> Result<(admin_types::PaymentLinkConfig, String), error_stack::Report<errors::ApiErrorResponse>> ) -> Result<(admin_types::PaymentLinkConfig, String), error_stack::Report<errors::ApiErrorResponse>>
{ {
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 let extracted_value: api_models::admin::BusinessPaymentLinkConfig = business_config
.parse_value("BusinessPaymentLinkConfig") .parse_value("BusinessPaymentLinkConfig")
.change_context(errors::ApiErrorResponse::InvalidDataValue { .change_context(errors::ApiErrorResponse::InvalidDataValue {
@ -402,73 +414,32 @@ pub fn get_payment_link_config_based_on_priority(
.clone() .clone()
.map(|d_name| format!("https://{}", d_name)) .map(|d_name| format!("https://{}", d_name))
.unwrap_or_else(|| default_domain_name.clone()), .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 { } else {
(default_domain_name, None) (default_domain_name, None)
}; };
let theme = payment_create_link_config let (theme, logo, seller_name, sdk_layout, display_sdk_only, enabled_saved_payment_method) = get_payment_link_config_value!(
.as_ref() payment_create_link_config,
.and_then(|pc_config| pc_config.config.theme.clone()) business_theme_configs,
.or_else(|| { (theme, DEFAULT_BACKGROUND_COLOR.to_string()),
business_config (logo, DEFAULT_MERCHANT_LOGO.to_string()),
.as_ref() (seller_name, merchant_name.clone()),
.and_then(|business_config| business_config.theme.clone()) (sdk_layout, DEFAULT_SDK_LAYOUT.to_owned()),
}) (display_sdk_only, DEFAULT_DISPLAY_SDK_ONLY),
.unwrap_or(DEFAULT_BACKGROUND_COLOR.to_string()); (
enabled_saved_payment_method,
let logo = payment_create_link_config DEFAULT_ENABLE_SAVED_PAYMENT_METHOD
.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 payment_link_config = admin_types::PaymentLinkConfig { let payment_link_config = admin_types::PaymentLinkConfig {
theme, theme,
@ -567,9 +538,13 @@ pub async fn get_payment_link_status(
field_name: "currency", field_name: "currency",
})?; })?;
let amount = currency let required_conversion_type = StringMajorUnitForCore;
.to_currency_base_unit(payment_attempt.net_amount.get_amount_as_i64())
.change_context(errors::ApiErrorResponse::CurrencyConversionFailed)?; 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 // converting first letter of merchant name to upperCase
let merchant_name = capitalize_first_char(&payment_link_config.seller_name); let merchant_name = capitalize_first_char(&payment_link_config.seller_name);

View File

@ -216,6 +216,7 @@ function boot() {
"quantity": null "quantity": null
}); });
} }
}
if (paymentDetails.merchant_name) { if (paymentDetails.merchant_name) {
document.title = "Payment requested by " + paymentDetails.merchant_name; document.title = "Payment requested by " + paymentDetails.merchant_name;
@ -228,7 +229,6 @@ function boot() {
link.type = "image/x-icon"; link.type = "image/x-icon";
document.head.appendChild(link); document.head.appendChild(link);
} }
}
// Render UI // Render UI
if (paymentDetails.display_sdk_only){ if (paymentDetails.display_sdk_only){

View File

@ -209,12 +209,12 @@ impl<F: Send + Clone> GetTracker<F, PaymentData<F>, api::PaymentsRequest> for Pa
), ),
)); ));
let payment_link_data = if let Some(payment_link_create) = request.payment_link { let payment_link_data = match request.payment_link {
if payment_link_create { Some(true) => {
let merchant_name = merchant_account let merchant_name = merchant_account
.merchant_name .merchant_name
.clone() .clone()
.map(|merchant_name| merchant_name.into_inner().peek().to_owned()) .map(|name| name.into_inner().peek().to_owned())
.unwrap_or_default(); .unwrap_or_default();
let default_domain_name = state.base_url.clone(); let default_domain_name = state.base_url.clone();
@ -225,7 +225,9 @@ impl<F: Send + Clone> GetTracker<F, PaymentData<F>, api::PaymentsRequest> for Pa
business_profile.payment_link_config.clone(), business_profile.payment_link_config.clone(),
merchant_name, merchant_name,
default_domain_name, default_domain_name,
request.payment_link_config_id.clone(),
)?; )?;
create_payment_link( create_payment_link(
request, request,
payment_link_config, payment_link_config,
@ -239,11 +241,8 @@ impl<F: Send + Clone> GetTracker<F, PaymentData<F>, api::PaymentsRequest> for Pa
session_expiry, session_expiry,
) )
.await? .await?
} else {
None
} }
} else { _ => None,
None
}; };
let payment_intent_new = Self::make_payment_intent( let payment_intent_new = Self::make_payment_intent(

View File

@ -9,3 +9,27 @@ macro_rules! get_formatted_date_time {
.change_context($crate::core::errors::ConnectorError::InvalidDateFormat) .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)),*
)
};
}