diff --git a/crates/api_models/src/admin.rs b/crates/api_models/src/admin.rs index 6162ceffcb..acffbfe812 100644 --- a/crates/api_models/src/admin.rs +++ b/crates/api_models/src/admin.rs @@ -95,6 +95,8 @@ pub struct MerchantAccountCreate { /// The id of the organization to which the merchant belongs to pub organization_id: Option, + + pub payment_link_config: Option, } #[derive(Clone, Debug, Deserialize, Serialize, ToSchema)] @@ -184,6 +186,8 @@ pub struct MerchantAccountUpdate { /// To unset this field, pass an empty string #[schema(max_length = 64)] pub default_profile: Option, + + pub payment_link_config: Option, } #[derive(Clone, Debug, ToSchema, Serialize)] @@ -277,6 +281,8 @@ pub struct MerchantAccountResponse { /// A enum value to indicate the status of recon service. By default it is not_requested. #[schema(value_type = ReconStatus, example = "not_requested")] pub recon_status: enums::ReconStatus, + + pub payment_link_config: Option, } #[derive(Clone, Debug, Deserialize, ToSchema, Serialize)] @@ -497,6 +503,22 @@ pub struct PrimaryBusinessDetails { pub business: String, } +#[derive(Clone, Debug, Deserialize, ToSchema, Serialize, PartialEq)] +#[serde(deny_unknown_fields)] +pub struct PaymentLinkConfig { + pub merchant_logo: Option, + pub color_scheme: Option, +} + +#[derive(Clone, Debug, Deserialize, ToSchema, Serialize, PartialEq)] +#[serde(deny_unknown_fields)] + +pub struct PaymentLinkColorSchema { + pub primary_color: Option, + pub primary_accent_color: Option, + pub secondary_color: Option, +} + #[derive(Clone, Debug, Deserialize, ToSchema, Serialize)] #[serde(deny_unknown_fields)] pub struct WebhookDetails { diff --git a/crates/api_models/src/payments.rs b/crates/api_models/src/payments.rs index 14bbf705ab..e763a3e267 100644 --- a/crates/api_models/src/payments.rs +++ b/crates/api_models/src/payments.rs @@ -226,6 +226,7 @@ pub struct PaymentsRequest { "product_name": "gillete creme", "quantity": 15, "amount" : 900 + "product_img_link" : "https://dummy-img-link.com" }]"#)] pub order_details: Option>, @@ -2418,6 +2419,8 @@ pub struct OrderDetailsWithAmount { pub quantity: u16, /// the amount per quantity of product pub amount: i64, + /// The image URL of the product + pub product_img_link: Option, } #[derive(Debug, Default, Eq, PartialEq, serde::Deserialize, serde::Serialize, Clone, ToSchema)] @@ -2428,6 +2431,8 @@ pub struct OrderDetails { /// The quantity of the product to be purchased #[schema(example = 1)] pub quantity: u16, + /// The image URL of the product + pub product_img_link: Option, } #[derive(Default, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize, Clone, ToSchema)] @@ -3120,3 +3125,19 @@ pub struct PaymentLinkInitiateRequest { pub merchant_id: String, pub payment_id: String, } + +#[derive(Debug, serde::Serialize)] +pub struct PaymentLinkDetails { + pub amount: i64, + pub currency: api_enums::Currency, + pub pub_key: String, + pub client_secret: String, + pub payment_id: String, + #[serde(with = "common_utils::custom_serde::iso8601")] + pub expiry: PrimitiveDateTime, + pub merchant_logo: String, + pub return_url: String, + pub merchant_name: crypto::OptionalEncryptableName, + pub order_details: Vec, + pub max_items_visible_after_collapse: i8, +} diff --git a/crates/diesel_models/src/merchant_account.rs b/crates/diesel_models/src/merchant_account.rs index dd68f3755f..65bba47977 100644 --- a/crates/diesel_models/src/merchant_account.rs +++ b/crates/diesel_models/src/merchant_account.rs @@ -40,6 +40,7 @@ pub struct MerchantAccount { pub is_recon_enabled: bool, pub default_profile: Option, pub recon_status: storage_enums::ReconStatus, + pub payment_link_config: Option, } #[derive(Clone, Debug, Insertable, router_derive::DebugAsDisplay)] @@ -69,6 +70,7 @@ pub struct MerchantAccountNew { pub is_recon_enabled: bool, pub default_profile: Option, pub recon_status: storage_enums::ReconStatus, + pub payment_link_config: Option, } #[derive(Clone, Debug, Default, AsChangeset, router_derive::DebugAsDisplay)] @@ -97,4 +99,5 @@ pub struct MerchantAccountUpdateInternal { pub is_recon_enabled: bool, pub default_profile: Option>, pub recon_status: storage_enums::ReconStatus, + pub payment_link_config: Option, } diff --git a/crates/diesel_models/src/schema.rs b/crates/diesel_models/src/schema.rs index 93a204446a..578d22c6ad 100644 --- a/crates/diesel_models/src/schema.rs +++ b/crates/diesel_models/src/schema.rs @@ -442,6 +442,7 @@ diesel::table! { #[max_length = 64] default_profile -> Nullable, recon_status -> ReconStatus, + payment_link_config -> Nullable, } } diff --git a/crates/router/src/core/admin.rs b/crates/router/src/core/admin.rs index 2fecb57323..d80175ee9e 100644 --- a/crates/router/src/core/admin.rs +++ b/crates/router/src/core/admin.rs @@ -136,6 +136,17 @@ pub async fn create_merchant_account( .transpose()? .map(Secret::new); + let payment_link_config = req + .payment_link_config + .as_ref() + .map(|pl_metadata| { + utils::Encode::::encode_to_value(pl_metadata) + .change_context(errors::ApiErrorResponse::InvalidDataValue { + field_name: "payment_link_config", + }) + }) + .transpose()?; + let mut merchant_account = async { Ok(domain::MerchantAccount { merchant_id: req.merchant_id, @@ -171,6 +182,7 @@ pub async fn create_merchant_account( is_recon_enabled: false, default_profile: None, recon_status: diesel_models::enums::ReconStatus::NotRequested, + payment_link_config, }) } .await @@ -458,6 +470,7 @@ pub async fn merchant_account_update( intent_fulfillment_time: req.intent_fulfillment_time.map(i64::from), payout_routing_algorithm: req.payout_routing_algorithm, default_profile: business_profile_id_update, + payment_link_config: req.payment_link_config, }; let response = db diff --git a/crates/router/src/core/payment_link.rs b/crates/router/src/core/payment_link.rs index 2f817beb53..2c51fa0c3c 100644 --- a/crates/router/src/core/payment_link.rs +++ b/crates/router/src/core/payment_link.rs @@ -1,7 +1,8 @@ +use api_models::admin as admin_types; use common_utils::ext_traits::AsyncExt; -use error_stack::ResultExt; +use error_stack::{IntoReport, ResultExt}; -use super::errors::{self, StorageErrorExt}; +use super::errors::{self, RouterResult, StorageErrorExt}; use crate::{ core::payments::helpers, errors::RouterResponse, @@ -41,6 +42,19 @@ pub async fn intiate_payment_link_flow( ) .await .to_not_found_response(errors::ApiErrorResponse::PaymentNotFound)?; + + helpers::validate_payment_status_against_not_allowed_statuses( + &payment_intent.status, + &[ + storage_enums::IntentStatus::Cancelled, + storage_enums::IntentStatus::Succeeded, + storage_enums::IntentStatus::Processing, + storage_enums::IntentStatus::RequiresCapture, + storage_enums::IntentStatus::RequiresMerchantAction, + ], + "create payment link", + )?; + let fulfillment_time = payment_intent .payment_link_id .as_ref() @@ -56,32 +70,63 @@ pub async fn intiate_payment_link_flow( .get_required_value("fulfillment_time") .change_context(errors::ApiErrorResponse::PaymentNotFound)?; - helpers::validate_payment_status_against_not_allowed_statuses( - &payment_intent.status, - &[ - storage_enums::IntentStatus::Cancelled, - storage_enums::IntentStatus::Succeeded, - storage_enums::IntentStatus::Processing, - storage_enums::IntentStatus::RequiresCapture, - storage_enums::IntentStatus::RequiresMerchantAction, - ], - "create payment link", + let payment_link_config = merchant_account + .payment_link_config + .map(|pl_config| { + serde_json::from_value::(pl_config) + .into_report() + .change_context(errors::ApiErrorResponse::InvalidDataValue { + field_name: "payment_link_config", + }) + }) + .transpose()?; + + let order_details = payment_intent + .order_details + .get_required_value("order_details") + .change_context(errors::ApiErrorResponse::MissingRequiredField { + field_name: "order_details", + })?; + + let return_url = if let Some(payment_create_return_url) = payment_intent.return_url { + payment_create_return_url + } else { + merchant_account + .return_url + .ok_or(errors::ApiErrorResponse::MissingRequiredField { + field_name: "return_url", + })? + }; + + let (pub_key, currency, client_secret) = validate_sdk_requirements( + merchant_account.publishable_key, + payment_intent.currency, + payment_intent.client_secret, )?; - let expiry = fulfillment_time.assume_utc().unix_timestamp(); - - let js_script = get_js_script( - payment_intent.amount.to_string(), - payment_intent.currency.unwrap_or_default().to_string(), - merchant_account.publishable_key.unwrap_or_default(), - payment_intent.client_secret.unwrap_or_default(), - payment_intent.payment_id, - expiry, - ); + let payment_details = api_models::payments::PaymentLinkDetails { + amount: payment_intent.amount, + currency, + payment_id: payment_intent.payment_id, + merchant_name: merchant_account.merchant_name, + order_details, + return_url, + expiry: fulfillment_time, + pub_key, + client_secret, + merchant_logo: payment_link_config + .clone() + .map(|pl_metadata| pl_metadata.merchant_logo.unwrap_or_default()) + .unwrap_or_default(), + max_items_visible_after_collapse: 3, + }; + let js_script = get_js_script(payment_details)?; + let css_script = get_color_scheme_css(payment_link_config.clone()); let payment_link_data = services::PaymentLinkFormData { js_script, sdk_url: state.conf.payment_link.sdk_url.clone(), + css_script, }; Ok(services::ApplicationResponse::PaymenkLinkForm(Box::new( payment_link_data, @@ -93,80 +138,68 @@ The get_js_script function is used to inject dynamic value to payment_link sdk, */ fn get_js_script( - amount: String, - currency: String, - pub_key: String, - secret: String, - payment_id: String, - expiry: i64, -) -> String { - format!( - "window.__PAYMENT_DETAILS_STR = JSON.stringify({{ - client_secret: '{secret}', - amount: '{amount}', - currency: '{currency}', - payment_id: '{payment_id}', - expiry: {expiry}, - // TODO: Remove hardcoded values - merchant_logo: 'https://upload.wikimedia.org/wikipedia/commons/8/83/Steam_icon_logo.svg', - return_url: 'http://localhost:5500/public/index.html', - currency_symbol: '$', - merchant: 'Steam', - max_items_visible_after_collapse: 3, - order_details: [ - {{ - product_name: - 'dskjghbdsiuh sagfvbsajd ugbfiusedg fiudshgiu sdhgvishd givuhdsifu gnb gidsug biuesbdg iubsedg bsduxbg jhdxbgv jdskfbgi sdfgibuh ew87t54378 ghdfjbv jfdhgvb dufhvbfidu hg5784ghdfbjnk f (taxes incl.)', - quantity: 2, - amount: 100, - product_image: - 'https://upload.wikimedia.org/wikipedia/commons/8/83/Steam_icon_logo.svg', - }}, - {{ - product_name: \"F1 '23\", - quantity: 4, - amount: 500, - product_image: - 'https://upload.wikimedia.org/wikipedia/commons/8/83/Steam_icon_logo.svg', - }}, - {{ - product_name: \"Motosport '24\", - quantity: 4, - amount: 500, - product_image: - 'https://upload.wikimedia.org/wikipedia/commons/8/83/Steam_icon_logo.svg', - }}, - {{ - product_name: 'Trackmania', - quantity: 4, - amount: 500, - product_image: - 'https://upload.wikimedia.org/wikipedia/commons/8/83/Steam_icon_logo.svg', - }}, - {{ - product_name: 'Ghost Recon', - quantity: 4, - amount: 500, - product_image: - 'https://upload.wikimedia.org/wikipedia/commons/8/83/Steam_icon_logo.svg', - }}, - {{ - product_name: 'Cup of Tea', - quantity: 4, - amount: 500, - product_image: - 'https://upload.wikimedia.org/wikipedia/commons/8/83/Steam_icon_logo.svg', - }}, - {{ - product_name: 'Tea cups', - quantity: 4, - amount: 500, - product_image: - 'https://upload.wikimedia.org/wikipedia/commons/8/83/Steam_icon_logo.svg', - }}, - ] - }}); + payment_details: api_models::payments::PaymentLinkDetails, +) -> RouterResult { + let payment_details_str = serde_json::to_string(&payment_details) + .into_report() + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed to serialize PaymentLinkDetails")?; + Ok(format!("window.__PAYMENT_DETAILS = {payment_details_str};")) +} - const hyper = Hyper(\"{pub_key}\");" +fn get_color_scheme_css( + payment_link_config: Option, +) -> String { + let (default_primary_color, default_accent_color, default_secondary_color) = ( + "#C6C7C8".to_string(), + "#6A8EF5".to_string(), + "#0C48F6".to_string(), + ); + + let (primary_color, primary_accent_color, secondary_color) = payment_link_config + .and_then(|pl_config| { + pl_config.color_scheme.map(|color| { + ( + color.primary_color.unwrap_or(default_primary_color.clone()), + color + .primary_accent_color + .unwrap_or(default_accent_color.clone()), + color + .secondary_color + .unwrap_or(default_secondary_color.clone()), + ) + }) + }) + .unwrap_or(( + default_primary_color, + default_accent_color, + default_secondary_color, + )); + + format!( + ":root {{ + --primary-color: {primary_color}; + --primary-accent-color: {primary_accent_color}; + --secondary-color: {secondary_color}; + }}" ) } + +fn validate_sdk_requirements( + pub_key: Option, + currency: Option, + client_secret: Option, +) -> Result<(String, api_models::enums::Currency, String), errors::ApiErrorResponse> { + let pub_key = pub_key.ok_or(errors::ApiErrorResponse::MissingRequiredField { + field_name: "pub_key", + })?; + + let currency = currency.ok_or(errors::ApiErrorResponse::MissingRequiredField { + field_name: "currency", + })?; + + let client_secret = client_secret.ok_or(errors::ApiErrorResponse::MissingRequiredField { + field_name: "client_secret", + })?; + Ok((pub_key, currency, client_secret)) +} diff --git a/crates/router/src/core/payment_link/payment_link.html b/crates/router/src/core/payment_link/payment_link.html index 4ce2ff1919..462a11d256 100644 --- a/crates/router/src/core/payment_link/payment_link.html +++ b/crates/router/src/core/payment_link/payment_link.html @@ -3,15 +3,12 @@ {{ hyperloader_sdk_link }}