using CMSMicroservice.Infrastructure.Persistence; using CMSMicroservice.Infrastructure.Data.Seeding; using CMSMicroservice.Application.Common.Interfaces; using CMSMicroservice.WebApi.Hubs; using Microsoft.AspNetCore.Builder; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.Logging; using System.Linq; using Serilog.Core; using Serilog; using System.Reflection; using System.Runtime.InteropServices; using Microsoft.OpenApi.Models; using CMSMicroservice.WebApi.Common.Behaviours; using Hangfire; using Hangfire.SqlServer; using Microsoft.AspNetCore.Server.Kestrel.Core; using System.IO; var builder = WebApplication.CreateBuilder(args); // Configure Kestrel to support both HTTP/1.1 and HTTP/2 builder.WebHost.ConfigureKestrel(options => { // Enable both HTTP/1.1 and HTTP/2 for all endpoints options.ConfigureEndpointDefaults(listenOptions => { listenOptions.Protocols = HttpProtocols.Http1AndHttp2; }); }); var levelSwitch = new LoggingLevelSwitch(); // Read Seq configuration from appsettings.json var seqServerUrl = builder.Configuration["Seq:ServerUrl"] ?? "http://seq-svc:5341"; var seqApiKey = builder.Configuration["Seq:ApiKey"]; var logger = new LoggerConfiguration() .WriteTo.Console() //.WriteTo.MSSqlServer(builder.Configuration.GetConnectionString("LogConnection"), // sinkOptions: new MSSqlServerSinkOptions // { // TableName = "LogCMSEvents", // SchemaName = "Log", // AutoCreateSqlTable = true // }) .WriteTo.Seq(seqServerUrl, apiKey: string.IsNullOrEmpty(seqApiKey) ? null : seqApiKey, controlLevelSwitch: levelSwitch) .CreateLogger(); builder.Logging.AddSerilog(logger); #if DEBUG Serilog.Debugging.SelfLog.Enable(msg => Console.WriteLine(msg)); #endif // Additional configuration is required to successfully run gRPC on macOS. // For instructions on how to configure Kestrel and gRPC clients on macOS, visit https://go.microsoft.com/fwlink/?linkid=2099682 // Add services to the container. builder.Services.AddGrpc(options => { options.Interceptors.Add(); options.Interceptors.Add(); options.Interceptors.Add(); options.Interceptors.Add(); //options.Interceptors.Add(); options.EnableDetailedErrors = true; options.MaxReceiveMessageSize = 1000 * 1024 * 1024; // 1 GB options.MaxSendMessageSize = 1000 * 1024 * 1024; // 1 GB }).AddJsonTranscoding(); builder.Services.AddApplicationServices(); builder.Services.AddInfrastructureServices(builder.Configuration); builder.Services.AddPresentationServices(builder.Configuration); builder.Services.AddProtobufServices(); #region Configure Hangfire builder.Services.AddHangfire(config => config .SetDataCompatibilityLevel(CompatibilityLevel.Version_180) .UseSimpleAssemblyNameTypeSerializer() .UseRecommendedSerializerSettings() .UseSqlServerStorage(builder.Configuration["ConnectionStrings:DefaultConnection"])); builder.Services.AddHangfireServer(); #endregion #region Configure Health Checks builder.Services.AddHealthChecks() .AddDbContextCheck("database"); #endregion // Add Controllers for REST APIs builder.Services.AddControllers(); // HttpClient for FMS fallback image download builder.Services.AddHttpClient("FMS", client => { client.Timeout = TimeSpan.FromSeconds(30); client.DefaultRequestHeaders.Add("User-Agent", "FourSat-CMS/1.0"); }); #region Configure Cors builder.Services.AddCors(options => { options.AddPolicy("AllowAll", builder => builder.AllowAnyOrigin().AllowAnyMethod().AllowAnyHeader().WithExposedHeaders("Grpc-Status", "Grpc-Message", "Grpc-Encoding", "Grpc-Accept-Encoding", "validation-errors-text")); }); #endregion builder.Services.AddGrpcSwagger(); builder.Services.AddSwaggerGen(c => { // CMS Core Services Documentation c.SwaggerDoc("cms", new OpenApiInfo { Title = "FourSat CMS - Core Services", Version = "v1", Description = "Core CMS microservice APIs - Internal business logic and data management", Contact = new OpenApiContact { Name = "FourSat Development Team", Email = "dev@foursat.com" } }); // Admin API Documentation (BFF) c.SwaggerDoc("admin", new OpenApiInfo { Title = "FourSat CMS - Admin BFF", Version = "v1", Description = "Admin Backend-for-Frontend API - User Management, Products, Commission, Network, Reports", Contact = new OpenApiContact { Name = "FourSat Development Team", Email = "dev@foursat.com" } }); // Customer API Documentation (BFF) c.SwaggerDoc("customer", new OpenApiInfo { Title = "FourSat CMS - Customer BFF", Version = "v1", Description = "Customer Backend-for-Frontend API - Profile, Shop, Commission, Network Statistics", Contact = new OpenApiContact { Name = "FourSat Development Team", Email = "dev@foursat.com" } }); // Unified API Documentation (All endpoints) c.SwaggerDoc("unified", new OpenApiInfo { Title = "FourSat CMS - Unified API", Version = "v1", Description = "Complete API Documentation - All Core, Admin & Customer endpoints in one place" }); c.CustomSchemaIds(type => type.ToString()); // Resolve conflicting actions for Swagger c.ResolveConflictingActions(apiDescriptions => apiDescriptions.First()); // Include XML documentation for gRPC services var xmlFile = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml"; var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile); if (File.Exists(xmlPath)) { c.IncludeXmlComments(xmlPath); } // Security Definition c.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme { In = ParameterLocation.Header, Description = "Please insert JWT with Bearer into field (Format: Bearer {token})", Name = "Authorization", Type = SecuritySchemeType.ApiKey, Scheme = "Bearer" }); c.AddSecurityRequirement(new OpenApiSecurityRequirement { { new OpenApiSecurityScheme { Reference = new OpenApiReference { Type = ReferenceType.SecurityScheme, Id = "Bearer" } }, new string[] { } } }); // Group endpoints by functionality c.TagActionsBy(api => { var serviceName = api.ActionDescriptor.RouteValues?["controller"] ?? GetGrpcServiceName(api.RelativePath); // Define service categories for better organization return serviceName switch { var name when name.Contains("User") => new[] { "👤 User Management" }, var name when name.Contains("Product") => new[] { "📦 Product Catalog" }, var name when name.Contains("Category") => new[] { "🗂️ Categories" }, var name when name.Contains("Commission") => new[] { "💰 Commission & Earnings" }, var name when name.Contains("Network") => new[] { "🌐 Network & Binary Tree" }, var name when name.Contains("Club") => new[] { "🏆 Club Management" }, var name when name.Contains("Discount") => new[] { "🏷️ Discount Shop" }, var name when name.Contains("Order") => new[] { "🛒 Orders & Shopping" }, var name when name.Contains("Payment") => new[] { "💳 Payments & Transactions" }, var name when name.Contains("Inventory") => new[] { "📋 Inventory Management" }, var name when name.Contains("Health") => new[] { "🔧 System Health" }, var name when name.Contains("Configuration") => new[] { "⚙️ Configuration" }, var name when name.Contains("Admin") => new[] { "👑 Administration" }, _ => new[] { $"📋 {serviceName}" } }; }); c.DocInclusionPredicate((docName, apiDesc) => { // Get service name from both REST controllers and gRPC services var controllerName = apiDesc.ActionDescriptor.RouteValues?["controller"] ?? ""; var grpcServiceName = GetGrpcServiceName(apiDesc.RelativePath); var serviceName = !string.IsNullOrEmpty(controllerName) ? controllerName : grpcServiceName; return docName switch { "cms" => IsCoreService(serviceName), "admin" => IsAdminService(serviceName), "customer" => IsCustomerService(serviceName), "unified" => true, // Show all endpoints _ => true }; }); }); // Helper functions for gRPC service name extraction and categorization static string GetGrpcServiceName(string? relativePath) { if (string.IsNullOrEmpty(relativePath)) return ""; var segments = relativePath.Split('/', StringSplitOptions.RemoveEmptyEntries); return segments.Length > 0 ? segments[0] : ""; } static bool IsCoreService(string serviceName) { var coreServices = new[] { "Health", "Configuration", "OtpToken", "Contract", "AppVersion" }; return coreServices.Any(core => serviceName.Contains(core, StringComparison.OrdinalIgnoreCase)); } static bool IsAdminService(string serviceName) { var adminServices = new[] { "Admin", "Role", "UserRole", "ManualPayment", "Inventory", "Tag", "ProductTag", "ProductGalleries", "ProductImages", "DiscountOrder", "FactorDetails", "Products", "ProductCategory", "Category", "DiscountCategory", "DiscountProduct" }; return adminServices.Any(admin => serviceName.Contains(admin, StringComparison.OrdinalIgnoreCase)); } static bool IsCustomerService(string serviceName) { var customerServices = new[] { "User", "UserAddress", "Commission", "NetworkMembership", "ClubMembership", "UserOrder", "UserWallet", "UserCarts", "DiscountShoppingCart", "City", "Package", "Transactions", "UserContract", "UserWalletHistory" }; return customerServices.Any(customer => serviceName.Contains(customer, StringComparison.OrdinalIgnoreCase)) && !IsAdminService(serviceName); // Exclude admin services } var app = builder.Build(); // Configure the HTTP request pipeline. if (app.Environment.IsDevelopment()) { app.UseDeveloperExceptionPage(); app.UseMigrationsEndPoint(); // Initialise and seed database using (var scope = app.Services.CreateScope()) { var initialiser = scope.ServiceProvider.GetRequiredService(); await initialiser.InitialiseAsync(); await initialiser.SeedAsync(); // Run Migration: ParentId → NetworkParentId (فقط یکبار اجرا می‌شود) var migrationLogger = scope.ServiceProvider.GetRequiredService>(); var dbContext = scope.ServiceProvider.GetRequiredService(); var migrationSeeder = new NetworkParentIdMigrationSeeder(dbContext, migrationLogger); await migrationSeeder.SeedAsync(); // Seed WeekDefinitions (هفته‌ها) var weekSeederLogger = scope.ServiceProvider.GetRequiredService>(); var weekSeeder = new WeekDefinitionSeeder(dbContext, weekSeederLogger); await weekSeeder.SeedAsync(); } } else { // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts. app.UseHsts(); } // Load WeekDefinition cache (برای هر دو محیط Development و Production) var weekDefinitionRepository = app.Services.GetRequiredService(); await weekDefinitionRepository.ReloadCacheAsync(); app.UseRouting(); app.UseCors("AllowAll"); app.UseAuthentication(); app.UseAuthorization(); // Enable static files for Swagger custom CSS app.UseStaticFiles(); // Map Health Check endpoints app.MapHealthChecks("/health"); app.MapHealthChecks("/health/ready", new Microsoft.AspNetCore.Diagnostics.HealthChecks.HealthCheckOptions { Predicate = check => check.Tags.Contains("ready") }); app.MapHealthChecks("/health/live", new Microsoft.AspNetCore.Diagnostics.HealthChecks.HealthCheckOptions { Predicate = _ => false }); app.MapControllers(); app.UseGrpcWeb(new GrpcWebOptions { DefaultEnabled = true }); // Configure the HTTP request pipeline. // Map SignalR Hub for token notifications app.MapHub("/hubs/token-notification"); app.MapHub("/hubs/token-relay"); // Alias for FrontOffice backward compatibility app.ConfigureGrpcEndpoints(Assembly.GetExecutingAssembly(), endpoints => { // endpoints.MapGrpcService(); }); app.MapGet("/", () => "Communication with gRPC endpoints must be made through a gRPC client. To learn how to create a client, visit: https://go.microsoft.com/fwlink/?linkid=2086909"); app.UseSwagger(); app.UseSwaggerUI(c => { // CMS Core Services c.SwaggerEndpoint("/swagger/cms/swagger.json", "🔧 CMS Core Services"); // Admin BFF c.SwaggerEndpoint("/swagger/admin/swagger.json", "👑 Admin BFF"); // Customer BFF c.SwaggerEndpoint("/swagger/customer/swagger.json", "👥 Customer BFF"); // Unified API (All endpoints) c.SwaggerEndpoint("/swagger/unified/swagger.json", "🌐 Unified API (All)"); // UI Customization c.DocumentTitle = "FourSat CMS API Documentation"; c.RoutePrefix = "swagger"; // Available at /swagger // Default to CMS core view c.DefaultModelExpandDepth(2); c.DefaultModelsExpandDepth(-1); c.DocExpansion(Swashbuckle.AspNetCore.SwaggerUI.DocExpansion.None); c.EnableFilter(); c.EnableDeepLinking(); c.EnableValidator(); // Custom CSS c.InjectStylesheet("/swagger-ui/custom.css"); }); // Configure Hangfire Dashboard app.UseHangfireDashboard("/hangfire", new Hangfire.DashboardOptions { // TODO: برای production از Authorization filter استفاده کنید Authorization = Array.Empty() }); // Configure Recurring Jobs using (var scope = app.Services.CreateScope()) { var recurringJobManager = scope.ServiceProvider.GetRequiredService(); // Weekly Commission Calculation: Every Sunday at 00:05 (configurable) var weeklyCommissionEnabled = app.Configuration.GetValue("BackgroundJobs:WeeklyCommissionCalculation:Enabled", true); var weeklyCommissionCron = app.Configuration.GetValue("BackgroundJobs:WeeklyCommissionCalculation:CronExpression", "5 0 * * 0"); if (weeklyCommissionEnabled) { recurringJobManager.AddOrUpdate( recurringJobId: "weekly-commission-calculation", methodCall: job => job.ExecuteAsync(null, CancellationToken.None), cronExpression: weeklyCommissionCron, options: new RecurringJobOptions { TimeZone = TimeZoneInfo.Local }); app.Logger.LogInformation("✅ Hangfire recurring job 'weekly-commission-calculation' registered (Cron: {Cron})", weeklyCommissionCron); } else { recurringJobManager.RemoveIfExists("weekly-commission-calculation"); app.Logger.LogInformation("⚠️ Hangfire recurring job 'weekly-commission-calculation' is DISABLED in configuration"); } // Daya Loan Check: Every 15 minutes CMSMicroservice.WebApi.Workers.DayaLoanCheckWorker.Schedule(recurringJobManager); app.Logger.LogInformation("✅ Hangfire recurring job 'daya-loan-check' registered (Cron: */15 * * * * - Every 15 minutes)"); // Chatika Account Activation: Every 5 minutes recurringJobManager.AddOrUpdate( recurringJobId: "chatika-account-activation", methodCall: job => job.ExecuteAsync(CancellationToken.None), cronExpression: "*/5 * * * *", options: new RecurringJobOptions { TimeZone = TimeZoneInfo.Local }); app.Logger.LogInformation("✅ Hangfire recurring job 'chatika-account-activation' registered (Cron: */5 * * * * - Every 5 minutes)"); } app.Run();