using System.Net.Http.Json; namespace Bff.Api; /// What the self-service submit returns to the portal (the domain's registration id + status). public sealed record SubmitAccepted(string RegistrationId, string Status); /// The caller's current open registration, for resuming the self-service portal after a /// refresh (S-26): the reference (registration id) + its status. public sealed record CurrentRegistration(string RegistrationId, string Status); /// A projection row as the projection-api serves it. Bsn/NaamPlaceholder are /// read but never surfaced by the openbaar endpoint (public-safe filtering, ADR-0010/S-09). /// Reference is the public-safe citizen reference (the zaak identificatie, #78). public sealed record ProjectionEntry(string Id, string Status, string? Reference, string? Bsn, string? NaamPlaceholder); /// A public-safe openbaar register row — only non-sensitive fields leave the BFF. public sealed record OpenbaarEntry(string Id, string Status, string? Reference); /// A behandelaar's werkbak row: a registration awaiting beoordeling, with the bsn + status a /// behandelaar sees (staff view — reached only behind medewerker/behandelaar authorization, S-12c). public sealed record WerkbakItem(string RegistrationId, string Bsn, string Status); /// Port to the Domain Service (§8.3: the BFF is the portals' only backend; it fans out). public interface IDomainClient { Task SubmitRegistrationAsync(string bsn, CancellationToken ct = default); /// The caller's current open registration (resume after refresh, S-26), or null /// when they have none in flight. Owner-scoped by . Task GetCurrentRegistrationAsync(string bsn, CancellationToken ct = default); /// Withdraw the caller's own registration ("trek aanvraag in"). Owner-scoped by /// . Returns false when the domain reports the registration is /// unknown or not the caller's (404), so the BFF can relay a 404 rather than a 500. Task WithdrawRegistrationAsync(string registrationId, string bsn, CancellationToken ct = default); /// Provide (upload) the diploma the caller's own registration is waiting for ("documenten /// aanleveren"). The file is carried base64-encoded. Owner-scoped by . Returns /// false when the domain reports the registration is unknown or not the caller's (404). Task ProvideDocumentsAsync( string registrationId, string bsn, string contentBase64, string? fileName, string? contentType, CancellationToken ct = default); /// The behandelaar's werkbak — registrations awaiting beoordeling. Task> GetWerkbakAsync(CancellationToken ct = default); /// Apply a behandelaar's decision (goedkeuren/afwijzen) to a registration. Task DecideAsync(string registrationId, string besluit, CancellationToken ct = default); } /// Port to the read projection. public interface IProjectionClient { Task> GetRegisterAsync(CancellationToken ct = default); } /// A published zaaktype as the beheer catalogus viewer shows it (S-15a): the business /// Identificatie + human Omschrijving. The ZGW URL the ACL also returns is dropped — an /// internal reference, not shown in the portal. public sealed record BeheerZaaktype(string Identificatie, string Omschrijving); /// Port to the ACL for read-only catalogus queries (beheer portal, S-15a). The BFF reaches the /// ACL directly for this read: the catalogus isn't a domain concern, and the ACL is the only code /// allowed to read the ZGW Catalogi API (§8.1, ADR-0025). public interface IAclClient { Task> GetZaaktypenAsync(CancellationToken ct = default); } /// Calls the Domain Service's POST /registrations. public sealed class DomainClient(HttpClient http) : IDomainClient { public async Task SubmitRegistrationAsync(string bsn, CancellationToken ct = default) { using var response = await http.PostAsJsonAsync("registrations", new { bsn }, ct); response.EnsureSuccessStatusCode(); var dto = await response.Content.ReadFromJsonAsync(ct) ?? throw new InvalidOperationException("The Domain Service returned an empty registration response."); return new SubmitAccepted(dto.RegistrationId, dto.Status); } public async Task GetCurrentRegistrationAsync(string bsn, CancellationToken ct = default) { using var response = await http.GetAsync($"registrations/current?bsn={Uri.EscapeDataString(bsn)}", ct); // The domain 404s when the citizen has no open registration — that's "none", not an error. if (response.StatusCode == System.Net.HttpStatusCode.NotFound) return null; response.EnsureSuccessStatusCode(); var dto = await response.Content.ReadFromJsonAsync(ct) ?? throw new InvalidOperationException("The Domain Service returned an empty registration response."); return new CurrentRegistration(dto.RegistrationId, dto.Status); } public async Task WithdrawRegistrationAsync(string registrationId, string bsn, CancellationToken ct = default) { using var response = await http.PostAsJsonAsync( $"registrations/{registrationId}/withdraw", new { bsn }, ct); // The domain 404s an unknown or not-owned registration; relay that rather than fail hard. if (response.StatusCode == System.Net.HttpStatusCode.NotFound) return false; response.EnsureSuccessStatusCode(); return true; } public async Task ProvideDocumentsAsync( string registrationId, string bsn, string contentBase64, string? fileName, string? contentType, CancellationToken ct = default) { using var response = await http.PostAsJsonAsync( $"registrations/{registrationId}/documents", new { bsn, contentBase64, fileName, contentType }, ct); // The domain 404s an unknown or not-owned registration; relay that rather than fail hard. if (response.StatusCode == System.Net.HttpStatusCode.NotFound) return false; response.EnsureSuccessStatusCode(); return true; } public async Task> GetWerkbakAsync(CancellationToken ct = default) => await http.GetFromJsonAsync>("behandel/werkbak", ct) ?? []; public async Task DecideAsync(string registrationId, string besluit, CancellationToken ct = default) { using var response = await http.PostAsJsonAsync( $"registrations/{registrationId}/decide", new { besluit }, ct); response.EnsureSuccessStatusCode(); } private sealed record DomainResponse(string RegistrationId, string Status, string? ZaakUrl); } /// Calls the projection-api's GET /register. public sealed class ProjectionClient(HttpClient http) : IProjectionClient { public async Task> GetRegisterAsync(CancellationToken ct = default) => await http.GetFromJsonAsync>("register", ct) ?? []; } /// Calls the ACL's GET /catalogi/zaaktypen (S-15a). The ACL also returns each zaaktype's /// ZGW URL; deserializing into keeps only the public-safe fields. public sealed class AclClient(HttpClient http) : IAclClient { public async Task> GetZaaktypenAsync(CancellationToken ct = default) => await http.GetFromJsonAsync>("catalogi/zaaktypen", ct) ?? []; }