From 03f0ea1582c76f1ed9dc4ff2215e80d659e5591a Mon Sep 17 00:00:00 2001 From: Sakil Mostak <73734619+Sakilmostak@users.noreply.github.com> Date: Thu, 8 Aug 2024 19:50:07 +0530 Subject: [PATCH] feat(core): [Payment Link] add dynamic merchant fields (#5512) Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com> --- Cargo.lock | 29 +++---- api-reference/openapi_spec.json | 10 +++ crates/api_models/Cargo.toml | 1 + crates/api_models/src/admin.rs | 6 ++ crates/api_models/src/payments.rs | 6 +- crates/common_utils/src/consts.rs | 3 + crates/router/src/core/payment_link.rs | 76 +++++++++++++------ .../payment_link_initiate/payment_link.css | 30 +++++++- .../payment_link_initiate/payment_link.html | 2 + .../payment_link_initiate/payment_link.js | 37 +++++++++ crates/router/src/types/transformers.rs | 1 + 11 files changed, 160 insertions(+), 41 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 3f15060e10..9d37e75077 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -448,6 +448,7 @@ dependencies = [ "common_utils", "error-stack", "euclid", + "indexmap 2.3.0", "masking", "mime", "reqwest", @@ -2074,7 +2075,7 @@ version = "0.1.0" dependencies = [ "anyhow", "clap", - "indexmap 2.2.6", + "indexmap 2.3.0", "serde", "serde_json", "toml 0.8.12", @@ -3466,7 +3467,7 @@ dependencies = [ "futures-sink", "futures-util", "http 0.2.12", - "indexmap 2.2.6", + "indexmap 2.3.0", "slab", "tokio 1.37.0", "tokio-util", @@ -3485,7 +3486,7 @@ dependencies = [ "futures-sink", "futures-util", "http 1.1.0", - "indexmap 2.2.6", + "indexmap 2.3.0", "slab", "tokio 1.37.0", "tokio-util", @@ -4008,9 +4009,9 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.2.6" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "168fb715dda47215e360912c096649d23d58bf392ac62f73919e831745e40f26" +checksum = "de3fc2e30ba82dd1b3911c8de1ffc143c74a914a14e99514d7637e3099df5ea0" dependencies = [ "equivalent", "hashbrown 0.14.3", @@ -6165,7 +6166,7 @@ dependencies = [ "common_utils", "diesel", "error-stack", - "indexmap 2.2.6", + "indexmap 2.3.0", "proc-macro2", "quote", "serde", @@ -6662,7 +6663,7 @@ version = "1.0.115" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "12dc5c46daa8e9fdf4f5e71b6cf9a53f2487da0e86e55808e2d35539666497dd" dependencies = [ - "indexmap 2.2.6", + "indexmap 2.3.0", "itoa", "ryu", "serde", @@ -6740,7 +6741,7 @@ dependencies = [ "chrono", "hex", "indexmap 1.9.3", - "indexmap 2.2.6", + "indexmap 2.3.0", "serde", "serde_derive", "serde_json", @@ -7018,7 +7019,7 @@ dependencies = [ "futures-util", "hashlink", "hex", - "indexmap 2.2.6", + "indexmap 2.3.0", "log", "memchr", "native-tls", @@ -7954,7 +7955,7 @@ version = "0.8.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e9dd1545e8208b4a5af1aa9bbd0b4cf7e9ea08fabc5d0a5c67fcaafa17433aa3" dependencies = [ - "indexmap 2.2.6", + "indexmap 2.3.0", "serde", "serde_spanned", "toml_datetime", @@ -7976,7 +7977,7 @@ version = "0.19.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" dependencies = [ - "indexmap 2.2.6", + "indexmap 2.3.0", "serde", "serde_spanned", "toml_datetime", @@ -7989,7 +7990,7 @@ version = "0.21.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6a8534fd7f78b5405e860340ad6575217ce99f38d4d5c8f2442cb5ecb50090e1" dependencies = [ - "indexmap 2.2.6", + "indexmap 2.3.0", "toml_datetime", "winnow 0.5.40", ] @@ -8000,7 +8001,7 @@ version = "0.22.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e40bb779c5187258fd7aad0eb68cb8706a0a81fa712fbea808ab43c4b8374c4" dependencies = [ - "indexmap 2.2.6", + "indexmap 2.3.0", "serde", "serde_spanned", "toml_datetime", @@ -8407,7 +8408,7 @@ version = "4.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "272ebdfbc99111033031d2f10e018836056e4d2c8e2acda76450ec7974269fa7" dependencies = [ - "indexmap 2.2.6", + "indexmap 2.3.0", "serde", "serde_json", "utoipa-gen", diff --git a/api-reference/openapi_spec.json b/api-reference/openapi_spec.json index 456fcea7fd..81d142e999 100644 --- a/api-reference/openapi_spec.json +++ b/api-reference/openapi_spec.json @@ -13977,6 +13977,11 @@ "description": "A list of allowed domains (glob patterns) where this link can be embedded / opened from", "uniqueItems": true, "nullable": true + }, + "transaction_details": { + "type": "string", + "description": "Dynamic details related to merchant to be rendered in payment link", + "nullable": true } } }, @@ -14024,6 +14029,11 @@ "default": false, "example": true, "nullable": true + }, + "transaction_details": { + "type": "object", + "description": "Dynamic details related to merchant to be rendered in payment link", + "nullable": true } } }, diff --git a/crates/api_models/Cargo.toml b/crates/api_models/Cargo.toml index e2b9e2d3a5..10c4ee2692 100644 --- a/crates/api_models/Cargo.toml +++ b/crates/api_models/Cargo.toml @@ -30,6 +30,7 @@ routing_v2 = [] [dependencies] actix-web = { version = "4.5.1", optional = true } error-stack = "0.4.1" +indexmap = "2.3.0" mime = "0.3.17" reqwest = { version = "0.11.27", optional = true } serde = { version = "1.0.197", features = ["derive"] } diff --git a/crates/api_models/src/admin.rs b/crates/api_models/src/admin.rs index 9371e8ed07..571dc49320 100644 --- a/crates/api_models/src/admin.rs +++ b/crates/api_models/src/admin.rs @@ -12,6 +12,7 @@ use common_utils::{ not(feature = "merchant_account_v2") ))] use common_utils::{crypto::OptionalEncryptableName, ext_traits::ValueExt}; +use indexmap::IndexMap; #[cfg(all(feature = "v2", feature = "merchant_account_v2"))] use masking::ExposeInterface; use masking::Secret; @@ -2235,6 +2236,9 @@ pub struct PaymentLinkConfigRequest { /// Enable saved payment method option for payment link #[schema(default = false, example = true)] pub enabled_saved_payment_method: Option, + /// Dynamic details related to merchant to be rendered in payment link + #[schema(value_type = Option, example = r#"{ "value1": "some-value", "value2": "some-value" }"#)] + pub transaction_details: Option>, } #[derive(Clone, Debug, serde::Serialize, serde::Deserialize, PartialEq, ToSchema)] @@ -2253,6 +2257,8 @@ pub struct PaymentLinkConfig { pub enabled_saved_payment_method: bool, /// A list of allowed domains (glob patterns) where this link can be embedded / opened from pub allowed_domains: Option>, + /// Dynamic details related to merchant to be rendered in payment link + pub transaction_details: Option, } #[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq, Eq)] diff --git a/crates/api_models/src/payments.rs b/crates/api_models/src/payments.rs index 7b9167a19d..88c915f0e7 100644 --- a/crates/api_models/src/payments.rs +++ b/crates/api_models/src/payments.rs @@ -5385,8 +5385,8 @@ pub struct PaymentLinkInitiateRequest { #[derive(Debug, serde::Serialize)] #[serde(untagged)] pub enum PaymentLinkData { - PaymentLinkDetails(PaymentLinkDetails), - PaymentLinkStatusDetails(PaymentLinkStatusDetails), + PaymentLinkDetails(Box), + PaymentLinkStatusDetails(Box), } #[derive(Debug, serde::Serialize, Clone)] @@ -5408,6 +5408,7 @@ pub struct PaymentLinkDetails { pub sdk_layout: String, pub display_sdk_only: bool, pub locale: Option, + pub transaction_details: Option, } #[derive(Debug, serde::Serialize, Clone)] @@ -5433,6 +5434,7 @@ pub struct PaymentLinkStatusDetails { pub theme: String, pub return_url: String, pub locale: Option, + pub transaction_details: Option, } #[derive(Clone, Debug, serde::Deserialize, ToSchema, serde::Serialize)] diff --git a/crates/common_utils/src/consts.rs b/crates/common_utils/src/consts.rs index 75ba14586b..a331a05f6f 100644 --- a/crates/common_utils/src/consts.rs +++ b/crates/common_utils/src/consts.rs @@ -86,6 +86,9 @@ pub const DEFAULT_ENABLE_SAVED_PAYMENT_METHOD: bool = false; /// Default allowed domains for payment links pub const DEFAULT_ALLOWED_DOMAINS: Option> = None; +/// Default merchant details for payment links +pub const DEFAULT_TRANSACTION_DETAILS: Option = None; + /// Default ttl for Extended card info in redis (in seconds) pub const DEFAULT_TTL_FOR_EXTENDED_CARD_INFO: u16 = 15 * 60; diff --git a/crates/router/src/core/payment_link.rs b/crates/router/src/core/payment_link.rs index 299ae97d3a..643b69511c 100644 --- a/crates/router/src/core/payment_link.rs +++ b/crates/router/src/core/payment_link.rs @@ -8,7 +8,7 @@ use common_utils::{ consts::{ DEFAULT_ALLOWED_DOMAINS, DEFAULT_BACKGROUND_COLOR, DEFAULT_DISPLAY_SDK_ONLY, DEFAULT_ENABLE_SAVED_PAYMENT_METHOD, DEFAULT_MERCHANT_LOGO, DEFAULT_PRODUCT_IMG, - DEFAULT_SDK_LAYOUT, DEFAULT_SESSION_EXPIRY, + DEFAULT_SDK_LAYOUT, DEFAULT_SESSION_EXPIRY, DEFAULT_TRANSACTION_DETAILS, }, ext_traits::{OptionExt, ValueExt}, types::{AmountConvertor, MinorUnit, StringMajorUnitForCore}, @@ -109,6 +109,7 @@ pub async fn form_payment_link_data( display_sdk_only: DEFAULT_DISPLAY_SDK_ONLY, enabled_saved_payment_method: DEFAULT_ENABLE_SAVED_PAYMENT_METHOD, allowed_domains: DEFAULT_ALLOWED_DOMAINS, + transaction_details: DEFAULT_TRANSACTION_DETAILS, } }; @@ -219,36 +220,41 @@ pub async fn form_payment_link_data( theme: payment_link_config.theme.clone(), return_url: return_url.clone(), locale: locale.clone(), + transaction_details: payment_link_config.transaction_details.clone(), }; return Ok(( payment_link, - PaymentLinkData::PaymentLinkStatusDetails(payment_details), + PaymentLinkData::PaymentLinkStatusDetails(Box::new(payment_details)), payment_link_config, )); }; - let payment_link_details = - PaymentLinkData::PaymentLinkDetails(api_models::payments::PaymentLinkDetails { - amount, - currency, - payment_id: payment_intent.payment_id, - merchant_name, - order_details, - return_url, - session_expiry, - pub_key: merchant_account.publishable_key, - client_secret, - merchant_logo: payment_link_config.logo.clone(), - max_items_visible_after_collapse: 3, - theme: payment_link_config.theme.clone(), - merchant_description: payment_intent.description, - sdk_layout: payment_link_config.sdk_layout.clone(), - display_sdk_only: payment_link_config.display_sdk_only, - locale, - }); + let payment_link_details = api_models::payments::PaymentLinkDetails { + amount, + currency, + payment_id: payment_intent.payment_id, + merchant_name, + order_details, + return_url, + session_expiry, + pub_key: merchant_account.publishable_key, + client_secret, + merchant_logo: payment_link_config.logo.clone(), + max_items_visible_after_collapse: 3, + theme: payment_link_config.theme.clone(), + merchant_description: payment_intent.description, + sdk_layout: payment_link_config.sdk_layout.clone(), + display_sdk_only: payment_link_config.display_sdk_only, + locale, + transaction_details: payment_link_config.transaction_details.clone(), + }; - Ok((payment_link, payment_link_details, payment_link_config)) + Ok(( + payment_link, + PaymentLinkData::PaymentLinkDetails(Box::new(payment_link_details)), + payment_link_config, + )) } pub async fn initiate_secure_payment_link_flow( @@ -297,7 +303,7 @@ pub async fn initiate_secure_payment_link_flow( PaymentLinkData::PaymentLinkDetails(link_details) => { let secure_payment_link_details = api_models::payments::SecurePaymentLinkDetails { enabled_saved_payment_method: payment_link_config.enabled_saved_payment_method, - payment_link_details: link_details.to_owned(), + payment_link_details: *link_details.to_owned(), }; let js_script = format!( "window.__PAYMENT_DETAILS = {}", @@ -602,6 +608,24 @@ pub fn get_payment_link_config_based_on_priority( display_sdk_only, enabled_saved_payment_method, allowed_domains, + transaction_details: payment_create_link_config.and_then(|payment_link_config| { + payment_link_config + .theme_config + .transaction_details + .and_then(|transaction_details| { + match serde_json::to_string(&transaction_details).change_context( + errors::ApiErrorResponse::InvalidDataValue { + field_name: "transaction_details", + }, + ) { + Ok(details) => Some(details), + Err(err) => { + logger::error!("Failed to serialize transaction details: {:?}", err); + None + } + } + }) + }), }; Ok((payment_link_config, domain_name)) @@ -689,6 +713,7 @@ pub async fn get_payment_link_status( display_sdk_only: DEFAULT_DISPLAY_SDK_ONLY, enabled_saved_payment_method: DEFAULT_ENABLE_SAVED_PAYMENT_METHOD, allowed_domains: DEFAULT_ALLOWED_DOMAINS, + transaction_details: DEFAULT_TRANSACTION_DETAILS, } }; @@ -748,8 +773,11 @@ pub async fn get_payment_link_status( theme: payment_link_config.theme.clone(), return_url, locale, + transaction_details: payment_link_config.transaction_details, }; - let js_script = get_js_script(&PaymentLinkData::PaymentLinkStatusDetails(payment_details))?; + let js_script = get_js_script(&PaymentLinkData::PaymentLinkStatusDetails(Box::new( + payment_details, + )))?; let payment_link_status_data = services::PaymentLinkStatusData { js_script, css_script, diff --git a/crates/router/src/core/payment_link/payment_link_initiate/payment_link.css b/crates/router/src/core/payment_link/payment_link_initiate/payment_link.css index 617a28377a..d560060d88 100644 --- a/crates/router/src/core/payment_link/payment_link_initiate/payment_link.css +++ b/crates/router/src/core/payment_link/payment_link_initiate/payment_link.css @@ -90,10 +90,21 @@ body { .content-details-wrap { display: flex; flex-flow: row; - margin: 20px 20px 30px 20px; + margin: 20px 20px 10px 20px; justify-content: space-between; } +#hyper-checkout-payment-merchant-dynamic-details { + margin: 20px 20px 10px 20px; +} + +.hyper-checkout-payment-horizontal-line { + margin: 0px 20px; + height: 2px; + background-color: #e5e5e5; + border: none; +} + .hyper-checkout-payment-price { font-weight: 700; font-size: 40px; @@ -111,6 +122,11 @@ body { font-size: 19px; } +.hyper-checkout-payment-merchant-dynamic-data { + font-size: 12px; + margin-top: 5px; +} + .hyper-checkout-payment-ref { font-size: 12px; margin-top: 5px; @@ -694,6 +710,18 @@ body { margin: 0; } + #hyper-checkout-payment-merchant-dynamic-details { + flex-flow: column; + flex-direction: column-reverse; + margin: 0; + } + + .hyper-checkout-payment-horizontal-line { + margin: 10px 0px; + height: 2px; + border: none; + } + #hyper-checkout-merchant-image { background-color: white; } diff --git a/crates/router/src/core/payment_link/payment_link_initiate/payment_link.html b/crates/router/src/core/payment_link/payment_link_initiate/payment_link.html index 3513de1bb6..b66242305b 100644 --- a/crates/router/src/core/payment_link/payment_link_initiate/payment_link.html +++ b/crates/router/src/core/payment_link/payment_link_initiate/payment_link.html @@ -225,6 +225,8 @@ +
+
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 18b90a9117..0d1b8c64d3 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 @@ -238,6 +238,7 @@ function boot() { } else{ renderPaymentDetails(paymentDetails); + renderDynamicMerchantDetails(paymentDetails); renderCart(paymentDetails); renderSDKHeader(paymentDetails); } @@ -605,6 +606,42 @@ function renderPaymentDetails(paymentDetails) { } } +function renderDynamicMerchantDetails(paymentDetails) { + var merchantDynamicDetails = document.getElementById( + "hyper-checkout-payment-merchant-dynamic-details" + ); + if (merchantDynamicDetails instanceof HTMLDivElement) { + // add dynamic merchant details in the payment details section if present + appendMerchantDetails(paymentDetails, merchantDynamicDetails); + } +} + +function appendMerchantDetails(paymentDetails, merchantDynamicDetails) { + if (Object.keys(paymentDetails.transaction_details).length === 0) { + return; + } + + // render a horizontal line above dynamic merchant details + let horizontalLineContainer = document.getElementById("hyper-checkout-payment-horizontal-line-container"); + let horizontalLine = document.createElement("hr"); + horizontalLine.className = "hyper-checkout-payment-horizontal-line"; + horizontalLineContainer.append(horizontalLine); + + // max number of items to show in the merchant details + let maxItemsInDetails = 5; + let merchantDetailsObject = JSON.parse(paymentDetails.transaction_details); + for(const key in merchantDetailsObject) { + var merchantData = document.createElement("div"); + merchantData.className = "hyper-checkout-payment-merchant-dynamic-data"; + merchantData.innerHTML = key+": "+merchantDetailsObject[key].bold(); + + merchantDynamicDetails.append(merchantData); + if(--maxItemsInDetails === 0) { + break; + } + } +} + /** * Trigger - on boot * Uses diff --git a/crates/router/src/types/transformers.rs b/crates/router/src/types/transformers.rs index 52d6e7330c..fe65e84885 100644 --- a/crates/router/src/types/transformers.rs +++ b/crates/router/src/types/transformers.rs @@ -1740,6 +1740,7 @@ impl ForeignFrom sdk_layout: item.sdk_layout, display_sdk_only: item.display_sdk_only, enabled_saved_payment_method: item.enabled_saved_payment_method, + transaction_details: None, } } }