using System.Text.Json.Serialization; using ERPCore.Infra.Auth; using ERPCore.Infra.Auth.AuthHex; using ERPCore.Infra.Gl; using ERPCore.Infra.Persistence; using ERPCore.Infra.Storage; using ERPCore.Infra.UoW; using ERPCore.Repositories; using ERPCore.Repositories.Interfaces; using ERPCore.Services; using ERPCore.Services.Interfaces; using ERPCore.Services.Auth; using ERPCore.Services.Hrm; using ERPCore.Services.Production; using ERPCore.Services.Stock; using ERPCore.System.Errors; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.HttpOverrides; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Diagnostics; using Microsoft.OpenApi; using Npgsql; using Serilog; var builder = WebApplication.CreateBuilder(args); // Fail fast on missing deployment config. Outside Development these arrive as environment // variables (docs/40-DEPLOYMENT.md §5); a blank value would otherwise surface later as an // unrelated crash (e.g. `new Uri("")` for AuthHex:BaseUrl) instead of naming the key. if (!builder.Environment.IsDevelopment()) { string[] requiredKeys = [ "ConnectionStrings:DefaultConnection", "AuthHex:BaseUrl", "GeneralLedgerService:BaseUrl", "GeneralLedgerService:ApiKey", "Auth:RsaPublicKeyXml", ]; var missing = requiredKeys.Where(k => string.IsNullOrWhiteSpace(builder.Configuration[k])).ToArray(); if (missing.Length > 0) throw new InvalidOperationException( $"Missing required configuration for environment '{builder.Environment.EnvironmentName}': " + $"{string.Join(", ", missing)}. Set them as environment variables (':' becomes '__', " + "e.g. ConnectionStrings__DefaultConnection)."); } // Serilog (file sink) — reads levels from configuration, plus a rolling daily file. The path is // configurable so deployed instances log outside their release folder (LogFile:Path). builder.Host.UseSerilog((ctx, cfg) => cfg .ReadFrom.Configuration(ctx.Configuration) .WriteTo.File(ctx.Configuration["LogFile:Path"] ?? "logs/erpcore-.log", rollingInterval: RollingInterval.Day)); // Controllers + JSON: serialize enums as their string names (docs/11 §8, camelCase). builder.Services.AddControllers() .AddJsonOptions(o => o.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter())); // EF Core + PostgreSQL builder.Services.AddDbContext(o => { o.UseNpgsql(builder.Configuration.GetConnectionString("DefaultConnection")); o.ConfigureWarnings(w => w.Ignore(RelationalEventId.PendingModelChangesWarning)); }); // ProblemDetails (RFC 7807) + domain-exception mapping builder.Services.AddProblemDetails(); builder.Services.AddExceptionHandler(); // Auth: validate external AuthHex RS256 tokens + ERP door policy (docs/10 A.4) builder.Services.AddErpJwtAuth(builder.Configuration); // AuthController proxy → AuthHex (docs/11 §2.0) builder.Services.AddHttpClient(c => { // Blank counts as missing: the base appsettings.json ships these keys empty on purpose. var baseUrl = builder.Configuration["AuthHex:BaseUrl"]; if (string.IsNullOrWhiteSpace(baseUrl)) throw new InvalidOperationException("AuthHex:BaseUrl is not configured (set it in appsettings.Development.json locally)."); c.BaseAddress = new Uri(baseUrl); }); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); // General Ledger service proxy → external GL microservice (docs/12-GENERAL-LEDGER-INTEGRATION.md) builder.Services.AddHttpClient(c => { var baseUrl = builder.Configuration["GeneralLedgerService:BaseUrl"]; if (string.IsNullOrWhiteSpace(baseUrl)) throw new InvalidOperationException("GeneralLedgerService:BaseUrl is not configured (set it in appsettings.Development.json locally)."); c.BaseAddress = new Uri(baseUrl); }); builder.Services.AddScoped(); // Current-user (audit actor). AuthHex has no sub/nameid → a claims transformation // JIT-provisions a local shadow user and injects the local `int` id as `nameid`. builder.Services.AddHttpContextAccessor(); builder.Services.AddScoped(); builder.Services.AddScoped(); // Unit of work + generic repository base builder.Services.AddScoped(); builder.Services.AddScoped(typeof(IRepository<>), typeof(Repository<>)); // Master-data services (docs/11 §2) 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(); builder.Services.AddScoped(); builder.Services.AddSingleton(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); // RBAC / sidebar: Role (shadow of AuthHex) + Permission assignment + user management builder.Services.AddScoped(); builder.Services.AddScoped(); // Cross-cutting + procurement services (docs/11 §3) builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); // Stock core + goods receipt (docs/11 §4–5) builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); // Purchase invoices (vendor invoices consolidating Confirmed GRNs) builder.Services.AddScoped(); builder.Services.AddScoped(); // Sales (Phase 1) 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(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); // Stock transactions + reference data (docs/11 §5–6) builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); // Cross-cutting: audit trail + GL-ready journal read access (docs/11 §6 / FR-X-02, FR-STK-13) builder.Services.AddScoped(); // Dashboard aggregate stats (cross-domain read: stock, GRN, procurement) builder.Services.AddScoped(); // HRM (docs/13-BACKEND-HRM-API.md): org masters, employee core, staff documents builder.Services.AddSingleton(); 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(); // HRM: Leave (docs/13-BACKEND-HRM-API.md §5) — LeaveType/LeaveBalance built before // LeaveRequest since approval increments balances; Attendance depends on LeaveRequest. builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); // HRM: Attendance (docs/13-BACKEND-HRM-API.md §4) builder.Services.AddScoped(); builder.Services.AddScoped(); // HRM: Payroll (docs/13-BACKEND-HRM-API.md §6) — masters/settings before the // calculation service, which composes them; PayrollRunService orchestrates last. builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); // HRM: Reports (docs/13-BACKEND-HRM-API.md §6) — read-only, no new entities builder.Services.AddScoped(); // Manufacturing / Production Lines (docs/30-BACKEND-PHASE2.md Part A). Templates stand // alone; runs consume FifoCostingService for all stock movement. ProductionGraphValidator // is deliberately unregistered — it is a pure static algorithm, not an injected service. builder.Services.AddScoped(); builder.Services.AddScoped(); // Health checks (EF Core DB) builder.Services.AddHealthChecks().AddDbContextCheck(); // Swagger / OpenAPI (Swashbuckle v10 → OpenAPI 3.1) builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(o => { o.SwaggerDoc("v1", new OpenApiInfo { Title = "ERPCore API", Version = "v1" }); o.CustomSchemaIds(t => t.FullName!.Replace("+", ".")); }); var app = builder.Build(); // Seed configurable reference data (reason codes) idempotently at startup. using (var scope = app.Services.CreateScope()) { var db = scope.ServiceProvider.GetRequiredService(); // await EnsureMigrationBaselineAsync(db); await db.Database.MigrateAsync(); try { // Demo rows (sample catalog, stock, customers, sales docs) default on only in Development; // the test deployment opts in with Seed__DemoData=true, release never does. var includeDemoData = app.Configuration.GetValue("Seed:DemoData", app.Environment.IsDevelopment()); await DataSeeder.SeedAsync(db, includeDemoData); } catch (Exception ex) { throw new InvalidOperationException("Database migration succeeded, but startup seeding failed.", ex); } } // Behind the co-located Apache reverse proxy (docs/40-DEPLOYMENT.md): take the client IP and // scheme from X-Forwarded-*. Only loopback proxies are trusted by default, which is exactly it. app.UseForwardedHeaders(new ForwardedHeadersOptions { ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto }); app.UseSerilogRequestLogging(); // Off in Production only. The test deployment (Staging) keeps it; it is still not public there // because Apache proxies only /api/ — reach it through an SSH tunnel to the backend port. if (!app.Environment.IsProduction()) { app.UseSwagger(); app.UseSwaggerUI(); } app.UseExceptionHandler(); // -> DomainExceptionHandler / ProblemDetails app.UseAuthentication(); app.UseAuthorization(); app.MapControllers(); app.MapHealthChecks("/health"); app.Run();