diff --git a/api-reference-v2/openapi_spec.json b/api-reference-v2/openapi_spec.json index acff517e10..1e901161b9 100644 --- a/api-reference-v2/openapi_spec.json +++ b/api-reference-v2/openapi_spec.json @@ -17368,7 +17368,8 @@ "single", "priority", "volume_split", - "advanced" + "advanced", + "dynamic" ] }, "RoutingConfigRequest": { diff --git a/api-reference/openapi_spec.json b/api-reference/openapi_spec.json index e6cb4ede37..cc212de0c3 100644 --- a/api-reference/openapi_spec.json +++ b/api-reference/openapi_spec.json @@ -3884,6 +3884,161 @@ ] } }, + "/account/:account_id/business_profile/:profile_id/dynamic_routing/success_based/toggle": { + "post": { + "tags": [ + "Routing" + ], + "summary": "Routing - Toggle success based dynamic routing for profile", + "description": "Create a success based dynamic routing algorithm", + "operationId": "Toggle success based dynamic routing algprithm", + "parameters": [ + { + "name": "account_id", + "in": "path", + "description": "Merchant id", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "profile_id", + "in": "path", + "description": "Profile id under which Dynamic routing needs to be toggled", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "status", + "in": "query", + "description": "Boolean value for mentioning the expected state of dynamic routing", + "required": true, + "schema": { + "type": "boolean" + } + } + ], + "responses": { + "200": { + "description": "Routing Algorithm created", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RoutingDictionaryRecord" + } + } + } + }, + "400": { + "description": "Request body is malformed" + }, + "403": { + "description": "Forbidden" + }, + "404": { + "description": "Resource missing" + }, + "422": { + "description": "Unprocessable request" + }, + "500": { + "description": "Internal server error" + } + }, + "security": [ + { + "api_key": [] + }, + { + "jwt_key": [] + } + ] + } + }, + "/account/:account_id/business_profile/:profile_id/dynamic_routing/success_based/config/:algorithm_id": { + "patch": { + "tags": [ + "Routing" + ], + "summary": "Routing - Update config for success based dynamic routing", + "description": "Update config for success based dynamic routing", + "operationId": "Update configs for success based dynamic routing algorithm", + "parameters": [ + { + "name": "account_id", + "in": "path", + "description": "Merchant id", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "profile_id", + "in": "path", + "description": "The unique identifier for a profile", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "algorithm_id", + "in": "path", + "description": "The unique identifier for routing algorithm", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SuccessBasedRoutingConfig" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Routing Algorithm updated", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RoutingDictionaryRecord" + } + } + } + }, + "400": { + "description": "Request body is malformed" + }, + "403": { + "description": "Forbidden" + }, + "404": { + "description": "Resource missing" + }, + "422": { + "description": "Unprocessable request" + }, + "500": { + "description": "Internal server error" + } + }, + "security": [ + { + "admin_api_key": [] + } + ] + } + }, "/blocklist": { "delete": { "tags": [ @@ -9314,6 +9469,23 @@ "ZMW" ] }, + "CurrentBlockThreshold": { + "type": "object", + "properties": { + "duration_in_mins": { + "type": "integer", + "format": "int64", + "nullable": true, + "minimum": 0 + }, + "max_total_count": { + "type": "integer", + "format": "int64", + "nullable": true, + "minimum": 0 + } + } + }, "CustomerAcceptance": { "type": "object", "description": "This \"CustomerAcceptance\" object is passed during Payments-Confirm request, it enlists the type, time, and mode of acceptance properties related to an acceptance done by the customer. The customer_acceptance sub object is usually passed by the SDK or client.", @@ -21795,7 +21967,8 @@ "single", "priority", "volume_split", - "advanced" + "advanced", + "dynamic" ] }, "RoutingConfigRequest": { @@ -22387,6 +22560,80 @@ "destination" ] }, + "SuccessBasedRoutingConfig": { + "type": "object", + "properties": { + "params": { + "type": "array", + "items": { + "$ref": "#/components/schemas/SuccessBasedRoutingConfigParams" + }, + "nullable": true + }, + "config": { + "allOf": [ + { + "$ref": "#/components/schemas/SuccessBasedRoutingConfigBody" + } + ], + "nullable": true + } + } + }, + "SuccessBasedRoutingConfigBody": { + "type": "object", + "properties": { + "min_aggregates_size": { + "type": "integer", + "format": "int32", + "nullable": true, + "minimum": 0 + }, + "default_success_rate": { + "type": "number", + "format": "double", + "nullable": true + }, + "max_aggregates_size": { + "type": "integer", + "format": "int32", + "nullable": true, + "minimum": 0 + }, + "current_block_threshold": { + "allOf": [ + { + "$ref": "#/components/schemas/CurrentBlockThreshold" + } + ], + "nullable": true + } + } + }, + "SuccessBasedRoutingConfigParams": { + "type": "string", + "enum": [ + "PaymentMethod", + "PaymentMethodType", + "Currency", + "AuthenticationType" + ] + }, + "SuccessBasedRoutingUpdateConfigQuery": { + "type": "object", + "required": [ + "algorithm_id", + "profile_id" + ], + "properties": { + "algorithm_id": { + "type": "string" + }, + "profile_id": { + "type": "string" + } + } + }, "SurchargeDetailsResponse": { "type": "object", "required": [ @@ -22641,6 +22888,28 @@ } } }, + "ToggleSuccessBasedRoutingPath": { + "type": "object", + "required": [ + "profile_id" + ], + "properties": { + "profile_id": { + "type": "string" + } + } + }, + "ToggleSuccessBasedRoutingQuery": { + "type": "object", + "required": [ + "status" + ], + "properties": { + "status": { + "type": "boolean" + } + } + }, "TouchNGoRedirection": { "type": "object" }, diff --git a/crates/api_models/src/admin.rs b/crates/api_models/src/admin.rs index b27a279b96..c6f541dba0 100644 --- a/crates/api_models/src/admin.rs +++ b/crates/api_models/src/admin.rs @@ -2355,6 +2355,10 @@ pub struct BusinessProfileUpdate { /// Indicates if tax_calculator connector is enabled or not. /// If set to `true` tax_connector_id will be checked. pub is_tax_connector_enabled: Option, + + /// Indicates if dynamic routing is enabled or not. + #[serde(default)] + pub dynamic_routing_algorithm: Option, } #[cfg(feature = "v2")] diff --git a/crates/api_models/src/events/routing.rs b/crates/api_models/src/events/routing.rs index 9cfcafc873..ffa5e008f0 100644 --- a/crates/api_models/src/events/routing.rs +++ b/crates/api_models/src/events/routing.rs @@ -4,7 +4,9 @@ use crate::routing::{ LinkedRoutingConfigRetrieveResponse, MerchantRoutingAlgorithm, ProfileDefaultRoutingConfig, RoutingAlgorithmId, RoutingConfigRequest, RoutingDictionaryRecord, RoutingKind, RoutingLinkWrapper, RoutingPayloadWrapper, RoutingRetrieveLinkQuery, - RoutingRetrieveLinkQueryWrapper, RoutingRetrieveQuery, + RoutingRetrieveLinkQueryWrapper, RoutingRetrieveQuery, SuccessBasedRoutingConfig, + SuccessBasedRoutingPayloadWrapper, SuccessBasedRoutingUpdateConfigQuery, + ToggleSuccessBasedRoutingQuery, ToggleSuccessBasedRoutingWrapper, }; impl ApiEventMetric for RoutingKind { @@ -76,3 +78,33 @@ impl ApiEventMetric for RoutingRetrieveLinkQueryWrapper { Some(ApiEventsType::Routing) } } + +impl ApiEventMetric for ToggleSuccessBasedRoutingQuery { + fn get_api_event_type(&self) -> Option { + Some(ApiEventsType::Routing) + } +} + +impl ApiEventMetric for SuccessBasedRoutingConfig { + fn get_api_event_type(&self) -> Option { + Some(ApiEventsType::Routing) + } +} + +impl ApiEventMetric for SuccessBasedRoutingPayloadWrapper { + fn get_api_event_type(&self) -> Option { + Some(ApiEventsType::Routing) + } +} + +impl ApiEventMetric for ToggleSuccessBasedRoutingWrapper { + fn get_api_event_type(&self) -> Option { + Some(ApiEventsType::Routing) + } +} + +impl ApiEventMetric for SuccessBasedRoutingUpdateConfigQuery { + fn get_api_event_type(&self) -> Option { + Some(ApiEventsType::Routing) + } +} diff --git a/crates/api_models/src/routing.rs b/crates/api_models/src/routing.rs index bec917b06c..5023d30f72 100644 --- a/crates/api_models/src/routing.rs +++ b/crates/api_models/src/routing.rs @@ -260,6 +260,7 @@ pub enum RoutingAlgorithmKind { Priority, VolumeSplit, Advanced, + Dynamic, } #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] @@ -495,3 +496,138 @@ pub struct RoutingLinkWrapper { pub profile_id: common_utils::id_type::ProfileId, pub algorithm_id: RoutingAlgorithmId, } + +#[derive(Debug, Default, Clone, serde::Serialize, serde::Deserialize)] +pub struct DynamicAlgorithmWithTimestamp { + pub algorithm_id: Option, + pub timestamp: i64, +} + +#[derive(Debug, Default, Clone, serde::Serialize, serde::Deserialize)] +pub struct DynamicRoutingAlgorithmRef { + pub success_based_algorithm: + Option>, +} + +impl DynamicRoutingAlgorithmRef { + pub fn update_algorithm_id(&mut self, new_id: common_utils::id_type::RoutingId) { + self.success_based_algorithm = Some(DynamicAlgorithmWithTimestamp { + algorithm_id: Some(new_id), + timestamp: common_utils::date_time::now_unix_timestamp(), + }) + } +} + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, ToSchema)] +pub struct ToggleSuccessBasedRoutingQuery { + pub status: bool, +} + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, ToSchema)] +pub struct SuccessBasedRoutingUpdateConfigQuery { + #[schema(value_type = String)] + pub algorithm_id: common_utils::id_type::RoutingId, + #[schema(value_type = String)] + pub profile_id: common_utils::id_type::ProfileId, +} + +#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] +pub struct ToggleSuccessBasedRoutingWrapper { + pub profile_id: common_utils::id_type::ProfileId, + pub status: bool, +} + +#[derive(Clone, Debug, serde::Deserialize, serde::Serialize, ToSchema)] +pub struct ToggleSuccessBasedRoutingPath { + #[schema(value_type = String)] + pub profile_id: common_utils::id_type::ProfileId, +} +#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, ToSchema)] +pub struct SuccessBasedRoutingConfig { + pub params: Option>, + pub config: Option, +} + +impl Default for SuccessBasedRoutingConfig { + fn default() -> Self { + Self { + params: Some(vec![SuccessBasedRoutingConfigParams::PaymentMethod]), + config: Some(SuccessBasedRoutingConfigBody { + min_aggregates_size: Some(2), + default_success_rate: Some(100.0), + max_aggregates_size: Some(3), + current_block_threshold: Some(CurrentBlockThreshold { + duration_in_mins: Some(5), + max_total_count: Some(2), + }), + }), + } + } +} + +#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, ToSchema)] +pub enum SuccessBasedRoutingConfigParams { + PaymentMethod, + PaymentMethodType, + Currency, + AuthenticationType, +} + +#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, ToSchema)] +pub struct SuccessBasedRoutingConfigBody { + pub min_aggregates_size: Option, + pub default_success_rate: Option, + pub max_aggregates_size: Option, + pub current_block_threshold: Option, +} + +#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, ToSchema)] +pub struct CurrentBlockThreshold { + pub duration_in_mins: Option, + pub max_total_count: Option, +} + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +pub struct SuccessBasedRoutingPayloadWrapper { + pub updated_config: SuccessBasedRoutingConfig, + pub algorithm_id: common_utils::id_type::RoutingId, + pub profile_id: common_utils::id_type::ProfileId, +} + +impl SuccessBasedRoutingConfig { + pub fn update(&mut self, new: Self) { + if let Some(params) = new.params { + self.params = Some(params) + } + if let Some(new_config) = new.config { + self.config.as_mut().map(|config| config.update(new_config)); + } + } +} + +impl SuccessBasedRoutingConfigBody { + pub fn update(&mut self, new: Self) { + if let Some(min_aggregates_size) = new.min_aggregates_size { + self.min_aggregates_size = Some(min_aggregates_size) + } + if let Some(default_success_rate) = new.default_success_rate { + self.default_success_rate = Some(default_success_rate) + } + if let Some(max_aggregates_size) = new.max_aggregates_size { + self.max_aggregates_size = Some(max_aggregates_size) + } + if let Some(current_block_threshold) = new.current_block_threshold { + self.current_block_threshold + .as_mut() + .map(|threshold| threshold.update(current_block_threshold)); + } + } +} + +impl CurrentBlockThreshold { + pub fn update(&mut self, new: Self) { + if let Some(max_total_count) = new.max_total_count { + self.max_total_count = Some(max_total_count) + } + } +} diff --git a/crates/diesel_models/src/business_profile.rs b/crates/diesel_models/src/business_profile.rs index 029add42ce..a72263b96a 100644 --- a/crates/diesel_models/src/business_profile.rs +++ b/crates/diesel_models/src/business_profile.rs @@ -53,6 +53,7 @@ pub struct BusinessProfile { pub tax_connector_id: Option, pub is_tax_connector_enabled: Option, pub version: common_enums::ApiVersion, + pub dynamic_routing_algorithm: Option, } #[cfg(feature = "v1")] @@ -129,6 +130,7 @@ pub struct BusinessProfileUpdateInternal { pub always_collect_shipping_details_from_wallet_connector: Option, pub tax_connector_id: Option, pub is_tax_connector_enabled: Option, + pub dynamic_routing_algorithm: Option, } #[cfg(feature = "v1")] @@ -164,6 +166,7 @@ impl BusinessProfileUpdateInternal { always_collect_shipping_details_from_wallet_connector, tax_connector_id, is_tax_connector_enabled, + dynamic_routing_algorithm, } = self; BusinessProfile { profile_id: source.profile_id, @@ -217,6 +220,8 @@ impl BusinessProfileUpdateInternal { tax_connector_id: tax_connector_id.or(source.tax_connector_id), is_tax_connector_enabled: is_tax_connector_enabled.or(source.is_tax_connector_enabled), version: source.version, + dynamic_routing_algorithm: dynamic_routing_algorithm + .or(source.dynamic_routing_algorithm), } } } @@ -266,6 +271,7 @@ pub struct BusinessProfile { pub default_fallback_routing: Option, pub id: common_utils::id_type::ProfileId, pub version: common_enums::ApiVersion, + pub dynamic_routing_algorithm: Option, } impl BusinessProfile { @@ -452,6 +458,7 @@ impl BusinessProfileUpdateInternal { .or(source.payout_routing_algorithm_id), default_fallback_routing: default_fallback_routing.or(source.default_fallback_routing), version: source.version, + dynamic_routing_algorithm: None, } } } @@ -502,6 +509,7 @@ impl From for BusinessProfile { payout_routing_algorithm_id: new.payout_routing_algorithm_id, default_fallback_routing: new.default_fallback_routing, version: new.version, + dynamic_routing_algorithm: None, } } } diff --git a/crates/diesel_models/src/enums.rs b/crates/diesel_models/src/enums.rs index 8f9469c028..68809d8d2f 100644 --- a/crates/diesel_models/src/enums.rs +++ b/crates/diesel_models/src/enums.rs @@ -50,6 +50,7 @@ pub enum RoutingAlgorithmKind { Priority, VolumeSplit, Advanced, + Dynamic, } #[derive( diff --git a/crates/diesel_models/src/schema.rs b/crates/diesel_models/src/schema.rs index 6e8918f02a..affae09f01 100644 --- a/crates/diesel_models/src/schema.rs +++ b/crates/diesel_models/src/schema.rs @@ -209,6 +209,7 @@ diesel::table! { tax_connector_id -> Nullable, is_tax_connector_enabled -> Nullable, version -> ApiVersion, + dynamic_routing_algorithm -> Nullable, } } diff --git a/crates/diesel_models/src/schema_v2.rs b/crates/diesel_models/src/schema_v2.rs index bd64c4bb00..e71e406c20 100644 --- a/crates/diesel_models/src/schema_v2.rs +++ b/crates/diesel_models/src/schema_v2.rs @@ -216,6 +216,7 @@ diesel::table! { #[max_length = 64] id -> Varchar, version -> ApiVersion, + dynamic_routing_algorithm -> Nullable, } } diff --git a/crates/hyperswitch_domain_models/src/business_profile.rs b/crates/hyperswitch_domain_models/src/business_profile.rs index c6b76680d4..b13d79175c 100644 --- a/crates/hyperswitch_domain_models/src/business_profile.rs +++ b/crates/hyperswitch_domain_models/src/business_profile.rs @@ -54,6 +54,7 @@ pub struct BusinessProfile { pub tax_connector_id: Option, pub is_tax_connector_enabled: bool, pub version: common_enums::ApiVersion, + pub dynamic_routing_algorithm: Option, } #[cfg(feature = "v1")] @@ -90,6 +91,7 @@ pub struct BusinessProfileSetter { pub always_collect_shipping_details_from_wallet_connector: Option, pub tax_connector_id: Option, pub is_tax_connector_enabled: bool, + pub dynamic_routing_algorithm: Option, } #[cfg(feature = "v1")] @@ -133,6 +135,7 @@ impl From for BusinessProfile { tax_connector_id: value.tax_connector_id, is_tax_connector_enabled: value.is_tax_connector_enabled, version: consts::API_VERSION, + dynamic_routing_algorithm: value.dynamic_routing_algorithm, } } } @@ -178,6 +181,7 @@ pub struct BusinessProfileGeneralUpdate { pub always_collect_shipping_details_from_wallet_connector: Option, pub tax_connector_id: Option, pub is_tax_connector_enabled: Option, + pub dynamic_routing_algorithm: Option, } #[cfg(feature = "v1")] @@ -188,6 +192,9 @@ pub enum BusinessProfileUpdate { routing_algorithm: Option, payout_routing_algorithm: Option, }, + DynamicRoutingAlgorithmUpdate { + dynamic_routing_algorithm: Option, + }, ExtendedCardInfoUpdate { is_extended_card_info_enabled: Option, }, @@ -230,6 +237,7 @@ impl From for BusinessProfileUpdateInternal { always_collect_shipping_details_from_wallet_connector, tax_connector_id, is_tax_connector_enabled, + dynamic_routing_algorithm, } = *update; Self { @@ -263,6 +271,7 @@ impl From for BusinessProfileUpdateInternal { always_collect_shipping_details_from_wallet_connector, tax_connector_id, is_tax_connector_enabled, + dynamic_routing_algorithm, } } BusinessProfileUpdate::RoutingAlgorithmUpdate { @@ -298,6 +307,41 @@ impl From for BusinessProfileUpdateInternal { always_collect_shipping_details_from_wallet_connector: None, tax_connector_id: None, is_tax_connector_enabled: None, + dynamic_routing_algorithm: None, + }, + BusinessProfileUpdate::DynamicRoutingAlgorithmUpdate { + dynamic_routing_algorithm, + } => Self { + profile_name: None, + modified_at: now, + return_url: None, + enable_payment_response_hash: None, + payment_response_hash_key: None, + redirect_to_merchant_with_http_post: None, + webhook_details: None, + metadata: None, + routing_algorithm: None, + intent_fulfillment_time: None, + frm_routing_algorithm: None, + payout_routing_algorithm: None, + is_recon_enabled: None, + applepay_verified_domains: None, + payment_link_config: None, + session_expiry: None, + authentication_connector_details: None, + payout_link_config: None, + is_extended_card_info_enabled: None, + extended_card_info_config: None, + is_connector_agnostic_mit_enabled: None, + use_billing_as_payment_method_billing: None, + collect_shipping_details_from_wallet_connector: None, + collect_billing_details_from_wallet_connector: None, + outgoing_webhook_custom_http_headers: None, + always_collect_billing_details_from_wallet_connector: None, + always_collect_shipping_details_from_wallet_connector: None, + tax_connector_id: None, + is_tax_connector_enabled: None, + dynamic_routing_algorithm, }, BusinessProfileUpdate::ExtendedCardInfoUpdate { is_extended_card_info_enabled, @@ -331,6 +375,7 @@ impl From for BusinessProfileUpdateInternal { always_collect_shipping_details_from_wallet_connector: None, tax_connector_id: None, is_tax_connector_enabled: None, + dynamic_routing_algorithm: None, }, BusinessProfileUpdate::ConnectorAgnosticMitUpdate { is_connector_agnostic_mit_enabled, @@ -364,6 +409,7 @@ impl From for BusinessProfileUpdateInternal { always_collect_shipping_details_from_wallet_connector: None, tax_connector_id: None, is_tax_connector_enabled: None, + dynamic_routing_algorithm: None, }, } } @@ -416,6 +462,7 @@ impl super::behaviour::Conversion for BusinessProfile { tax_connector_id: self.tax_connector_id, is_tax_connector_enabled: Some(self.is_tax_connector_enabled), version: self.version, + dynamic_routing_algorithm: self.dynamic_routing_algorithm, }) } @@ -480,6 +527,7 @@ impl super::behaviour::Conversion for BusinessProfile { tax_connector_id: item.tax_connector_id, is_tax_connector_enabled: item.is_tax_connector_enabled.unwrap_or(false), version: item.version, + dynamic_routing_algorithm: item.dynamic_routing_algorithm, }) } .await @@ -984,6 +1032,7 @@ impl super::behaviour::Conversion for BusinessProfile { tax_connector_id: self.tax_connector_id, is_tax_connector_enabled: Some(self.is_tax_connector_enabled), version: self.version, + dynamic_routing_algorithm: None, }) } diff --git a/crates/openapi/src/openapi.rs b/crates/openapi/src/openapi.rs index 3b63b7ac20..6219fd6de2 100644 --- a/crates/openapi/src/openapi.rs +++ b/crates/openapi/src/openapi.rs @@ -158,6 +158,8 @@ Never share your secret api keys. Keep them guarded and secure. routes::routing::routing_retrieve_linked_config, routes::routing::routing_retrieve_default_config_for_profiles, routes::routing::routing_update_default_config_for_profile, + routes::routing::toggle_success_based_routing, + routes::routing::success_based_routing_update_configs, // Routes for blocklist routes::blocklist::remove_entry_from_blocklist, @@ -546,6 +548,13 @@ Never share your secret api keys. Keep them guarded and secure. api_models::routing::StraightThroughAlgorithm, api_models::routing::ConnectorVolumeSplit, api_models::routing::ConnectorSelection, + api_models::routing::ToggleSuccessBasedRoutingQuery, + api_models::routing::SuccessBasedRoutingConfig, + api_models::routing::SuccessBasedRoutingConfigParams, + api_models::routing::SuccessBasedRoutingConfigBody, + api_models::routing::CurrentBlockThreshold, + api_models::routing::SuccessBasedRoutingUpdateConfigQuery, + api_models::routing::ToggleSuccessBasedRoutingPath, api_models::routing::ast::RoutableChoiceKind, api_models::enums::RoutableConnectors, api_models::routing::ast::ProgramConnectorSelection, diff --git a/crates/openapi/src/routes/routing.rs b/crates/openapi/src/routes/routing.rs index 5e1e3f60c6..009708d860 100644 --- a/crates/openapi/src/routes/routing.rs +++ b/crates/openapi/src/routes/routing.rs @@ -255,3 +255,56 @@ pub async fn routing_retrieve_default_config_for_profiles() {} security(("api_key" = []), ("jwt_key" = [])) )] pub async fn routing_update_default_config_for_profile() {} + +#[cfg(feature = "v1")] +/// Routing - Toggle success based dynamic routing for profile +/// +/// Create a success based dynamic routing algorithm +#[utoipa::path( + post, + path = "/account/:account_id/business_profile/:profile_id/dynamic_routing/success_based/toggle", + params( + ("account_id" = String, Path, description = "Merchant id"), + ("profile_id" = String, Path, description = "Profile id under which Dynamic routing needs to be toggled"), + ("status" = bool, Query, description = "Boolean value for mentioning the expected state of dynamic routing"), + ), + responses( + (status = 200, description = "Routing Algorithm created", body = RoutingDictionaryRecord), + (status = 400, description = "Request body is malformed"), + (status = 500, description = "Internal server error"), + (status = 404, description = "Resource missing"), + (status = 422, description = "Unprocessable request"), + (status = 403, description = "Forbidden"), + ), + tag = "Routing", + operation_id = "Toggle success based dynamic routing algprithm", + security(("api_key" = []), ("jwt_key" = [])) +)] +pub async fn toggle_success_based_routing() {} + +#[cfg(feature = "v1")] +/// Routing - Update config for success based dynamic routing +/// +/// Update config for success based dynamic routing +#[utoipa::path( + patch, + path = "/account/:account_id/business_profile/:profile_id/dynamic_routing/success_based/config/:algorithm_id", + request_body = SuccessBasedRoutingConfig, + params( + ("account_id" = String, Path, description = "Merchant id"), + ("profile_id" = String, Path, description = "The unique identifier for a profile"), + ("algorithm_id" = String, Path, description = "The unique identifier for routing algorithm"), + ), + responses( + (status = 200, description = "Routing Algorithm updated", body = RoutingDictionaryRecord), + (status = 400, description = "Request body is malformed"), + (status = 500, description = "Internal server error"), + (status = 404, description = "Resource missing"), + (status = 422, description = "Unprocessable request"), + (status = 403, description = "Forbidden"), + ), + tag = "Routing", + operation_id = "Update configs for success based dynamic routing algorithm", + security(("admin_api_key" = [])) +)] +pub async fn success_based_routing_update_configs() {} diff --git a/crates/router/src/core/admin.rs b/crates/router/src/core/admin.rs index 5b80f64efa..2579b4ea16 100644 --- a/crates/router/src/core/admin.rs +++ b/crates/router/src/core/admin.rs @@ -3478,6 +3478,7 @@ impl BusinessProfileCreateBridge for api::BusinessProfileCreate { .always_collect_billing_details_from_wallet_connector, always_collect_shipping_details_from_wallet_connector: self .always_collect_shipping_details_from_wallet_connector, + dynamic_routing_algorithm: None, }, )) } @@ -3825,6 +3826,7 @@ impl BusinessProfileUpdateBridge for api::BusinessProfileUpdate { .always_collect_shipping_details_from_wallet_connector, tax_connector_id: self.tax_connector_id, is_tax_connector_enabled: self.is_tax_connector_enabled, + dynamic_routing_algorithm: self.dynamic_routing_algorithm, }, ))) } diff --git a/crates/router/src/core/payments/operations/payment_confirm.rs b/crates/router/src/core/payments/operations/payment_confirm.rs index 95e254f341..90158793f9 100644 --- a/crates/router/src/core/payments/operations/payment_confirm.rs +++ b/crates/router/src/core/payments/operations/payment_confirm.rs @@ -923,12 +923,12 @@ impl Domain> for Paymen Some(helpers::PaymentExternalAuthenticationFlow::PostAuthenticationFlow { authentication_id, }) => { - let authentication = authentication::perform_post_authentication( + let authentication = Box::pin(authentication::perform_post_authentication( state, key_store, business_profile.clone(), authentication_id.clone(), - ) + )) .await?; //If authentication is not successful, skip the payment connector flows and mark the payment as failure if authentication.authentication_status diff --git a/crates/router/src/core/routing.rs b/crates/router/src/core/routing.rs index fd58a2c571..2ce7506567 100644 --- a/crates/router/src/core/routing.rs +++ b/crates/router/src/core/routing.rs @@ -2,12 +2,13 @@ pub mod helpers; pub mod transformers; use api_models::{ - enums, mandates as mandates_api, + enums, mandates as mandates_api, routing, routing::{self as routing_types, RoutingRetrieveQuery}, }; use diesel_models::routing_algorithm::RoutingAlgorithm; use error_stack::ResultExt; use hyperswitch_domain_models::{mandates, payment_address}; +use router_env::metrics::add_attributes; use rustc_hash::FxHashSet; #[cfg(feature = "payouts")] @@ -411,42 +412,89 @@ pub async fn link_routing_config( core_utils::validate_profile_id_from_auth_layer(authentication_profile_id, &business_profile)?; - let mut routing_ref: routing_types::RoutingAlgorithmRef = business_profile - .routing_algorithm - .clone() - .map(|val| val.parse_value("RoutingAlgorithmRef")) - .transpose() - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("unable to deserialize routing algorithm ref from merchant account")? - .unwrap_or_default(); + match routing_algorithm.kind { + diesel_models::enums::RoutingAlgorithmKind::Dynamic => { + let mut dynamic_routing_ref: routing_types::DynamicRoutingAlgorithmRef = + business_profile + .dynamic_routing_algorithm + .clone() + .map(|val| val.parse_value("DynamicRoutingAlgorithmRef")) + .transpose() + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable( + "unable to deserialize Dynamic routing algorithm ref from business profile", + )? + .unwrap_or_default(); - utils::when(routing_algorithm.algorithm_for != *transaction_type, || { - Err(errors::ApiErrorResponse::PreconditionFailed { - message: format!( - "Cannot use {}'s routing algorithm for {} operation", - routing_algorithm.algorithm_for, transaction_type - ), - }) - })?; + utils::when( + matches!( + dynamic_routing_ref.success_based_algorithm, + Some(routing_types::DynamicAlgorithmWithTimestamp { + algorithm_id: Some(ref id), + timestamp: _ + }) if id == &algorithm_id + ), + || { + Err(errors::ApiErrorResponse::PreconditionFailed { + message: "Algorithm is already active".to_string(), + }) + }, + )?; - utils::when( - routing_ref.algorithm_id == Some(algorithm_id.clone()), - || { - Err(errors::ApiErrorResponse::PreconditionFailed { - message: "Algorithm is already active".to_string(), - }) - }, - )?; - routing_ref.update_algorithm_id(algorithm_id); - helpers::update_business_profile_active_algorithm_ref( - db, - key_manager_state, - &key_store, - business_profile, - routing_ref, - transaction_type, - ) - .await?; + dynamic_routing_ref.update_algorithm_id(algorithm_id); + helpers::update_business_profile_active_dynamic_algorithm_ref( + db, + key_manager_state, + &key_store, + business_profile, + dynamic_routing_ref, + ) + .await?; + } + diesel_models::enums::RoutingAlgorithmKind::Single + | diesel_models::enums::RoutingAlgorithmKind::Priority + | diesel_models::enums::RoutingAlgorithmKind::Advanced + | diesel_models::enums::RoutingAlgorithmKind::VolumeSplit => { + let mut routing_ref: routing_types::RoutingAlgorithmRef = business_profile + .routing_algorithm + .clone() + .map(|val| val.parse_value("RoutingAlgorithmRef")) + .transpose() + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable( + "unable to deserialize routing algorithm ref from business profile", + )? + .unwrap_or_default(); + + utils::when(routing_algorithm.algorithm_for != *transaction_type, || { + Err(errors::ApiErrorResponse::PreconditionFailed { + message: format!( + "Cannot use {}'s routing algorithm for {} operation", + routing_algorithm.algorithm_for, transaction_type + ), + }) + })?; + + utils::when( + routing_ref.algorithm_id == Some(algorithm_id.clone()), + || { + Err(errors::ApiErrorResponse::PreconditionFailed { + message: "Algorithm is already active".to_string(), + }) + }, + )?; + routing_ref.update_algorithm_id(algorithm_id); + helpers::update_business_profile_active_algorithm_ref( + db, + key_manager_state, + &key_store, + business_profile, + routing_ref, + transaction_type, + ) + .await?; + } + }; metrics::ROUTING_LINK_CONFIG_SUCCESS_RESPONSE.add(&metrics::CONTEXT, 1, &[]); Ok(service_api::ApplicationResponse::Json( @@ -1111,3 +1159,192 @@ pub async fn update_default_routing_config_for_profile( }, )) } + +#[cfg(feature = "v1")] +pub async fn toggle_success_based_routing( + state: SessionState, + merchant_account: domain::MerchantAccount, + key_store: domain::MerchantKeyStore, + status: bool, + profile_id: common_utils::id_type::ProfileId, +) -> RouterResponse { + metrics::ROUTING_CREATE_REQUEST_RECEIVED.add( + &metrics::CONTEXT, + 1, + &add_attributes([("profile_id", profile_id.get_string_repr().to_owned())]), + ); + let db = state.store.as_ref(); + let key_manager_state = &(&state).into(); + + let business_profile: domain::BusinessProfile = core_utils::validate_and_get_business_profile( + db, + key_manager_state, + &key_store, + Some(&profile_id), + merchant_account.get_id(), + ) + .await? + .get_required_value("BusinessProfile") + .change_context(errors::ApiErrorResponse::BusinessProfileNotFound { + id: profile_id.get_string_repr().to_owned(), + })?; + + let mut success_based_dynamic_routing_algo_ref: routing_types::DynamicRoutingAlgorithmRef = + business_profile + .dynamic_routing_algorithm + .clone() + .map(|val| val.parse_value("DynamicRoutingAlgorithmRef")) + .transpose() + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable( + "unable to deserialize dynamic routing algorithm ref from business profile", + )? + .unwrap_or_default(); + + if status { + let default_success_based_routing_config = routing::SuccessBasedRoutingConfig::default(); + let algorithm_id = common_utils::generate_routing_id_of_default_length(); + let timestamp = common_utils::date_time::now(); + let algo = RoutingAlgorithm { + algorithm_id: algorithm_id.clone(), + profile_id: business_profile.get_id().to_owned(), + merchant_id: merchant_account.get_id().to_owned(), + name: "Dynamic routing algorithm".to_string(), + description: None, + kind: diesel_models::enums::RoutingAlgorithmKind::Dynamic, + algorithm_data: serde_json::json!(default_success_based_routing_config), + created_at: timestamp, + modified_at: timestamp, + algorithm_for: common_enums::TransactionType::Payment, + }; + + let record = db + .insert_routing_algorithm(algo) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Unable to insert record in routing algorithm table")?; + + success_based_dynamic_routing_algo_ref.update_algorithm_id(algorithm_id); + helpers::update_business_profile_active_dynamic_algorithm_ref( + db, + key_manager_state, + &key_store, + business_profile, + success_based_dynamic_routing_algo_ref, + ) + .await?; + + let new_record = record.foreign_into(); + + metrics::ROUTING_CREATE_SUCCESS_RESPONSE.add( + &metrics::CONTEXT, + 1, + &add_attributes([("profile_id", profile_id.get_string_repr().to_owned())]), + ); + Ok(service_api::ApplicationResponse::Json(new_record)) + } else { + let timestamp = common_utils::date_time::now_unix_timestamp(); + match success_based_dynamic_routing_algo_ref.success_based_algorithm { + Some(algorithm_ref) => { + if let Some(algorithm_id) = algorithm_ref.algorithm_id { + let dynamic_routing_algorithm = routing_types::DynamicRoutingAlgorithmRef { + success_based_algorithm: Some( + routing_types::DynamicAlgorithmWithTimestamp { + algorithm_id: None, + timestamp, + }, + ), + }; + + let record = db + .find_routing_algorithm_by_profile_id_algorithm_id( + business_profile.get_id(), + &algorithm_id, + ) + .await + .to_not_found_response(errors::ApiErrorResponse::ResourceIdNotFound)?; + let response = record.foreign_into(); + helpers::update_business_profile_active_dynamic_algorithm_ref( + db, + key_manager_state, + &key_store, + business_profile, + dynamic_routing_algorithm, + ) + .await?; + + metrics::ROUTING_UNLINK_CONFIG_SUCCESS_RESPONSE.add( + &metrics::CONTEXT, + 1, + &add_attributes([("profile_id", profile_id.get_string_repr().to_owned())]), + ); + + Ok(service_api::ApplicationResponse::Json(response)) + } else { + Err(errors::ApiErrorResponse::PreconditionFailed { + message: "Algorithm is already inactive".to_string(), + })? + } + } + None => Err(errors::ApiErrorResponse::PreconditionFailed { + message: "Algorithm is already inactive".to_string(), + })?, + } + } +} + +#[cfg(feature = "v1")] +pub async fn success_based_routing_update_configs( + state: SessionState, + request: routing_types::SuccessBasedRoutingConfig, + algorithm_id: common_utils::id_type::RoutingId, + profile_id: common_utils::id_type::ProfileId, +) -> RouterResponse { + metrics::ROUTING_UPDATE_CONFIG_FOR_PROFILE.add( + &metrics::CONTEXT, + 1, + &add_attributes([("profile_id", profile_id.get_string_repr().to_owned())]), + ); + let db = state.store.as_ref(); + let dynamic_routing_algo_to_update = db + .find_routing_algorithm_by_profile_id_algorithm_id(&profile_id, &algorithm_id) + .await + .to_not_found_response(errors::ApiErrorResponse::ResourceIdNotFound)?; + + let mut config_to_update: routing::SuccessBasedRoutingConfig = dynamic_routing_algo_to_update + .algorithm_data + .parse_value::("SuccessBasedRoutingConfig") + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("unable to deserialize algorithm data from routing table into SuccessBasedRoutingConfig")?; + + config_to_update.update(request); + + let algorithm_id = common_utils::generate_routing_id_of_default_length(); + let timestamp = common_utils::date_time::now(); + let algo = RoutingAlgorithm { + algorithm_id, + profile_id: dynamic_routing_algo_to_update.profile_id, + merchant_id: dynamic_routing_algo_to_update.merchant_id, + name: dynamic_routing_algo_to_update.name, + description: dynamic_routing_algo_to_update.description, + kind: dynamic_routing_algo_to_update.kind, + algorithm_data: serde_json::json!(config_to_update), + created_at: timestamp, + modified_at: timestamp, + algorithm_for: dynamic_routing_algo_to_update.algorithm_for, + }; + let record = db + .insert_routing_algorithm(algo) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Unable to insert record in routing algorithm table")?; + + let new_record = record.foreign_into(); + + metrics::ROUTING_UPDATE_CONFIG_FOR_PROFILE_SUCCESS_RESPONSE.add( + &metrics::CONTEXT, + 1, + &add_attributes([("profile_id", profile_id.get_string_repr().to_owned())]), + ); + Ok(service_api::ApplicationResponse::Json(new_record)) +} diff --git a/crates/router/src/core/routing/helpers.rs b/crates/router/src/core/routing/helpers.rs index bd4107a48d..f818dc05f5 100644 --- a/crates/router/src/core/routing/helpers.rs +++ b/crates/router/src/core/routing/helpers.rs @@ -236,6 +236,33 @@ pub async fn update_business_profile_active_algorithm_ref( Ok(()) } +#[cfg(feature = "v1")] +pub async fn update_business_profile_active_dynamic_algorithm_ref( + db: &dyn StorageInterface, + key_manager_state: &KeyManagerState, + merchant_key_store: &domain::MerchantKeyStore, + current_business_profile: domain::BusinessProfile, + dynamic_routing_algorithm: routing_types::DynamicRoutingAlgorithmRef, +) -> RouterResult<()> { + let ref_val = dynamic_routing_algorithm + .encode_to_value() + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed to convert dynamic routing ref to value")?; + let business_profile_update = domain::BusinessProfileUpdate::DynamicRoutingAlgorithmUpdate { + dynamic_routing_algorithm: Some(ref_val), + }; + db.update_business_profile_by_profile_id( + key_manager_state, + merchant_key_store, + current_business_profile, + business_profile_update, + ) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed to update dynamic routing algorithm ref in business profile")?; + Ok(()) +} + #[cfg(feature = "v2")] #[derive(Clone, Debug)] pub struct RoutingAlgorithmHelpers<'h> { diff --git a/crates/router/src/core/routing/transformers.rs b/crates/router/src/core/routing/transformers.rs index 6bffde12a9..652422086b 100644 --- a/crates/router/src/core/routing/transformers.rs +++ b/crates/router/src/core/routing/transformers.rs @@ -72,6 +72,7 @@ impl ForeignFrom for RoutingAlgorithmKind { storage_enums::RoutingAlgorithmKind::Priority => Self::Priority, storage_enums::RoutingAlgorithmKind::VolumeSplit => Self::VolumeSplit, storage_enums::RoutingAlgorithmKind::Advanced => Self::Advanced, + storage_enums::RoutingAlgorithmKind::Dynamic => Self::Dynamic, } } } @@ -83,6 +84,7 @@ impl ForeignFrom for storage_enums::RoutingAlgorithmKind { RoutingAlgorithmKind::Priority => Self::Priority, RoutingAlgorithmKind::VolumeSplit => Self::VolumeSplit, RoutingAlgorithmKind::Advanced => Self::Advanced, + RoutingAlgorithmKind::Dynamic => Self::Dynamic, } } } diff --git a/crates/router/src/routes/app.rs b/crates/router/src/routes/app.rs index 06d2d38f83..da1a50b82e 100644 --- a/crates/router/src/routes/app.rs +++ b/crates/router/src/routes/app.rs @@ -1626,6 +1626,23 @@ impl BusinessProfile { ) .service( web::scope("/{profile_id}") + .service( + web::scope("/dynamic_routing").service( + web::scope("/success_based") + .service( + web::resource("/toggle").route( + web::post().to(routing::toggle_success_based_routing), + ), + ) + .service(web::resource("/config/{algorithm_id}").route( + web::patch().to(|state, req, path, payload| { + routing::success_based_routing_update_configs( + state, req, path, payload, + ) + }), + )), + ), + ) .service( web::resource("") .route(web::get().to(admin::business_profile_retrieve)) diff --git a/crates/router/src/routes/lock_utils.rs b/crates/router/src/routes/lock_utils.rs index 0cdfd90e99..e63b00cd9b 100644 --- a/crates/router/src/routes/lock_utils.rs +++ b/crates/router/src/routes/lock_utils.rs @@ -65,6 +65,8 @@ impl From for ApiIdentifier { | Flow::RoutingDeleteConfig | Flow::DecisionManagerDeleteConfig | Flow::DecisionManagerRetrieveConfig + | Flow::ToggleDynamicRouting + | Flow::UpdateDynamicRoutingConfigs | Flow::DecisionManagerUpsertConfig => Self::Routing, Flow::RetrieveForexFlow => Self::Forex, diff --git a/crates/router/src/routes/routing.rs b/crates/router/src/routes/routing.rs index ee0a10a822..86ad2078f6 100644 --- a/crates/router/src/routes/routing.rs +++ b/crates/router/src/routes/routing.rs @@ -978,3 +978,88 @@ pub async fn routing_update_default_config_for_profile( )) .await } + +#[cfg(all(feature = "olap", feature = "v1"))] +#[instrument(skip_all)] +pub async fn toggle_success_based_routing( + state: web::Data, + req: HttpRequest, + query: web::Query, + path: web::Path, +) -> impl Responder { + let flow = Flow::ToggleDynamicRouting; + let wrapper = routing_types::ToggleSuccessBasedRoutingWrapper { + status: query.into_inner().status, + profile_id: path.into_inner().profile_id, + }; + Box::pin(oss_api::server_wrap( + flow, + state, + &req, + wrapper.clone(), + |state, + auth: auth::AuthenticationData, + wrapper: routing_types::ToggleSuccessBasedRoutingWrapper, + _| { + routing::toggle_success_based_routing( + state, + auth.merchant_account, + auth.key_store, + wrapper.status, + wrapper.profile_id, + ) + }, + #[cfg(not(feature = "release"))] + auth::auth_type( + &auth::HeaderAuth(auth::ApiKeyAuth), + &auth::JWTAuthProfileFromRoute { + profile_id: wrapper.profile_id, + required_permission: Permission::RoutingWrite, + minimum_entity_level: EntityType::Merchant, + }, + req.headers(), + ), + #[cfg(feature = "release")] + &auth::JWTAuthProfileFromRoute { + profile_id: wrapper.profile_id, + required_permission: Permission::RoutingWrite, + minimum_entity_level: EntityType::Merchant, + }, + api_locking::LockAction::NotApplicable, + )) + .await +} + +#[cfg(all(feature = "olap", feature = "v1"))] +#[instrument(skip_all)] +pub async fn success_based_routing_update_configs( + state: web::Data, + req: HttpRequest, + path: web::Path, + json_payload: web::Json, +) -> impl Responder { + let flow = Flow::UpdateDynamicRoutingConfigs; + let routing_payload_wrapper = routing_types::SuccessBasedRoutingPayloadWrapper { + updated_config: json_payload.into_inner(), + algorithm_id: path.clone().algorithm_id, + profile_id: path.clone().profile_id, + }; + Box::pin(oss_api::server_wrap( + flow, + state, + &req, + routing_payload_wrapper, + |state, _, wrapper: routing_types::SuccessBasedRoutingPayloadWrapper, _| async { + Box::pin(routing::success_based_routing_update_configs( + state, + wrapper.updated_config, + wrapper.algorithm_id, + wrapper.profile_id, + )) + .await + }, + &auth::AdminApiAuth, + api_locking::LockAction::NotApplicable, + )) + .await +} diff --git a/crates/router/src/types/api/admin.rs b/crates/router/src/types/api/admin.rs index 3d915f838e..9d90ef530a 100644 --- a/crates/router/src/types/api/admin.rs +++ b/crates/router/src/types/api/admin.rs @@ -347,6 +347,7 @@ pub async fn create_business_profile_from_merchant_account( .map(Into::into), tax_connector_id: request.tax_connector_id, is_tax_connector_enabled: request.is_tax_connector_enabled, + dynamic_routing_algorithm: None, }, )) } diff --git a/crates/router_env/src/logger/types.rs b/crates/router_env/src/logger/types.rs index 6126ca9afb..d815325a81 100644 --- a/crates/router_env/src/logger/types.rs +++ b/crates/router_env/src/logger/types.rs @@ -244,6 +244,10 @@ pub enum Flow { RoutingUpdateDefaultConfig, /// Routing delete config RoutingDeleteConfig, + /// Toggle dynamic routing + ToggleDynamicRouting, + /// Update dynamic routing config + UpdateDynamicRoutingConfigs, /// Add record to blocklist AddToBlocklist, /// Delete record from blocklist diff --git a/migrations/2024-09-05-155712_add_new_variant_in_routing_algorithm_kind_type/down.sql b/migrations/2024-09-05-155712_add_new_variant_in_routing_algorithm_kind_type/down.sql new file mode 100644 index 0000000000..9ca7ff5389 --- /dev/null +++ b/migrations/2024-09-05-155712_add_new_variant_in_routing_algorithm_kind_type/down.sql @@ -0,0 +1,6 @@ +-- This file should undo anything in `up.sql` +DELETE FROM pg_enum +WHERE enumlabel = 'dynamic' +AND enumtypid = ( + SELECT oid FROM pg_type WHERE typname = 'RoutingAlgorithmKind' +); diff --git a/migrations/2024-09-05-155712_add_new_variant_in_routing_algorithm_kind_type/up.sql b/migrations/2024-09-05-155712_add_new_variant_in_routing_algorithm_kind_type/up.sql new file mode 100644 index 0000000000..ebe6188c7d --- /dev/null +++ b/migrations/2024-09-05-155712_add_new_variant_in_routing_algorithm_kind_type/up.sql @@ -0,0 +1,2 @@ +-- Your SQL goes here +ALTER TYPE "RoutingAlgorithmKind" ADD VALUE 'dynamic'; diff --git a/migrations/2024-09-05-160455_add_new_col_is_dynamic_routing_algorithm_in_business_profile/down.sql b/migrations/2024-09-05-160455_add_new_col_is_dynamic_routing_algorithm_in_business_profile/down.sql new file mode 100644 index 0000000000..61c22e45ea --- /dev/null +++ b/migrations/2024-09-05-160455_add_new_col_is_dynamic_routing_algorithm_in_business_profile/down.sql @@ -0,0 +1,3 @@ +-- This file should undo anything in `up.sql` +ALTER TABLE business_profile +DROP COLUMN dynamic_routing_algorithm; diff --git a/migrations/2024-09-05-160455_add_new_col_is_dynamic_routing_algorithm_in_business_profile/up.sql b/migrations/2024-09-05-160455_add_new_col_is_dynamic_routing_algorithm_in_business_profile/up.sql new file mode 100644 index 0000000000..2482fc9f59 --- /dev/null +++ b/migrations/2024-09-05-160455_add_new_col_is_dynamic_routing_algorithm_in_business_profile/up.sql @@ -0,0 +1,3 @@ +-- Your SQL goes here +ALTER TABLE business_profile +ADD COLUMN dynamic_routing_algorithm JSON DEFAULT NULL;