using System.Text.Json; using System.Text.Json.Serialization; using BigRegister.Api.Contracts; using BigRegister.Api.Data; using BigRegister.Domain.Diplomas; using BigRegister.Domain.Intake; using BigRegister.Domain.Registrations; using BigRegister.Domain.Submissions; var builder = WebApplication.CreateBuilder(args); builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(c => c.SwaggerDoc("v1", new() { Title = "BIG-register BFF", Version = "v1" })); builder.Services.AddProblemDetails(); builder.Services.ConfigureHttpJsonOptions(o => { o.SerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.CamelCase; o.SerializerOptions.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull; }); const string SpaCors = "spa"; builder.Services.AddCors(o => o.AddPolicy(SpaCors, p => p.WithOrigins("http://localhost:4200").AllowAnyHeader().AllowAnyMethod())); var app = builder.Build(); app.UseSwagger(); app.UseSwaggerUI(); app.UseCors(SpaCors); // Liveness/readiness for orchestrators (k8s probes, load balancers). No data, no PII. app.MapGet("/health", () => Results.Ok(new { status = "ok" })); app.MapGet("/health/ready", () => Results.Ok(new { status = "ready" })); // Versioned prefix: additive changes (new fields) stay on v1 — the generated client // + FE parse* boundary absorb them; a breaking change introduces /api/v2 alongside. var api = app.MapGroup("/api/v1"); // --- GET: screen-shaped reads. Decisions are computed here, never on the client. --- api.MapGet("/dashboard-view", () => { var reg = SeedData.Registration; var (eligible, reason) = HerregistratieRule.Evaluate(reg, DateOnly.FromDateTime(DateTime.Today)); return new DashboardViewDto(reg.ToDto(), SeedData.Person.ToDto(), new HerregistratieDecisionsDto(eligible, reason)); }); api.MapGet("/notes", () => SeedData.Notes.Select(n => new AantekeningDto(n.Type, n.Omschrijving, n.Datum)).ToList()); // BRP "no address" fallback would be `new BrpAddressDto(false, null)` — the seeded // citizen has one. api.MapGet("/brp/address", () => new BrpAddressDto(true, SeedData.BrpAddress.ToDto())); api.MapGet("/duo/diplomas", () => new DuoLookupDto( SeedData.Diplomas.Select(d => d.ToDto()).ToList(), new ManualDiplomaPolicyDto( DiplomaRules.ManualProfessions(), DiplomaRules.ManualQuestions().Select(q => q.ToDto()).ToList()))); api.MapGet("/intake/policy", () => new IntakePolicyDto(IntakePolicy.ScholingThreshold)); // --- POST: submits. The server is the authority; it re-validates and decides. --- api.MapPost("/registrations", (RegistratieRequest req, HttpContext ctx) => Submit(ctx, "registratie", SubmissionRules.RejectRegistratie(req.DiplomaHerkomst))) .Produces() .ProducesProblem(StatusCodes.Status422UnprocessableEntity); api.MapPost("/herregistraties", (HerregistratieRequest req, HttpContext ctx) => Submit(ctx, "herregistratie", SubmissionRules.RejectZeroUren(req.Uren))) .Produces() .ProducesProblem(StatusCodes.Status422UnprocessableEntity); api.MapPost("/intakes", (IntakeRequest req, HttpContext ctx) => Submit(ctx, "intake", SubmissionRules.RejectZeroUren(req.Uren))) .Produces() .ProducesProblem(StatusCodes.Status422UnprocessableEntity); api.MapPost("/change-requests", (ChangeRequestRequest req, HttpContext ctx) => Submit(ctx, "adreswijziging", SubmissionRules.RejectChangeRequest(req.Straat, req.Postcode))) .Produces() .ProducesProblem(StatusCodes.Status422UnprocessableEntity); app.Run(); // Audit + outcome for a submit, with NO personal data: only kind, outcome, // generated reference and the caller's correlation id (the observability seam — a // real system ships this to structured logging / an audit store). IResult Submit(HttpContext ctx, string kind, string? reject) { var cid = ctx.Request.Headers.TryGetValue("X-Correlation-Id", out var v) && !string.IsNullOrEmpty(v) ? v.ToString() : "none"; if (reject is not null) { app.Logger.LogInformation("submit kind={Kind} outcome=rejected correlationId={Cid}", kind, cid); return Results.Problem(detail: reject, statusCode: StatusCodes.Status422UnprocessableEntity); } var reference = SubmissionRules.NewReference(); app.Logger.LogInformation( "submit kind={Kind} outcome=accepted reference={Reference} correlationId={Cid} at={At:o}", kind, reference, cid, DateTimeOffset.UtcNow); return Results.Ok(new ReferentieResponse(reference)); } // Exposed so the integration tests can spin up the app with WebApplicationFactory. public partial class Program { }