using System.Diagnostics; using Baya.Application.Features.Users.Commands.Create; using Baya.Application.Models.ApiResult; using Baya.Application.Models.Identity; using Baya.Application.ServiceConfiguration; using Baya.Domain.Entities.User; using Baya.Infrastructure.CrossCutting.Logging; using Baya.Infrastructure.CrossCutting.ServiceConfiguration; using Baya.Infrastructure.Identity.Identity.Dtos; using Baya.Infrastructure.Identity.Jwt; using Baya.Infrastructure.Identity.ServiceConfiguration; using Baya.Infrastructure.Monitoring.Configurations; using Baya.Infrastructure.Persistence.ServiceConfiguration; using Baya.SharedKernel.Extensions; using Baya.Web.Api.Configuration; using Baya.Web.Plugins.Grpc; using Baya.WebFramework.Filters; using Baya.WebFramework.Middlewares; using Baya.WebFramework.Routing; using Baya.WebFramework.ServiceConfiguration; using Baya.WebFramework.Swagger; using Mapster; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.ApplicationModels; using Serilog; var builder = WebApplication.CreateBuilder(args); builder.Host.UseSerilog(LoggingConfiguration.ConfigureLogger); var configuration = builder.Configuration; // Fail fast if a load-bearing secret (DB connection, JWE/field-encryption keys) is missing or still a // committed placeholder — before any service reaches for it. Skipped in the "Testing" environment. builder.ValidateRequiredSecrets(); Activity.DefaultIdFormat = ActivityIdFormat.W3C; // HTTPS metadata is required for the token exchange in deployed environments; relaxed for local // Development and the Testing host, which run over plain HTTP. var requireHttpsMetadata = !builder.Environment.IsDevelopment() && !builder.Environment.IsEnvironment("Testing"); builder .ConfigureHealthChecks() .SetupOpenTelemetry(); builder.Services.Configure(configuration.GetSection(nameof(IdentitySettings))); var identitySettings = configuration.GetSection(nameof(IdentitySettings)).Get(); builder.Services.AddControllers(options => { options.Conventions.Add(new RouteTokenTransformerConvention(new SnakeCaseParameterTransformer())); options.Filters.Add(typeof(OkResultAttribute)); options.Filters.Add(typeof(NotFoundResultAttribute)); options.Filters.Add(typeof(ContentResultFilterAttribute)); options.Filters.Add(typeof(ModelStateValidationAttribute)); options.Filters.Add(typeof(BadRequestResultFilterAttribute)); options.Filters.Add(new ProducesResponseTypeAttribute(typeof(ApiResult>>), StatusCodes.Status400BadRequest)); options.Filters.Add(new ProducesResponseTypeAttribute(typeof(ApiResult), StatusCodes.Status401Unauthorized)); options.Filters.Add(new ProducesResponseTypeAttribute(typeof(ApiResult), StatusCodes.Status403Forbidden)); options.Filters.Add(new ProducesResponseTypeAttribute(typeof(ApiResult), StatusCodes.Status500InternalServerError)); }).ConfigureApiBehaviorOptions(options => { options.SuppressModelStateInvalidFilter = true; options.SuppressMapClientErrors = true; }); builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwagger("v1","v1.1"); builder.Services.AddApplicationServices() .RegisterIdentityServices(identitySettings, requireHttpsMetadata) .AddPersistenceServices(configuration) .AddCrossCuttingSeams(configuration) .AddWebFrameworkServices() .AddCorsPolicies(configuration) .AddForwardedHeadersConfiguration(configuration) .AddRateLimitingPolicies(); // Development-only: capture each OTP in-memory so GET /api/v1/dev/last_otp/{phone} can complete a login // without an SMS gateway. refinement-phase-8: the capture bridge runs ONLY while the log-only mock SMS sender is // selected — once a real gateway (Seams:Sms:Provider) ships, the OTP is delivered over the wire and never logged // or captured. Nothing here is wired in any other environment. var smsProvider = configuration["Seams:Sms:Provider"]; var usingMockSms = string.IsNullOrWhiteSpace(smsProvider) || smsProvider.Equals("mock", StringComparison.OrdinalIgnoreCase); if (builder.Environment.IsDevelopment() && usingMockSms) builder.Services.AddDevelopmentOtpCapture(); // The IPaymentCaptureSimulator + bookings/convert path is a Development/Testing affordance (b10's real webhook // confirm supersedes it in production). Re-register the succeeding mock over the production fail-closed stand-in. if (builder.Environment.IsDevelopment() || builder.Environment.IsEnvironment("Testing")) builder.Services.AddDevelopmentPaymentCapture(); builder.Services.RegisterValidatorsAsServices(); builder.Services.AddExceptionHandler(); builder.Services.AddMapster(); TypeAdapterConfig.GlobalSettings.Scan(typeof(UserCreateCommand).Assembly, typeof(GetRolesDto).Assembly); #region Plugin Services Configuration builder.Services.ConfigureGrpcPluginServices(builder.Environment); #endregion var app = builder.Build(); // Deploy-time migration one-shot (refinement-phase-7): `dotnet run -- migrate` (or ` migrate`) applies // EF migrations + the idempotent seeders, then exits. Running DDL as a separate deploy step means normal boots — // especially concurrent multi-instance start-ups — never race on schema, and the runtime login needs no // permanent DDL rights. if (args.Any(a => string.Equals(a, "migrate", StringComparison.OrdinalIgnoreCase))) { await app.ApplyMigrationsAsync(); await app.SeedDefaultUsersAsync(); if (app.Environment.IsDevelopment()) { await app.SeedPaymentGatewaysAsync(); await app.SeedDemoWorldAsync(); await app.SeedDemoLifecycleAsync(); } return; } // Integration tests (WebApplicationFactory, env "Testing") run on in-memory SQLite — the SQL Server // migrations can't apply there; the test factory does EnsureCreated + seeding itself. if (!app.Environment.IsEnvironment("Testing")) { if (app.Environment.IsDevelopment()) { // Local convenience: apply migrations + seed on boot. Development-only: a sandbox payment gateway // (all-zeros merchant id), a demo marketplace (nurses/variants/search rows, customers/patients), and // the lifecycle demo world (bookings/money/reviews/tickets in every state) so the real-path screens // aren't empty. None of it belongs in a deployed DB — all are idempotent. await app.ApplyMigrationsAsync(); await app.SeedDefaultUsersAsync(); await app.SeedPaymentGatewaysAsync(); await app.SeedDemoWorldAsync(); await app.SeedDemoLifecycleAsync(); } else { // Deployed: DDL is the separate `migrate` step above. Boot only *checks* the schema is current (fail fast // on a pending migration) and seeds idempotent runtime data (roles + any configured break-glass admin). await app.EnsureSchemaUpToDateAsync(); await app.SeedDefaultUsersAsync(); } } if (app.Environment.IsDevelopment()) { app.UseDeveloperExceptionPage(); } else app.UseExceptionHandler(_=>{}); // First in the pipeline so the resolved client IP (X-Forwarded-For, from a trusted proxy) is in place // before anything downstream — notably the rate limiter — reads HttpContext.Connection.RemoteIpAddress. app.UseForwardedHeaders(); app.UseSwaggerAndUi(); app.UseRouting(); // After UseRouting and before the rate limiter / authentication so a pre-flight OPTIONS is answered // (and not rejected as 429/401) before the browser sends the real cross-origin request. app.UseCors(CorsServiceExtension.PolicyName); app.UseRateLimiter(); app.UseAuthentication(); app.UseAuthorization(); app.MapControllers(); app.UseMetrics() .UseHealthChecks(); app.ConfigureGrpcPipeline(); await app.RunAsync(); /// Exposes the entry point to WebApplicationFactory<Program>-based integration tests. public partial class Program;