Add flagsmith

This commit is contained in:
2025-12-11 19:22:54 +07:00
parent 65d00c0b9a
commit 35df25915f
6 changed files with 187 additions and 2 deletions

View File

@@ -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");

View File

@@ -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",

View File

@@ -0,0 +1,39 @@
namespace Managing.Application.Abstractions.Services;
/// <summary>
/// Interface for Flagsmith feature flag service
/// </summary>
public interface IFlagsmithService
{
/// <summary>
/// Gets flags for a specific user identity
/// </summary>
/// <param name="identity">The user identity identifier</param>
/// <returns>Flags object for the identity</returns>
Task<IFlagsmithFlags> GetIdentityFlagsAsync(string identity);
/// <summary>
/// Checks if a feature is enabled for a specific identity
/// </summary>
/// <param name="identity">The user identity identifier</param>
/// <param name="featureName">The name of the feature flag</param>
/// <returns>True if the feature is enabled</returns>
Task<bool> IsFeatureEnabledAsync(string identity, string featureName);
/// <summary>
/// Gets the feature value for a specific identity
/// </summary>
/// <param name="identity">The user identity identifier</param>
/// <param name="featureName">The name of the feature flag</param>
/// <returns>The feature value as string</returns>
Task<string?> GetFeatureValueAsync(string identity, string featureName);
}
/// <summary>
/// Wrapper interface for Flagsmith flags to enable testing
/// </summary>
public interface IFlagsmithFlags
{
Task<bool> IsFeatureEnabled(string featureName);
Task<string?> GetFeatureValue(string featureName);
}

View File

@@ -14,6 +14,7 @@
<ItemGroup>
<PackageReference Include="FluentValidation" Version="11.9.1"/>
<PackageReference Include="Flagsmith" Version="5.0.0"/>
<PackageReference Include="GeneticSharp" Version="3.1.4"/>
<PackageReference Include="MediatR" Version="12.2.0"/>
<PackageReference Include="MediatR.Extensions.Microsoft.DependencyInjection" Version="11.1.0"/>

View File

@@ -0,0 +1,122 @@
using Flagsmith;
using Managing.Application.Abstractions.Services;
using Microsoft.Extensions.Logging;
namespace Managing.Application.Shared;
/// <summary>
/// Configuration settings for Flagsmith
/// </summary>
public class FlagsmithSettings
{
public string ApiKey { get; set; } = string.Empty;
/// <summary>
/// API URL for self-hosted Flagsmith instance (required).
/// </summary>
public string ApiUrl { get; set; } = string.Empty;
}
/// <summary>
/// Service for managing feature flags using Flagsmith
/// </summary>
public class FlagsmithService : IFlagsmithService
{
private readonly FlagsmithClient _flagsmithClient;
private readonly ILogger<FlagsmithService> _logger;
public FlagsmithService(FlagsmithClient flagsmithClient, ILogger<FlagsmithService> logger)
{
_flagsmithClient = flagsmithClient ?? throw new ArgumentNullException(nameof(flagsmithClient));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
}
public async Task<IFlagsmithFlags> 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<bool> 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<string?> 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
}
}
}
/// <summary>
/// Wrapper for Flagsmith flags to implement IFlagsmithFlags interface
/// </summary>
internal class FlagsmithFlagsWrapper : IFlagsmithFlags
{
private readonly IFlags _flags;
public FlagsmithFlagsWrapper(IFlags flags)
{
_flags = flags ?? throw new ArgumentNullException(nameof(flags));
}
public Task<bool> IsFeatureEnabled(string featureName)
{
return _flags.IsFeatureEnabled(featureName);
}
public Task<string?> GetFeatureValue(string featureName)
{
return _flags.GetFeatureValue(featureName);
}
}

View File

@@ -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<ISynthPredictionService, SynthPredictionService>();
services.AddScoped<ISynthApiClient, SynthApiClient>();
services.AddScoped<IPricesService, PricesService>();
services.AddScoped<IFlagsmithService, FlagsmithService>();
services.AddTransient<ICommandHandler<OpenPositionRequest, Position>, OpenPositionCommandHandler>();
services.AddTransient<ICommandHandler<OpenSpotPositionRequest, Position>, OpenSpotPositionCommandHandler>();
services.AddTransient<ICommandHandler<CloseBacktestFuturesPositionCommand, Position>, CloseBacktestFuturesPositionCommandHandler>();
@@ -436,6 +438,24 @@ public static class ApiBootstrap
sp.GetRequiredService<IOptions<InfluxDbSettings>>().Value);
services.Configure<KaigenSettings>(configuration.GetSection("Kaigen"));
services.Configure<FlagsmithSettings>(configuration.GetSection("Flagsmith"));
// Flagsmith - Register client as Singleton
services.AddSingleton<FlagsmithClient>(sp =>
{
var settings = sp.GetRequiredService<IOptions<FlagsmithSettings>>().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();