using Dapr; using FastEndpoints; using GB5Shared.GOP.Qualifier; using FastEndpoints.Swagger; using GB5Shared.Connection; using GB5Shared.DaprCache; using GB5Shared.DateConverter; using GB5Shared.DTO.Framework.CommonConfig; using GB5Shared.DTO.Framework.Login; using GB5Shared.EntityHandler; using GB5Shared.EventLogPublish; using GB5Shared.GB5CommonFunction; using GB5Shared.GB5Library.Qualifier; using GB5Shared.GenerateAutoNumber; using GB5Shared.ListQuery; using GB5Shared.PubSub.OutBox; using GB5Shared.QueryExecutor; using GB5Shared.Telemetry; using GB5Shared.Validation; using GoodBooks.PAY.PAYBLL.Factory; using GoodBooks.PAY.PAYBLL.Vault; using GoodBooks.PAY.PAYSL.Services; using Microsoft.AspNetCore.HttpOverrides; using Microsoft.AspNetCore.ResponseCompression; using Microsoft.AspNetCore.Server.Kestrel.Core; using Microsoft.Extensions.Caching.Hybrid; using Newtonsoft.Json; using OpenTelemetry; using OpenTelemetry.Metrics; using OpenTelemetry.Resources; using OpenTelemetry.Trace; using System.Reflection; using System.Text; using VaultSharp; using VaultSharp.V1.AuthMethods.Token; using static GB5Shared.DateConverter.GB5JsonOptions; // ────────────────────────────────────────────────────────────────────────────── // Bootstrap // ────────────────────────────────────────────────────────────────────────────── var builder = WebApplication.CreateBuilder(args); // ── Console Encoding ────────────────────────────────────────────────────────── Console.OutputEncoding = Encoding.UTF8; // ── Port from config — change "AppPort" in appsettings.json to use any port ── var appPort = builder.Configuration.GetValue("AppPort"); builder.WebHost.UseUrls($"http://0.0.0.0:{appPort}"); // ── Dapr & Controllers ──────────────────────────────────────────────────────── builder.Services.AddDaprClient(); builder.Services.AddControllers() .AddDapr() .AddJsonOptions(options => { options.JsonSerializerOptions.PropertyNamingPolicy = null; options.JsonSerializerOptions.DictionaryKeyPolicy = null; options.JsonSerializerOptions.AddGB5Converters(); }); // ── JSON (Minimal API / HttpContext) ────────────────────────────────────────── builder.Services.ConfigureHttpJsonOptions(options => { options.SerializerOptions.PropertyNamingPolicy = null; options.SerializerOptions.DictionaryKeyPolicy = null; options.SerializerOptions.AddGB5Converters(); }); // ── Caching ─────────────────────────────────────────────────────────────────── #pragma warning disable EXTEXP0018 builder.Services.AddHybridCache(options => { options.DefaultEntryOptions = new HybridCacheEntryOptions(); options.DisableCompression = false; }); #pragma warning restore EXTEXP0018 builder.Services.AddMemoryCache(); // used by VaultService (secret TTL cache) builder.Services.AddDistributedMemoryCache(); // fallback distributed cache // ── CORS ────────────────────────────────────────────────────────────────────── // ── HTTP Context ────────────────────────────────────────────────────────────── builder.Services.AddHttpContextAccessor(); // ── Login Header (per-request) ──────────────────────────────────────────────── // âš  FOR HTTP ENDPOINTS ONLY. // Returns empty DTO instead of throwing — background scopes that transitively // touch this registration won't crash. builder.Services.AddScoped(sp => { var httpContext = sp.GetRequiredService().HttpContext; if (httpContext != null && httpContext.Request.Headers.TryGetValue("Login", out var loginHeader)) return JsonConvert.DeserializeObject(loginHeader!)!; return new LoginDTO(); }); // ── Core Framework Services ─────────────────────────────────────────────────── builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(typeof(BaseEntityAppService<>), typeof(BaseEntityAppService<>)); builder.Services.AddScoped(); builder.Services.AddGB5QualifierEngine(); builder.Services.AddScoped(); builder.Services.AddHttpClient(); builder.Services.Configure( builder.Configuration.GetSection("Gb5SystemDTO")); // ── Vault (Singleton — thread-safe IVaultClient + IMemoryCache) ─────────────── builder.Services.AddSingleton(sp => { var authMethod = new TokenAuthMethodInfo(builder.Configuration["Vault:Token"]!); var settings = new VaultClientSettings(builder.Configuration["Vault:Address"]!, authMethod); return new VaultClient(settings); }); builder.Services.AddSingleton(); // ── Payment Gateway Factory (Singleton — stateless) ────────────────────────── builder.Services.AddSingleton(); // Gateway HTTP clients — .NET 9 AddStandardResilienceHandler: retry + circuit breaker builder.Services.AddHttpClient("Razorpay", c => { c.BaseAddress = new Uri("https://api.razorpay.com/v1/"); c.Timeout = TimeSpan.FromSeconds(30); }); builder.Services.AddHttpClient("Stripe", c => { c.BaseAddress = new Uri("https://api.stripe.com/v1/"); c.Timeout = TimeSpan.FromSeconds(30); }); builder.Services.AddHttpClient("Cashfree", c => { c.BaseAddress = new Uri("https://api.cashfree.com/pg/"); c.Timeout = TimeSpan.FromSeconds(30); }); // ── Cross-Module HTTP Clients ───────────────────────────────────────────────── builder.Services.AddHttpClient("SalesModule", c => { c.BaseAddress = new Uri(builder.Configuration["Integration:SalesModuleBaseUrl"] ?? throw new InvalidOperationException("Integration:SalesModuleBaseUrl not configured in appsettings.json")); c.Timeout = TimeSpan.FromSeconds(10); }); builder.Services.AddHttpClient("FinanceModule", c => { c.BaseAddress = new Uri(builder.Configuration["Integration:FinanceModuleBaseUrl"] ?? throw new InvalidOperationException("Integration:FinanceModuleBaseUrl not configured in appsettings.json")); c.Timeout = TimeSpan.FromSeconds(15); }); // ── Kestrel ─────────────────────────────────────────────────────────────────── builder.Services.Configure(options => { options.AllowSynchronousIO = true; }); // ── Response Compression ────────────────────────────────────────────────────── builder.Services.AddResponseCompression(o => { o.EnableForHttps = true; o.Providers.Add(); }); // ── OpenTelemetry ───────────────────────────────────────────────────────────── builder.Services.AddGB5Telemetry(builder.Configuration, "GB5-PAY"); // ── Assembly Scan (BLL + DAL — Scoped, excludes DTOs + records + abstracts) ─── var payBllAssembly = Assembly.Load("PAYBLL"); var payDalAssembly = Assembly.Load("PAYDAL"); builder.Services.Scan(scan => scan .FromAssemblies(payBllAssembly, payDalAssembly) .AddClasses(c => c.Where(t => !t.IsAbstract && !t.IsInterface && t.Namespace != null && !t.Namespace.Contains(".DTO") && !typeof(Exception).IsAssignableFrom(t) && t.GetMethod("$") == null)) // exclude record types .AsImplementedInterfaces() .WithScopedLifetime()); // Note: Singleton services (Vault, GatewayFactory) registered above are NOT // overwritten by Scrutor — it skips already-registered interfaces. builder.Services.AddListInfrastructure(payDalAssembly); // ── FastEndpoints ───────────────────────────────────────────────────────────── var endpointAssemblies = new[] { Assembly.Load("PAYSL"), Assembly.Load("PAYBLL"), Assembly.Load("PAYDAL"), Assembly.Load("GB5Shared") }; builder.Services.AddFastEndpoints(o => { o.Assemblies = endpointAssemblies; }); // ── Swagger ─────────────────────────────────────────────────────────────────── builder.Services.SwaggerDocument(o => { o.DocumentSettings = s => { s.Title = "GB5 PAY API"; s.Version = "v1"; }; o.EnableJWTBearerAuth = false; o.ShortSchemaNames = true; }); // ── Background Services (explicit — not auto-scanned) ──────────────────────── builder.Services.AddHostedService(); builder.Services.AddHostedService(); // Queue Processor + Handlers (CASHBACK, LOYALTY) builder.Services.AddHostedService(); builder.Services.AddScoped(); builder.Services.AddScoped(); // ────────────────────────────────────────────────────────────────────────────── // Build App // ────────────────────────────────────────────────────────────────────────────── var app = builder.Build(); // ── Middleware ──────────────────────────────────────────────────────────────── // 1. Reverse-proxy header forwarding (must be first) app.UseForwardedHeaders(); // 2. CORS (before auth) // 3. Response Compression app.UseResponseCompression(); // 4. Dapr CloudEvents + pub/sub subscription handler app.UseCloudEvents(); // 5. Request tracing + session heartbeat app.UseMiddleware(); app.UseMiddleware(); // 6. FastEndpoints (PascalCase serializer — matches all other GB5 modules) app.UseFastEndpoints(c => { c.Serializer.Options.PropertyNamingPolicy = null; c.Serializer.Options.DictionaryKeyPolicy = null; c.Serializer.Options.AddGB5Converters(); c.Serializer.Options.AddGB5Converters(); }); // 7. MVC Controllers (Dapr pub/sub handlers) app.MapControllers(); // 8. Dapr subscribe handler app.MapSubscribeHandler(); // ── Swagger UI ──────────────────────────────────────────────────────────────── app.UseOpenApi(); app.UseSwaggerUi(o => { o.Path = "/GB5Documentation"; o.DocumentPath = "/swagger/{documentName}/swagger.json"; o.TransformToExternalPath = (internalUiRoute, _) => "/pay" + internalUiRoute; }); // ── Health / Root ────────────────────────────────────────────────────────────── app.MapGet("/", () => "Hello from GB5 PAY in .NET 9 API!"); // ── HTTPS & Authorization ────────────────────────────────────────────────────── app.UseAuthorization(); // ── Dapr Pub/Sub Subscription Handlers ──────────────────────────────────────── // payorder.statuschanged — currently published by PayWebhookBLL; consumed downstream // (Finance module or notification service). No handler in PAY module itself. // ────────────────────────────────────────────────────────────────────────────── // Run // ────────────────────────────────────────────────────────────────────────────── app.Run();