diff --git a/src/Managing.Api/Controllers/BacktestController.cs b/src/Managing.Api/Controllers/BacktestController.cs
index 0e15ee83..9ddfe289 100644
--- a/src/Managing.Api/Controllers/BacktestController.cs
+++ b/src/Managing.Api/Controllers/BacktestController.cs
@@ -651,7 +651,6 @@ public class BacktestController : BaseController
{
var user = await GetUser();
-
if (string.IsNullOrEmpty(request.UniversalConfig.ScenarioName) && request.UniversalConfig.Scenario == null)
{
return BadRequest("Either scenario name or scenario object is required in universal configuration");
diff --git a/src/Managing.Api/appsettings.json b/src/Managing.Api/appsettings.json
index a8a73bdb..b74e7966 100644
--- a/src/Managing.Api/appsettings.json
+++ b/src/Managing.Api/appsettings.json
@@ -31,6 +31,10 @@
"DebitEndpoint": "/api/credits/debit",
"RefundEndpoint": "/api/credits/refund"
},
+ "Flagsmith": {
+ "ApiKey": "ser.ShJJJMtWYS9fwuzd83ejwR",
+ "ApiUrl": "https://flag.kaigen.ai/api/v1/"
+ },
"N8n": {
"WebhookUrl": "https://n8n.kai.managing.live/webhook/fa9308b6-983b-42ec-b085-71599d655951",
"IndicatorRequestWebhookUrl": "https://n8n.kai.managing.live/webhook/3aa07b66-1e64-46a7-8618-af300914cb11",
diff --git a/src/Managing.Application.Abstractions/Services/IFlagsmithService.cs b/src/Managing.Application.Abstractions/Services/IFlagsmithService.cs
new file mode 100644
index 00000000..42503917
--- /dev/null
+++ b/src/Managing.Application.Abstractions/Services/IFlagsmithService.cs
@@ -0,0 +1,39 @@
+namespace Managing.Application.Abstractions.Services;
+
+///
+/// Interface for Flagsmith feature flag service
+///
+public interface IFlagsmithService
+{
+ ///
+ /// Gets flags for a specific user identity
+ ///
+ /// The user identity identifier
+ /// Flags object for the identity
+ Task GetIdentityFlagsAsync(string identity);
+
+ ///
+ /// Checks if a feature is enabled for a specific identity
+ ///
+ /// The user identity identifier
+ /// The name of the feature flag
+ /// True if the feature is enabled
+ Task IsFeatureEnabledAsync(string identity, string featureName);
+
+ ///
+ /// Gets the feature value for a specific identity
+ ///
+ /// The user identity identifier
+ /// The name of the feature flag
+ /// The feature value as string
+ Task GetFeatureValueAsync(string identity, string featureName);
+}
+
+///
+/// Wrapper interface for Flagsmith flags to enable testing
+///
+public interface IFlagsmithFlags
+{
+ Task IsFeatureEnabled(string featureName);
+ Task GetFeatureValue(string featureName);
+}
diff --git a/src/Managing.Application/Managing.Application.csproj b/src/Managing.Application/Managing.Application.csproj
index 09a47e0a..636d6746 100644
--- a/src/Managing.Application/Managing.Application.csproj
+++ b/src/Managing.Application/Managing.Application.csproj
@@ -14,6 +14,7 @@
+
diff --git a/src/Managing.Application/Shared/FlagsmithService.cs b/src/Managing.Application/Shared/FlagsmithService.cs
new file mode 100644
index 00000000..bc33e1ec
--- /dev/null
+++ b/src/Managing.Application/Shared/FlagsmithService.cs
@@ -0,0 +1,122 @@
+using Flagsmith;
+using Managing.Application.Abstractions.Services;
+using Microsoft.Extensions.Logging;
+
+namespace Managing.Application.Shared;
+
+///
+/// Configuration settings for Flagsmith
+///
+public class FlagsmithSettings
+{
+ public string ApiKey { get; set; } = string.Empty;
+ ///
+ /// API URL for self-hosted Flagsmith instance (required).
+ ///
+ public string ApiUrl { get; set; } = string.Empty;
+}
+
+///
+/// Service for managing feature flags using Flagsmith
+///
+public class FlagsmithService : IFlagsmithService
+{
+ private readonly FlagsmithClient _flagsmithClient;
+ private readonly ILogger _logger;
+
+ public FlagsmithService(FlagsmithClient flagsmithClient, ILogger logger)
+ {
+ _flagsmithClient = flagsmithClient ?? throw new ArgumentNullException(nameof(flagsmithClient));
+ _logger = logger ?? throw new ArgumentNullException(nameof(logger));
+ }
+
+ public async Task GetIdentityFlagsAsync(string identity)
+ {
+ if (string.IsNullOrWhiteSpace(identity))
+ {
+ throw new ArgumentException("Identity cannot be null or empty", nameof(identity));
+ }
+
+ try
+ {
+ var flags = await _flagsmithClient.GetIdentityFlags(identity);
+ return new FlagsmithFlagsWrapper(flags);
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Error getting flags for identity {Identity}", identity);
+ throw;
+ }
+ }
+
+ public async Task IsFeatureEnabledAsync(string identity, string featureName)
+ {
+ if (string.IsNullOrWhiteSpace(identity))
+ {
+ throw new ArgumentException("Identity cannot be null or empty", nameof(identity));
+ }
+
+ if (string.IsNullOrWhiteSpace(featureName))
+ {
+ throw new ArgumentException("Feature name cannot be null or empty", nameof(featureName));
+ }
+
+ try
+ {
+ var flags = await GetIdentityFlagsAsync(identity);
+ return await flags.IsFeatureEnabled(featureName);
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Error checking feature {FeatureName} for identity {Identity}", featureName, identity);
+ return false; // Default to false on error
+ }
+ }
+
+ public async Task GetFeatureValueAsync(string identity, string featureName)
+ {
+ if (string.IsNullOrWhiteSpace(identity))
+ {
+ throw new ArgumentException("Identity cannot be null or empty", nameof(identity));
+ }
+
+ if (string.IsNullOrWhiteSpace(featureName))
+ {
+ throw new ArgumentException("Feature name cannot be null or empty", nameof(featureName));
+ }
+
+ try
+ {
+ var flags = await GetIdentityFlagsAsync(identity);
+ return await flags.GetFeatureValue(featureName);
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Error getting feature value {FeatureName} for identity {Identity}", featureName, identity);
+ return null; // Default to null on error
+ }
+ }
+}
+
+///
+/// Wrapper for Flagsmith flags to implement IFlagsmithFlags interface
+///
+internal class FlagsmithFlagsWrapper : IFlagsmithFlags
+{
+ private readonly IFlags _flags;
+
+ public FlagsmithFlagsWrapper(IFlags flags)
+ {
+ _flags = flags ?? throw new ArgumentNullException(nameof(flags));
+ }
+
+ public Task IsFeatureEnabled(string featureName)
+ {
+ return _flags.IsFeatureEnabled(featureName);
+ }
+
+ public Task GetFeatureValue(string featureName)
+ {
+ return _flags.GetFeatureValue(featureName);
+ }
+}
diff --git a/src/Managing.Bootstrap/ApiBootstrap.cs b/src/Managing.Bootstrap/ApiBootstrap.cs
index 9731d878..9e68c8e0 100644
--- a/src/Managing.Bootstrap/ApiBootstrap.cs
+++ b/src/Managing.Bootstrap/ApiBootstrap.cs
@@ -1,7 +1,8 @@
-using System.Net;
+using System.Net;
using System.Reflection;
using Discord.Commands;
using Discord.WebSocket;
+using Flagsmith;
using FluentValidation;
using Managing.Application;
using Managing.Application.Abstractions;
@@ -395,6 +396,7 @@ public static class ApiBootstrap
services.AddScoped();
services.AddScoped();
services.AddScoped();
+ services.AddScoped();
services.AddTransient, OpenPositionCommandHandler>();
services.AddTransient, OpenSpotPositionCommandHandler>();
services.AddTransient, CloseBacktestFuturesPositionCommandHandler>();
@@ -436,6 +438,24 @@ public static class ApiBootstrap
sp.GetRequiredService>().Value);
services.Configure(configuration.GetSection("Kaigen"));
+ services.Configure(configuration.GetSection("Flagsmith"));
+
+ // Flagsmith - Register client as Singleton
+ services.AddSingleton(sp =>
+ {
+ var settings = sp.GetRequiredService>().Value;
+ if (string.IsNullOrWhiteSpace(settings.ApiKey))
+ {
+ throw new InvalidOperationException("Flagsmith ApiKey is not configured. Please set the Flagsmith:ApiKey configuration value.");
+ }
+
+ if (string.IsNullOrWhiteSpace(settings.ApiUrl))
+ {
+ throw new InvalidOperationException("Flagsmith ApiUrl is not configured. Please set the Flagsmith:ApiUrl configuration value.");
+ }
+
+ return new FlagsmithClient(settings.ApiKey, settings.ApiUrl);
+ });
// Evm
services.AddGbcFeed();