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();