refactor(backend): delete dead legacy endpoints, make domain types unions (WP-72 + WP-73)
Two work packages in one commit because both edit Program.cs and splitting
them would leave a commit that does not build.
WP-72 — deletes POST /api/v1/intakes and /herregistraties. Both were dead
from the UI (the wizard submits via /applications/{id}/submit) and strictly
less capable: they minted a bare reference and wrote no Aanvraag, made no
ZGW call, and did no document-ownership check. The shared Submit(...) helper
survives — /registrations and /change-requests still use it. WP-69 hardened
/intakes with a 400 last session; removing the surface is the stronger fix,
and WP-69's /applications/{id}/submit enforcement is untouched.
WP-73 — RegistrationStatus becomes an abstract record with three sealed
variants behind a private base ctor, so only Geregistreerd carries a
herregistratie deadline and reden is required on Geschorst/Doorgehaald
(matching the FE union, which was already right). HerregistratieRule
.IsStatusConsistent and its test are deleted: the type now guarantees what
the runtime check was for, and the test could no longer construct the
illegal state it existed to catch.
Aanvraag splits into a Concept | Submitted | Decided union with the EF row
demoted to AanvraagEntity behind a two-way mapper. Submitted carries a
non-null Referentie and SubmittedAt, and Decided.Afgewezen/MeerInfoGevraagd
require a Toelichting — so the five Referentie! null-forgiving derefs in
StatusAt are gone, not merely suppressed. IZaakSource.CreateZaak narrows to
Aanvraag.Submitted, removing the same class of deref in both zaak sources.
Draft is now cleared on submit rather than lingering: ApplicationStore's
doc-comment claimed "Concept only" but Submit never cleared it. Verified
nothing reads a submitted aanvraag's draft (draft-sync's applyResume only
resumes unsubmitted wizards), so the comment is now true instead of
aspirational.
No migration, no schema change, no wire change — RegistrationStatusDto and
the application DTOs are byte-identical, confirmed against a live swagger.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -58,8 +58,6 @@ cd backend && dotnet test # rule unit tests + endpoint integration tests
|
|||||||
| GET | `/api/duo/diplomas` | diplomas with derived profession + applicable policy questions, + manual fallback |
|
| GET | `/api/duo/diplomas` | diplomas with derived profession + applicable policy questions, + manual fallback |
|
||||||
| GET | `/api/intake/policy` | scholing threshold (config value) |
|
| GET | `/api/intake/policy` | scholing threshold (config value) |
|
||||||
| POST | `/api/registrations` | submit registration → reference, or 422 (manual diploma) |
|
| POST | `/api/registrations` | submit registration → reference, or 422 (manual diploma) |
|
||||||
| POST | `/api/herregistraties` | submit re-registration → reference, or 422 (0 hours) |
|
|
||||||
| POST | `/api/intakes` | submit intake → reference, or 422 (0 hours) / 400 (incomplete scholing answer) |
|
|
||||||
|
|
||||||
Rejections use **ProblemDetails (RFC 7807)** with status **422**. Every request
|
Rejections use **ProblemDetails (RFC 7807)** with status **422**. Every request
|
||||||
carries an `X-Correlation-Id` (set by the FE fetch adapter); the backend echoes it
|
carries an `X-Correlation-Id` (set by the FE fetch adapter); the backend echoes it
|
||||||
|
|||||||
@@ -76,12 +76,6 @@ public sealed record DocumentRefDto(string CategoryId, string Channel, string? D
|
|||||||
// stay on the client). ponytail: a real submit would carry the full application.
|
// stay on the client). ponytail: a real submit would carry the full application.
|
||||||
public sealed record RegistratieRequest(string DiplomaHerkomst, IReadOnlyList<DocumentRefDto>? Documents = null);
|
public sealed record RegistratieRequest(string DiplomaHerkomst, IReadOnlyList<DocumentRefDto>? Documents = null);
|
||||||
|
|
||||||
// AanvullendeScholing/ScholingPunten (WP-69): the wizard's scholing answer, re-validated
|
|
||||||
// server-side as the authority by IntakePolicy.RejectIncompleteScholing. Named
|
|
||||||
// ScholingPunten (not Punten) — the sibling SubmitApplicationRequest is shared by all three
|
|
||||||
// wizard types and the herregistratie wizard has its own unrelated `punten`.
|
|
||||||
public sealed record IntakeRequest(int Uren, bool? AanvullendeScholing = null, int? ScholingPunten = null);
|
|
||||||
public sealed record HerregistratieRequest(int Uren, IReadOnlyList<DocumentRefDto>? Documents = null);
|
|
||||||
public sealed record ChangeRequestRequest(string Telefoon);
|
public sealed record ChangeRequestRequest(string Telefoon);
|
||||||
|
|
||||||
// Authz/PII-reveal audit row (WP-41) — data-minimised, no PII (see AuthzAuditEntry).
|
// Authz/PII-reveal audit row (WP-41) — data-minimised, no PII (see AuthzAuditEntry).
|
||||||
@@ -125,8 +119,8 @@ public sealed record DraftSyncRequest(
|
|||||||
IReadOnlyList<string>? DocumentIds = null);
|
IReadOnlyList<string>? DocumentIds = null);
|
||||||
|
|
||||||
// Submit carries only the fields the server re-validates per wizard type.
|
// Submit carries only the fields the server re-validates per wizard type.
|
||||||
// AanvullendeScholing/ScholingPunten (WP-69) — see IntakeRequest; intake-typed aanvragen
|
// AanvullendeScholing/ScholingPunten (WP-69) — intake-typed aanvragen only (gated by
|
||||||
// only (gated by IntakePolicy.RejectIncompleteScholing's caller), null for the others.
|
// IntakePolicy.RejectIncompleteScholing's caller), null for the others.
|
||||||
public sealed record SubmitApplicationRequest(
|
public sealed record SubmitApplicationRequest(
|
||||||
string? DiplomaHerkomst = null, int? Uren = null,
|
string? DiplomaHerkomst = null, int? Uren = null,
|
||||||
IReadOnlyList<DocumentRefDto>? Documents = null,
|
IReadOnlyList<DocumentRefDto>? Documents = null,
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
using System.Text.Json;
|
||||||
using BigRegister.Api.Data;
|
using BigRegister.Api.Data;
|
||||||
using BigRegister.Domain.Applications;
|
using BigRegister.Domain.Applications;
|
||||||
using BigRegister.Domain.Diplomas;
|
using BigRegister.Domain.Diplomas;
|
||||||
@@ -12,12 +13,13 @@ public static class Mappers
|
|||||||
{
|
{
|
||||||
private static string D(DateOnly d) => d.ToString("yyyy-MM-dd");
|
private static string D(DateOnly d) => d.ToString("yyyy-MM-dd");
|
||||||
|
|
||||||
public static RegistrationStatusDto ToDto(this RegistrationStatus s) => new(
|
public static RegistrationStatusDto ToDto(this RegistrationStatus s) => s switch
|
||||||
Tag: s.Tag.ToString(),
|
{
|
||||||
HerregistratieDatum: s.HerregistratieDatum is { } h ? D(h) : null,
|
RegistrationStatus.Geregistreerd g => new(s.Tag.ToString(), HerregistratieDatum: D(g.HerregistratieDatum)),
|
||||||
GeschorstTot: s.GeschorstTot is { } g ? D(g) : null,
|
RegistrationStatus.Geschorst g => new(s.Tag.ToString(), GeschorstTot: D(g.GeschorstTot), Reden: g.Reden),
|
||||||
Reden: s.Reden,
|
RegistrationStatus.Doorgehaald d => new(s.Tag.ToString(), DoorgehaaldOp: D(d.DoorgehaaldOp), Reden: d.Reden),
|
||||||
DoorgehaaldOp: s.DoorgehaaldOp is { } x ? D(x) : null);
|
_ => throw new ArgumentOutOfRangeException(nameof(s), s, "Unknown RegistrationStatus variant"),
|
||||||
|
};
|
||||||
|
|
||||||
public static RegistrationDto ToDto(this Registration r) => new(
|
public static RegistrationDto ToDto(this Registration r) => new(
|
||||||
r.BigNummer, r.Naam, r.Beroep, D(r.Registratiedatum), D(r.Geboortedatum), r.Status.ToDto());
|
r.BigNummer, r.Naam, r.Beroep, D(r.Registratiedatum), D(r.Geboortedatum), r.Status.ToDto());
|
||||||
@@ -45,19 +47,33 @@ public static class Mappers
|
|||||||
public static AanvraagStatusDto ToDto(this AanvraagStatus s) => new(
|
public static AanvraagStatusDto ToDto(this AanvraagStatus s) => new(
|
||||||
s.Tag?.ToString() ?? "Concept", s.StepIndex, s.StepCount, s.Referentie, s.Manual, s.Reden);
|
s.Tag?.ToString() ?? "Concept", s.StepIndex, s.StepCount, s.Referentie, s.Manual, s.Reden);
|
||||||
|
|
||||||
// Aanvraag status is COMPUTED ON READ (see Aanvraag.StatusAt) — this is now a one-line
|
// Aanvraag status is COMPUTED ON READ (see the StatusAt extension, Data/AanvraagMapper.cs) —
|
||||||
// projection of that domain method onto the wire DTO (WP-68 F3).
|
// this is now a one-line projection of that onto the wire DTO (WP-68 F3, WP-73).
|
||||||
public static AanvraagStatusDto ToStatusDto(this Aanvraag a, DateTimeOffset now) => a.StatusAt(now).ToDto();
|
public static AanvraagStatusDto ToStatusDto(this Aanvraag a, DateTimeOffset now) => a.StatusAt(now).ToDto();
|
||||||
|
|
||||||
|
/// <summary>SubmittedAt only exists once Submitted/Decided (WP-73) — null for a Concept,
|
||||||
|
/// same as the wire DTO's own nullable field.</summary>
|
||||||
|
private static string? SubmittedAtOf(Aanvraag a) => a switch
|
||||||
|
{
|
||||||
|
Aanvraag.Concept => null,
|
||||||
|
Aanvraag.Submitted s => s.SubmittedAt.ToString("o"),
|
||||||
|
Aanvraag.Decided d => d.SubmittedAt.ToString("o"),
|
||||||
|
_ => null,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// <summary>Draft only exists pre-submission (WP-73) — null once Submitted/Decided (nothing
|
||||||
|
/// reads it past that point; see <c>AanvraagMapper.ApplyTo</c>'s Submitted branch).</summary>
|
||||||
|
private static JsonElement? DraftOf(Aanvraag a) => a is Aanvraag.Concept c ? c.Draft : null;
|
||||||
|
|
||||||
public static ApplicationSummaryDto ToSummaryDto(this Aanvraag a, DateTimeOffset now) => new(
|
public static ApplicationSummaryDto ToSummaryDto(this Aanvraag a, DateTimeOffset now) => new(
|
||||||
a.Id, a.Type, a.ToStatusDto(now), a.DocumentIds,
|
a.Id, a.Type, a.ToStatusDto(now), a.DocumentIds,
|
||||||
a.CreatedAt.ToString("o"), a.UpdatedAt.ToString("o"), a.SubmittedAt?.ToString("o"));
|
a.CreatedAt.ToString("o"), a.UpdatedAt.ToString("o"), SubmittedAtOf(a));
|
||||||
|
|
||||||
/// Admin summary — same shape plus the owner (WP-36; the user-facing list leaves Owner null).
|
/// Admin summary — same shape plus the owner (WP-36; the user-facing list leaves Owner null).
|
||||||
public static ApplicationSummaryDto ToAdminSummaryDto(this Aanvraag a, DateTimeOffset now) =>
|
public static ApplicationSummaryDto ToAdminSummaryDto(this Aanvraag a, DateTimeOffset now) =>
|
||||||
a.ToSummaryDto(now) with { Owner = a.Owner };
|
a.ToSummaryDto(now) with { Owner = a.Owner };
|
||||||
|
|
||||||
public static ApplicationDetailDto ToDetailDto(this Aanvraag a, DateTimeOffset now) => new(
|
public static ApplicationDetailDto ToDetailDto(this Aanvraag a, DateTimeOffset now) => new(
|
||||||
a.Id, a.Type, a.ToStatusDto(now), a.Draft, a.DocumentIds,
|
a.Id, a.Type, a.ToStatusDto(now), DraftOf(a), a.DocumentIds,
|
||||||
a.CreatedAt.ToString("o"), a.UpdatedAt.ToString("o"), a.SubmittedAt?.ToString("o"));
|
a.CreatedAt.ToString("o"), a.UpdatedAt.ToString("o"), SubmittedAtOf(a));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,206 @@
|
|||||||
|
using BigRegister.Domain.Applications;
|
||||||
|
|
||||||
|
namespace BigRegister.Api.Data;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The two-way seam between <see cref="AanvraagEntity"/> (the EF-mapped persistence row —
|
||||||
|
/// mutable, no invariants of its own, exactly the shape SQLite needs) and <see cref="Aanvraag"/>
|
||||||
|
/// (the closed Concept/Submitted/Decided domain union, WP-73). <see cref="ToDomain"/> is the
|
||||||
|
/// read half: it reconstructs whichever variant a row's stored fields describe, going through
|
||||||
|
/// that variant's own constructor/required members, so a row that doesn't actually describe a
|
||||||
|
/// legal aanvraag throws here rather than downstream. <see cref="ApplyTo"/>/<see cref="ToEntity"/>
|
||||||
|
/// are the write half, used by <see cref="ApplicationStore"/>'s writers (and test fixtures, e.g.
|
||||||
|
/// <c>Builders/AanvraagBuilder.cs</c>) to flush a freshly-constructed domain value onto a row
|
||||||
|
/// before <c>SaveChanges</c>.
|
||||||
|
/// </summary>
|
||||||
|
public static class AanvraagMapper
|
||||||
|
{
|
||||||
|
public static Aanvraag ToDomain(this AanvraagEntity row)
|
||||||
|
{
|
||||||
|
if (!row.Submitted)
|
||||||
|
return new Aanvraag.Concept(row.StepIndex, row.StepCount)
|
||||||
|
{
|
||||||
|
Id = row.Id,
|
||||||
|
Type = row.Type,
|
||||||
|
Owner = row.Owner,
|
||||||
|
DocumentIds = row.DocumentIds,
|
||||||
|
CreatedAt = row.CreatedAt,
|
||||||
|
UpdatedAt = row.UpdatedAt,
|
||||||
|
ZaakUrl = row.ZaakUrl,
|
||||||
|
ZgwError = row.ZgwError,
|
||||||
|
Draft = row.Draft,
|
||||||
|
};
|
||||||
|
|
||||||
|
var referentie = row.Referentie
|
||||||
|
?? throw new InvalidOperationException($"Submitted aanvraag {row.Id} has no Referentie.");
|
||||||
|
var submittedAt = row.SubmittedAt
|
||||||
|
?? throw new InvalidOperationException($"Submitted aanvraag {row.Id} has no SubmittedAt.");
|
||||||
|
|
||||||
|
// Reden wins over BesluitStatus — matches the pre-WP-73 StatusAt's own priority. In
|
||||||
|
// practice a row never carries both (BeoordelingRules.CanDecide already refuses a besluit
|
||||||
|
// once Reden's auto-reject makes the projected status Afgewezen), but if it somehow did,
|
||||||
|
// the auto-reject at submission time is authoritative.
|
||||||
|
if (row.Reden is null && row.BesluitStatus is { } besluit)
|
||||||
|
return besluit switch
|
||||||
|
{
|
||||||
|
Besluit.Goedkeuren => new Aanvraag.Decided.Goedgekeurd
|
||||||
|
{
|
||||||
|
Id = row.Id,
|
||||||
|
Type = row.Type,
|
||||||
|
Owner = row.Owner,
|
||||||
|
DocumentIds = row.DocumentIds,
|
||||||
|
CreatedAt = row.CreatedAt,
|
||||||
|
UpdatedAt = row.UpdatedAt,
|
||||||
|
ZaakUrl = row.ZaakUrl,
|
||||||
|
ZgwError = row.ZgwError,
|
||||||
|
Referentie = referentie,
|
||||||
|
SubmittedAt = submittedAt,
|
||||||
|
},
|
||||||
|
Besluit.Afwijzen => new Aanvraag.Decided.Afgewezen
|
||||||
|
{
|
||||||
|
Id = row.Id,
|
||||||
|
Type = row.Type,
|
||||||
|
Owner = row.Owner,
|
||||||
|
DocumentIds = row.DocumentIds,
|
||||||
|
CreatedAt = row.CreatedAt,
|
||||||
|
UpdatedAt = row.UpdatedAt,
|
||||||
|
ZaakUrl = row.ZaakUrl,
|
||||||
|
ZgwError = row.ZgwError,
|
||||||
|
Referentie = referentie,
|
||||||
|
SubmittedAt = submittedAt,
|
||||||
|
Toelichting = row.BesluitToelichting
|
||||||
|
?? throw new InvalidOperationException($"Afgewezen aanvraag {row.Id} has no toelichting."),
|
||||||
|
},
|
||||||
|
Besluit.MeerInfoOpvragen => new Aanvraag.Decided.MeerInfoGevraagd
|
||||||
|
{
|
||||||
|
Id = row.Id,
|
||||||
|
Type = row.Type,
|
||||||
|
Owner = row.Owner,
|
||||||
|
DocumentIds = row.DocumentIds,
|
||||||
|
CreatedAt = row.CreatedAt,
|
||||||
|
UpdatedAt = row.UpdatedAt,
|
||||||
|
ZaakUrl = row.ZaakUrl,
|
||||||
|
ZgwError = row.ZgwError,
|
||||||
|
Referentie = referentie,
|
||||||
|
SubmittedAt = submittedAt,
|
||||||
|
Toelichting = row.BesluitToelichting
|
||||||
|
?? throw new InvalidOperationException($"MeerInfoGevraagd aanvraag {row.Id} has no toelichting."),
|
||||||
|
},
|
||||||
|
_ => throw new InvalidOperationException($"Unknown besluit {besluit}."),
|
||||||
|
};
|
||||||
|
|
||||||
|
return new Aanvraag.Submitted
|
||||||
|
{
|
||||||
|
Id = row.Id,
|
||||||
|
Type = row.Type,
|
||||||
|
Owner = row.Owner,
|
||||||
|
DocumentIds = row.DocumentIds,
|
||||||
|
CreatedAt = row.CreatedAt,
|
||||||
|
UpdatedAt = row.UpdatedAt,
|
||||||
|
ZaakUrl = row.ZaakUrl,
|
||||||
|
ZgwError = row.ZgwError,
|
||||||
|
Referentie = referentie,
|
||||||
|
SubmittedAt = submittedAt,
|
||||||
|
AutoApprovable = row.AutoApprovable,
|
||||||
|
Reden = row.Reden,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Flushes a domain value onto an already-tracked row — everything but identity
|
||||||
|
/// (Id/Type/Owner) and CreatedAt, which never change once a row exists. Used by
|
||||||
|
/// <see cref="ApplicationStore"/>'s SyncDraft/Submit/RecordBesluit, each of which already
|
||||||
|
/// <c>Find()</c>ed the row this applies to.</summary>
|
||||||
|
public static void ApplyTo(this Aanvraag a, AanvraagEntity row)
|
||||||
|
{
|
||||||
|
row.DocumentIds = a.DocumentIds.ToList();
|
||||||
|
row.UpdatedAt = a.UpdatedAt;
|
||||||
|
row.ZaakUrl = a.ZaakUrl;
|
||||||
|
row.ZgwError = a.ZgwError;
|
||||||
|
|
||||||
|
switch (a)
|
||||||
|
{
|
||||||
|
case Aanvraag.Concept c:
|
||||||
|
row.Draft = c.Draft;
|
||||||
|
row.StepIndex = c.StepIndex;
|
||||||
|
row.StepCount = c.StepCount;
|
||||||
|
row.Submitted = false;
|
||||||
|
row.Referentie = null;
|
||||||
|
row.SubmittedAt = null;
|
||||||
|
row.AutoApprovable = false;
|
||||||
|
row.Reden = null;
|
||||||
|
row.BesluitStatus = null;
|
||||||
|
row.BesluitToelichting = null;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case Aanvraag.Submitted s:
|
||||||
|
// Submitted ⇒ !Draft (WP-73's Draft decision) — nothing reads a submitted aanvraag's
|
||||||
|
// draft (registratie/application/draft-sync.ts only ever resumes a still-Concept
|
||||||
|
// wizard), so this is now actually true rather than the aspirational doc-comment it
|
||||||
|
// used to be.
|
||||||
|
row.Draft = null;
|
||||||
|
row.Submitted = true;
|
||||||
|
row.Referentie = s.Referentie;
|
||||||
|
row.SubmittedAt = s.SubmittedAt;
|
||||||
|
row.AutoApprovable = s.AutoApprovable;
|
||||||
|
row.Reden = s.Reden;
|
||||||
|
row.BesluitStatus = null;
|
||||||
|
row.BesluitToelichting = null;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case Aanvraag.Decided d:
|
||||||
|
row.Draft = null;
|
||||||
|
row.Submitted = true;
|
||||||
|
row.Referentie = d.Referentie;
|
||||||
|
row.SubmittedAt = d.SubmittedAt;
|
||||||
|
// AutoApprovable/Reden are left as whatever the row already carries from its earlier
|
||||||
|
// Submitted stage: Decided doesn't model them (StatusAt never consults them once a
|
||||||
|
// besluit is recorded — its Decided branches are checked first), and a fresh row built
|
||||||
|
// straight from a Decided fixture with no prior Submitted stage (see ToEntity) simply
|
||||||
|
// keeps their type defaults (false/null), which is equally harmless for the same reason.
|
||||||
|
row.BesluitStatus = d switch
|
||||||
|
{
|
||||||
|
Aanvraag.Decided.Goedgekeurd => Besluit.Goedkeuren,
|
||||||
|
Aanvraag.Decided.Afgewezen => Besluit.Afwijzen,
|
||||||
|
Aanvraag.Decided.MeerInfoGevraagd => Besluit.MeerInfoOpvragen,
|
||||||
|
_ => throw new InvalidOperationException($"Unknown Decided variant {d.GetType().Name}."),
|
||||||
|
};
|
||||||
|
row.BesluitToelichting = d switch
|
||||||
|
{
|
||||||
|
Aanvraag.Decided.Goedgekeurd => null,
|
||||||
|
Aanvraag.Decided.Afgewezen af => af.Toelichting,
|
||||||
|
Aanvraag.Decided.MeerInfoGevraagd m => m.Toelichting,
|
||||||
|
_ => throw new InvalidOperationException($"Unknown Decided variant {d.GetType().Name}."),
|
||||||
|
};
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>A brand-new row for a domain value that has no existing row yet — test fixtures'
|
||||||
|
/// <c>db.Applications.Add(...)</c> (see <c>Acceptance/BesluitLifecycleTests.cs</c>,
|
||||||
|
/// <c>Acceptance/IntakeSubmissionTests.cs</c>), and (indirectly, via <see cref="ApplyTo"/>)
|
||||||
|
/// <see cref="ApplicationStore.CreateConcept"/>'s very first insert.</summary>
|
||||||
|
public static AanvraagEntity ToEntity(this Aanvraag a)
|
||||||
|
{
|
||||||
|
var row = new AanvraagEntity { Id = a.Id, Type = a.Type, Owner = a.Owner, CreatedAt = a.CreatedAt };
|
||||||
|
a.ApplyTo(row);
|
||||||
|
return row;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>The status at a point in time (WP-68 F3, WP-73) — pattern matching over the
|
||||||
|
/// closed <see cref="Aanvraag"/> union, replacing the null-forgiving derefs the old flat
|
||||||
|
/// mutable row needed (Referentie/SubmittedAt are simply non-nullable on Submitted/Decided
|
||||||
|
/// now, so there's nothing left to force). A recorded decision wins over the auto-approve
|
||||||
|
/// computation, matching the pre-WP-73 priority.</summary>
|
||||||
|
public static AanvraagStatus StatusAt(this Aanvraag a, DateTimeOffset now) => a switch
|
||||||
|
{
|
||||||
|
Aanvraag.Concept c => AanvraagStatus.Concept(c.StepIndex, c.StepCount),
|
||||||
|
Aanvraag.Submitted { Reden: { } reden } s => AanvraagStatus.Afgewezen(s.Referentie, reden),
|
||||||
|
Aanvraag.Decided.Goedgekeurd g => AanvraagStatus.Goedgekeurd(g.Referentie),
|
||||||
|
Aanvraag.Decided.Afgewezen af => AanvraagStatus.Afgewezen(af.Referentie, af.Toelichting),
|
||||||
|
Aanvraag.Decided.MeerInfoGevraagd m => AanvraagStatus.MeerInfoGevraagd(m.Referentie, m.Toelichting),
|
||||||
|
Aanvraag.Submitted s when s.AutoApprovable && now > s.SubmittedAt + ApplicationStore.ProcessingWindow =>
|
||||||
|
AanvraagStatus.Goedgekeurd(s.Referentie),
|
||||||
|
Aanvraag.Submitted s => AanvraagStatus.InBehandeling(s.Referentie, manual: !s.AutoApprovable),
|
||||||
|
_ => throw new InvalidOperationException($"Unknown Aanvraag variant {a.GetType().Name}."),
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -7,7 +7,7 @@ namespace BigRegister.Api.Data;
|
|||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// EF Core/SQLite persistence for the three stores that used to be static
|
/// EF Core/SQLite persistence for the three stores that used to be static
|
||||||
/// in-memory dictionaries (WP-22): <see cref="Aanvraag"/>, <see cref="StoredDocument"/>
|
/// in-memory dictionaries (WP-22): <see cref="AanvraagEntity"/>, <see cref="StoredDocument"/>
|
||||||
/// + <see cref="AuditEntry"/>, and <see cref="BriefEntity"/>. Opaque nested shapes
|
/// + <see cref="AuditEntry"/>, and <see cref="BriefEntity"/>. Opaque nested shapes
|
||||||
/// (a wizard's draft snapshot, a brief's sections/placeholders/status) are stored as
|
/// (a wizard's draft snapshot, a brief's sections/placeholders/status) are stored as
|
||||||
/// JSON text columns rather than redesigned into relational tables — the backend
|
/// JSON text columns rather than redesigned into relational tables — the backend
|
||||||
@@ -21,7 +21,7 @@ public sealed class AppDbContext(DbContextOptions<AppDbContext> options) : DbCon
|
|||||||
public DbSet<AuditEntry> AuditEntries => Set<AuditEntry>();
|
public DbSet<AuditEntry> AuditEntries => Set<AuditEntry>();
|
||||||
public DbSet<AuthzAuditEntry> AuthzAudit => Set<AuthzAuditEntry>();
|
public DbSet<AuthzAuditEntry> AuthzAudit => Set<AuthzAuditEntry>();
|
||||||
public DbSet<FeatureFlagEntity> FeatureFlags => Set<FeatureFlagEntity>();
|
public DbSet<FeatureFlagEntity> FeatureFlags => Set<FeatureFlagEntity>();
|
||||||
public DbSet<Aanvraag> Applications => Set<Aanvraag>();
|
public DbSet<AanvraagEntity> Applications => Set<AanvraagEntity>();
|
||||||
public DbSet<BriefEntity> Briefs => Set<BriefEntity>();
|
public DbSet<BriefEntity> Briefs => Set<BriefEntity>();
|
||||||
public DbSet<OrgTemplateEntity> OrgTemplates => Set<OrgTemplateEntity>();
|
public DbSet<OrgTemplateEntity> OrgTemplates => Set<OrgTemplateEntity>();
|
||||||
|
|
||||||
@@ -43,7 +43,7 @@ public sealed class AppDbContext(DbContextOptions<AppDbContext> options) : DbCon
|
|||||||
|
|
||||||
modelBuilder.Entity<FeatureFlagEntity>().HasKey(f => f.Key);
|
modelBuilder.Entity<FeatureFlagEntity>().HasKey(f => f.Key);
|
||||||
|
|
||||||
modelBuilder.Entity<Aanvraag>(e =>
|
modelBuilder.Entity<AanvraagEntity>(e =>
|
||||||
{
|
{
|
||||||
e.HasKey(a => a.Id);
|
e.HasKey(a => a.Id);
|
||||||
e.Property(a => a.Draft).HasConversion(DraftConverter);
|
e.Property(a => a.Draft).HasConversion(DraftConverter);
|
||||||
|
|||||||
@@ -6,13 +6,16 @@ using BigRegister.Domain.Submissions;
|
|||||||
namespace BigRegister.Api.Data;
|
namespace BigRegister.Api.Data;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// An application (aanvraag) — the system of record the dashboard reads. A wizard
|
/// The EF-mapped persistence row for an application (aanvraag) — WP-73 demoted this to
|
||||||
/// creates one as a Concept on its first step, syncs its draft snapshot per step,
|
/// exactly that: a flat, mutable bag with no invariants of its own (SQLite needs precisely
|
||||||
/// then submits it into the Concept → In behandeling → Goedgekeurd/Afgewezen
|
/// this shape), never read or written directly outside this file. Everywhere else, production
|
||||||
/// lifecycle (ADR-0002). Status is COMPUTED ON READ (see <see cref="StatusAt"/>) so
|
/// code reads and writes <see cref="Aanvraag"/> (the closed Concept/Submitted/Decided domain
|
||||||
/// auto-approval is purely a function of stored timestamps — no timers, no jobs.
|
/// union, <c>Domain/Applications/Aanvraag.cs</c>) — <see cref="AanvraagMapper"/>'s
|
||||||
|
/// <c>ToDomain</c>/<c>ApplyTo</c>/<c>ToEntity</c> is the two-way seam between the two. Status is
|
||||||
|
/// COMPUTED ON READ (see the <c>StatusAt</c> extension below) so auto-approval is purely a
|
||||||
|
/// function of stored timestamps — no timers, no jobs.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class Aanvraag
|
public sealed class AanvraagEntity
|
||||||
{
|
{
|
||||||
public required string Id { get; init; }
|
public required string Id { get; init; }
|
||||||
public required string Type { get; init; } // registratie | herregistratie | intake
|
public required string Type { get; init; } // registratie | herregistratie | intake
|
||||||
@@ -53,26 +56,6 @@ public sealed class Aanvraag
|
|||||||
/// <summary>The behandelaar's toelichting — required for Afwijzen/MeerInfoOpvragen (becomes
|
/// <summary>The behandelaar's toelichting — required for Afwijzen/MeerInfoOpvragen (becomes
|
||||||
/// the published status's Reden), optional for Goedkeuren.</summary>
|
/// the published status's Reden), optional for Goedkeuren.</summary>
|
||||||
public string? BesluitToelichting { get; set; }
|
public string? BesluitToelichting { get; set; }
|
||||||
|
|
||||||
/// <summary>The status at a point in time (WP-68 F3) — moved here from
|
|
||||||
/// <c>Contracts.Mappers.ToStatusDto</c>, which is now a one-line projection of this. A
|
|
||||||
/// recorded decision wins over the auto-approve computation below.</summary>
|
|
||||||
public AanvraagStatus StatusAt(DateTimeOffset now)
|
|
||||||
{
|
|
||||||
if (!Submitted) return AanvraagStatus.Concept(StepIndex, StepCount);
|
|
||||||
if (Reden is not null) return AanvraagStatus.Afgewezen(Referentie!, Reden);
|
|
||||||
if (BesluitStatus is { } besluit)
|
|
||||||
return besluit switch
|
|
||||||
{
|
|
||||||
Besluit.Goedkeuren => AanvraagStatus.Goedgekeurd(Referentie!),
|
|
||||||
Besluit.Afwijzen => AanvraagStatus.Afgewezen(Referentie!, BesluitToelichting),
|
|
||||||
Besluit.MeerInfoOpvragen => AanvraagStatus.MeerInfoGevraagd(Referentie!, BesluitToelichting),
|
|
||||||
_ => throw new InvalidOperationException($"Unknown besluit {besluit}"),
|
|
||||||
};
|
|
||||||
if (AutoApprovable && now > SubmittedAt!.Value + ApplicationStore.ProcessingWindow)
|
|
||||||
return AanvraagStatus.Goedgekeurd(Referentie!);
|
|
||||||
return AanvraagStatus.InBehandeling(Referentie!, manual: !AutoApprovable);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -90,10 +73,13 @@ public static class ApplicationStore
|
|||||||
|
|
||||||
/// Create a Concept for <paramref name="owner"/> — UNLESS one of this
|
/// Create a Concept for <paramref name="owner"/> — UNLESS one of this
|
||||||
/// <paramref name="type"/> already exists unsubmitted. WP-35: at most one Concept per
|
/// <paramref name="type"/> already exists unsubmitted. WP-35: at most one Concept per
|
||||||
/// type is a server-enforced invariant (the FE's draft-sync only guards it best-effort).
|
/// type is a server-enforced invariant (the FE's draft-sync only guards it best-effort;
|
||||||
/// Race-free: the existence check and the insert share the single write gate. Returns
|
/// this stays procedural here — it's an AGGREGATE-SET rule over every (Owner, Type), not
|
||||||
/// null when a duplicate would be created (the caller maps that to 409 Conflict).
|
/// something a single Aanvraag value's own shape could ever encode, and there is no unique
|
||||||
public static Aanvraag? CreateConcept(string type, string owner)
|
/// index in the schema either). Race-free: the existence check and the insert share the
|
||||||
|
/// single write gate. Returns null when a duplicate would be created (the caller maps that
|
||||||
|
/// to 409 Conflict).
|
||||||
|
public static Aanvraag.Concept? CreateConcept(string type, string owner)
|
||||||
{
|
{
|
||||||
var now = DateTimeOffset.UtcNow;
|
var now = DateTimeOffset.UtcNow;
|
||||||
lock (_gate)
|
lock (_gate)
|
||||||
@@ -101,10 +87,18 @@ public static class ApplicationStore
|
|||||||
using var db = Db.Create();
|
using var db = Db.Create();
|
||||||
if (db.Applications.Any(a => a.Owner == owner && a.Type == type && !a.Submitted))
|
if (db.Applications.Any(a => a.Owner == owner && a.Type == type && !a.Submitted))
|
||||||
return null;
|
return null;
|
||||||
var a = new Aanvraag { Id = Guid.NewGuid().ToString(), Type = type, Owner = owner, CreatedAt = now, UpdatedAt = now };
|
var concept = new Aanvraag.Concept(stepIndex: 0, stepCount: 0)
|
||||||
db.Applications.Add(a);
|
{
|
||||||
|
Id = Guid.NewGuid().ToString(),
|
||||||
|
Type = type,
|
||||||
|
Owner = owner,
|
||||||
|
DocumentIds = Array.Empty<string>(),
|
||||||
|
CreatedAt = now,
|
||||||
|
UpdatedAt = now,
|
||||||
|
};
|
||||||
|
db.Applications.Add(concept.ToEntity());
|
||||||
db.SaveChanges();
|
db.SaveChanges();
|
||||||
return a;
|
return concept;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -114,7 +108,7 @@ public static class ApplicationStore
|
|||||||
{
|
{
|
||||||
using var db = Db.Create();
|
using var db = Db.Create();
|
||||||
var a = db.Applications.Find(id);
|
var a = db.Applications.Find(id);
|
||||||
return a is not null && a.Owner == owner ? a : null;
|
return a is not null && a.Owner == owner ? a.ToDomain() : null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -123,7 +117,7 @@ public static class ApplicationStore
|
|||||||
lock (_gate)
|
lock (_gate)
|
||||||
{
|
{
|
||||||
using var db = Db.Create();
|
using var db = Db.Create();
|
||||||
return db.Applications.Where(a => a.Owner == owner).ToList();
|
return db.Applications.Where(a => a.Owner == owner).ToList().Select(a => a.ToDomain()).ToList();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -134,7 +128,7 @@ public static class ApplicationStore
|
|||||||
lock (_gate)
|
lock (_gate)
|
||||||
{
|
{
|
||||||
using var db = Db.Create();
|
using var db = Db.Create();
|
||||||
return db.Applications.Find(id);
|
return db.Applications.Find(id)?.ToDomain();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -149,7 +143,7 @@ public static class ApplicationStore
|
|||||||
lock (_gate)
|
lock (_gate)
|
||||||
{
|
{
|
||||||
using var db = Db.Create();
|
using var db = Db.Create();
|
||||||
return db.Applications.FirstOrDefault(a => a.Referentie == referentie);
|
return db.Applications.FirstOrDefault(a => a.Referentie == referentie)?.ToDomain();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -162,23 +156,34 @@ public static class ApplicationStore
|
|||||||
using var db = Db.Create();
|
using var db = Db.Create();
|
||||||
// Order client-side: SQLite can't ORDER BY a DateTimeOffset (same constraint the
|
// Order client-side: SQLite can't ORDER BY a DateTimeOffset (same constraint the
|
||||||
// rest of the store sidesteps by never sorting in the query).
|
// rest of the store sidesteps by never sorting in the query).
|
||||||
return db.Applications.ToList().OrderByDescending(a => a.UpdatedAt).ToList();
|
return db.Applications.ToList().OrderByDescending(a => a.UpdatedAt).Select(a => a.ToDomain()).ToList();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Draft sync: idempotent upsert of the wizard snapshot. Only a Concept is mutable.
|
/// Draft sync: idempotent upsert of the wizard snapshot. Only a Concept is mutable — the
|
||||||
|
/// domain reconstruction below is what enforces "0 <= StepIndex <= StepCount"
|
||||||
|
/// (<see cref="Aanvraag.Concept"/>'s own constructor throws on an out-of-range pair instead
|
||||||
|
/// of this silently writing one onto the row, the way the pre-WP-73 code did).
|
||||||
public static bool SyncDraft(string id, string owner, JsonElement draft, int stepIndex, int stepCount, IReadOnlyList<string>? documentIds)
|
public static bool SyncDraft(string id, string owner, JsonElement draft, int stepIndex, int stepCount, IReadOnlyList<string>? documentIds)
|
||||||
{
|
{
|
||||||
lock (_gate)
|
lock (_gate)
|
||||||
{
|
{
|
||||||
using var db = Db.Create();
|
using var db = Db.Create();
|
||||||
var a = db.Applications.Find(id);
|
var row = db.Applications.Find(id);
|
||||||
if (a is null || a.Owner != owner || a.Submitted) return false;
|
if (row is null || row.Owner != owner || row.Submitted) return false;
|
||||||
a.Draft = draft.Clone(); // detach from the request's JsonDocument (disposed after the call)
|
var concept = new Aanvraag.Concept(stepIndex, stepCount)
|
||||||
a.StepIndex = stepIndex;
|
{
|
||||||
a.StepCount = stepCount;
|
Id = row.Id,
|
||||||
if (documentIds is not null) a.DocumentIds = documentIds.ToList();
|
Type = row.Type,
|
||||||
a.UpdatedAt = DateTimeOffset.UtcNow;
|
Owner = row.Owner,
|
||||||
|
DocumentIds = documentIds ?? row.DocumentIds,
|
||||||
|
CreatedAt = row.CreatedAt,
|
||||||
|
UpdatedAt = DateTimeOffset.UtcNow,
|
||||||
|
ZaakUrl = row.ZaakUrl,
|
||||||
|
ZgwError = row.ZgwError,
|
||||||
|
Draft = draft.Clone(), // detach from the request's JsonDocument (disposed after the call)
|
||||||
|
};
|
||||||
|
concept.ApplyTo(row);
|
||||||
db.SaveChanges();
|
db.SaveChanges();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -226,23 +231,39 @@ public static class ApplicationStore
|
|||||||
|
|
||||||
/// Submit transition. reject != null → Afgewezen; else accepted (In behandeling,
|
/// Submit transition. reject != null → Afgewezen; else accepted (In behandeling,
|
||||||
/// auto-advancing to Goedgekeurd after the window when autoApprovable). Returns null
|
/// auto-advancing to Goedgekeurd after the window when autoApprovable). Returns null
|
||||||
/// if the aanvraag is gone or already submitted (idempotency guard).
|
/// if the aanvraag is gone or already submitted (idempotency guard). WP-73: the returned
|
||||||
public static Aanvraag? Submit(string id, string owner, string? reject, bool autoApprovable, IReadOnlyList<string>? documentIds)
|
/// <see cref="Aanvraag.Submitted"/> is constructed with a non-null Referentie/SubmittedAt by
|
||||||
|
/// its own required members — there is no longer a null-forgiving deref anywhere down the
|
||||||
|
/// line reading them back (<c>StatusAt</c>, <c>IZaakSource.CreateZaak</c>). Submitting also
|
||||||
|
/// clears the row's Draft (<see cref="AanvraagMapper.ApplyTo"/>'s Submitted branch) — nothing
|
||||||
|
/// reads a submitted aanvraag's draft (the FE only ever resumes a still-Concept wizard), so
|
||||||
|
/// the doc-comment's old "Draft is Concept only" claim is now actually true, not aspirational.
|
||||||
|
public static Aanvraag.Submitted? Submit(string id, string owner, string? reject, bool autoApprovable, IReadOnlyList<string>? documentIds)
|
||||||
{
|
{
|
||||||
lock (_gate)
|
lock (_gate)
|
||||||
{
|
{
|
||||||
using var db = Db.Create();
|
using var db = Db.Create();
|
||||||
var a = db.Applications.Find(id);
|
var row = db.Applications.Find(id);
|
||||||
if (a is null || a.Owner != owner || a.Submitted) return null;
|
if (row is null || row.Owner != owner || row.Submitted) return null;
|
||||||
a.Submitted = true;
|
var now = DateTimeOffset.UtcNow;
|
||||||
a.SubmittedAt = DateTimeOffset.UtcNow;
|
var submitted = new Aanvraag.Submitted
|
||||||
a.UpdatedAt = a.SubmittedAt.Value;
|
{
|
||||||
a.Referentie = SubmissionRules.NewReference();
|
Id = row.Id,
|
||||||
a.AutoApprovable = autoApprovable;
|
Type = row.Type,
|
||||||
a.Reden = reject;
|
Owner = row.Owner,
|
||||||
if (documentIds is not null) a.DocumentIds = documentIds.ToList();
|
DocumentIds = documentIds ?? row.DocumentIds,
|
||||||
|
CreatedAt = row.CreatedAt,
|
||||||
|
UpdatedAt = now,
|
||||||
|
ZaakUrl = row.ZaakUrl,
|
||||||
|
ZgwError = row.ZgwError,
|
||||||
|
Referentie = SubmissionRules.NewReference(),
|
||||||
|
SubmittedAt = now,
|
||||||
|
AutoApprovable = autoApprovable,
|
||||||
|
Reden = reject,
|
||||||
|
};
|
||||||
|
submitted.ApplyTo(row);
|
||||||
db.SaveChanges();
|
db.SaveChanges();
|
||||||
return a;
|
return submitted;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -282,22 +303,83 @@ public static class ApplicationStore
|
|||||||
/// now runs INSIDE this lock, against a status read fresh under the lock, rather than in
|
/// now runs INSIDE this lock, against a status read fresh under the lock, rather than in
|
||||||
/// the endpoint beforehand — two concurrent besluiten used to both pass the endpoint's
|
/// the endpoint beforehand — two concurrent besluiten used to both pass the endpoint's
|
||||||
/// check before either wrote, letting the second silently overwrite a terminal decision.
|
/// check before either wrote, letting the second silently overwrite a terminal decision.
|
||||||
|
/// WP-73: <see cref="Aanvraag.Decided.Afgewezen"/>/<see cref="Aanvraag.Decided.MeerInfoGevraagd"/>
|
||||||
|
/// require a non-null Toelichting by their own shape — the endpoint already 400s a missing
|
||||||
|
/// one (<c>BeoordelingRules.RequiresToelichting</c>), and this is the defense-in-depth
|
||||||
|
/// backstop for any other caller (this method is public, and e.g.
|
||||||
|
/// <c>Acceptance/BesluitLifecycleTests.cs</c> calls it directly, bypassing the endpoint).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static (RecordBesluitOutcome Outcome, Aanvraag? Aanvraag) RecordBesluit(string id, Besluit besluit, string? toelichting, DateTimeOffset now)
|
public static (RecordBesluitOutcome Outcome, Aanvraag.Decided? Aanvraag) RecordBesluit(string id, Besluit besluit, string? toelichting, DateTimeOffset now)
|
||||||
{
|
{
|
||||||
lock (_gate)
|
lock (_gate)
|
||||||
{
|
{
|
||||||
using var db = Db.Create();
|
using var db = Db.Create();
|
||||||
var a = db.Applications.Find(id);
|
var row = db.Applications.Find(id);
|
||||||
if (a is null) return (RecordBesluitOutcome.NotFound, null);
|
if (row is null) return (RecordBesluitOutcome.NotFound, null);
|
||||||
var current = a.StatusAt(now).Tag;
|
|
||||||
if (current is null || !BeoordelingRules.CanDecide(current.Value))
|
var current = row.ToDomain();
|
||||||
|
var tag = current.StatusAt(now).Tag;
|
||||||
|
if (tag is null || !BeoordelingRules.CanDecide(tag.Value))
|
||||||
return (RecordBesluitOutcome.Conflict, null);
|
return (RecordBesluitOutcome.Conflict, null);
|
||||||
a.BesluitStatus = besluit;
|
|
||||||
a.BesluitToelichting = toelichting;
|
// tag non-null ⇒ current is Submitted or already Decided, never Concept ⇒ Referentie/
|
||||||
a.UpdatedAt = DateTimeOffset.UtcNow;
|
// SubmittedAt already exist — carried forward rather than re-derived.
|
||||||
|
var (referentie, submittedAt) = current switch
|
||||||
|
{
|
||||||
|
Aanvraag.Submitted s => (s.Referentie, s.SubmittedAt),
|
||||||
|
Aanvraag.Decided d => (d.Referentie, d.SubmittedAt),
|
||||||
|
_ => throw new InvalidOperationException($"Aanvraag {id} has a decidable status but is not submitted."),
|
||||||
|
};
|
||||||
|
|
||||||
|
Aanvraag.Decided decided = besluit switch
|
||||||
|
{
|
||||||
|
Besluit.Goedkeuren => new Aanvraag.Decided.Goedgekeurd
|
||||||
|
{
|
||||||
|
Id = row.Id,
|
||||||
|
Type = row.Type,
|
||||||
|
Owner = row.Owner,
|
||||||
|
DocumentIds = current.DocumentIds,
|
||||||
|
CreatedAt = current.CreatedAt,
|
||||||
|
UpdatedAt = now,
|
||||||
|
ZaakUrl = row.ZaakUrl,
|
||||||
|
ZgwError = row.ZgwError,
|
||||||
|
Referentie = referentie,
|
||||||
|
SubmittedAt = submittedAt,
|
||||||
|
},
|
||||||
|
Besluit.Afwijzen => new Aanvraag.Decided.Afgewezen
|
||||||
|
{
|
||||||
|
Id = row.Id,
|
||||||
|
Type = row.Type,
|
||||||
|
Owner = row.Owner,
|
||||||
|
DocumentIds = current.DocumentIds,
|
||||||
|
CreatedAt = current.CreatedAt,
|
||||||
|
UpdatedAt = now,
|
||||||
|
ZaakUrl = row.ZaakUrl,
|
||||||
|
ZgwError = row.ZgwError,
|
||||||
|
Referentie = referentie,
|
||||||
|
SubmittedAt = submittedAt,
|
||||||
|
Toelichting = toelichting ?? throw new InvalidOperationException("Afwijzen requires a toelichting."),
|
||||||
|
},
|
||||||
|
Besluit.MeerInfoOpvragen => new Aanvraag.Decided.MeerInfoGevraagd
|
||||||
|
{
|
||||||
|
Id = row.Id,
|
||||||
|
Type = row.Type,
|
||||||
|
Owner = row.Owner,
|
||||||
|
DocumentIds = current.DocumentIds,
|
||||||
|
CreatedAt = current.CreatedAt,
|
||||||
|
UpdatedAt = now,
|
||||||
|
ZaakUrl = row.ZaakUrl,
|
||||||
|
ZgwError = row.ZgwError,
|
||||||
|
Referentie = referentie,
|
||||||
|
SubmittedAt = submittedAt,
|
||||||
|
Toelichting = toelichting ?? throw new InvalidOperationException("MeerInfoOpvragen requires a toelichting."),
|
||||||
|
},
|
||||||
|
_ => throw new InvalidOperationException($"Unknown besluit {besluit}."),
|
||||||
|
};
|
||||||
|
|
||||||
|
decided.ApplyTo(row); // also sets row.UpdatedAt = decided.UpdatedAt (= now, above)
|
||||||
db.SaveChanges();
|
db.SaveChanges();
|
||||||
return (RecordBesluitOutcome.Ok, a);
|
return (RecordBesluitOutcome.Ok, decided);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,17 +33,20 @@ public interface IZaakSource
|
|||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Register a just-submitted <paramref name="aanvraag"/> as a zaak (WP-50). The aanvraag is
|
/// Register a just-submitted <paramref name="aanvraag"/> as a zaak (WP-50). The aanvraag is
|
||||||
/// already persisted locally (<c>ApplicationStore.Submit</c> already ran) — this is the
|
/// already persisted locally (<c>ApplicationStore.Submit</c> already ran, hence the
|
||||||
/// integration side-effect, and (Referentie, Status) is what the submit endpoint hands back
|
/// <see cref="Aanvraag.Submitted"/> parameter type — WP-73: a freshly submitted aanvraag
|
||||||
/// to the FE (ADR-0001: route the create through the existing submit response DTO, don't add
|
/// always has a Referentie, so neither implementation needs a null-forgiving deref for it
|
||||||
/// a second one). The local source is a pure passthrough of the already-computed local
|
/// any more) — this is the integration side-effect, and (Referentie, Status) is what the
|
||||||
/// reference/status (ZaakUrl null — nothing to persist); the OpenZaak source creates a Zaak
|
/// submit endpoint hands back to the FE (ADR-0001: route the create through the existing
|
||||||
/// (+ status + rol) and maps the result back into the same shape, returning the zaak's URL
|
/// submit response DTO, don't add a second one). The local source is a pure passthrough of
|
||||||
/// so the endpoint can persist it (<see cref="ApplicationStore.SetZaakUrl"/>, WP-51 needs it
|
/// the already-computed local reference/status (ZaakUrl null — nothing to persist); the
|
||||||
/// to later link documents to this zaak). <paramref name="caller"/> (WP-53) is the acting
|
/// OpenZaak source creates a Zaak (+ status + rol) and maps the result back into the same
|
||||||
/// citizen — the ZGW JWT's audit claims reflect them, not a static config identity.
|
/// shape, returning the zaak's URL so the endpoint can persist it
|
||||||
|
/// (<see cref="ApplicationStore.SetZaakUrl"/>, WP-51 needs it to later link documents to this
|
||||||
|
/// zaak). <paramref name="caller"/> (WP-53) is the acting citizen — the ZGW JWT's audit
|
||||||
|
/// claims reflect them, not a static config identity.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
(string Referentie, AanvraagStatusDto Status, string? ZaakUrl) CreateZaak(Aanvraag aanvraag, DateTimeOffset now, CallerIdentity caller);
|
(string Referentie, AanvraagStatusDto Status, string? ZaakUrl) CreateZaak(Aanvraag.Submitted aanvraag, DateTimeOffset now, CallerIdentity caller);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Extend a behandelaar's already-locally-recorded decision (WP-65b's
|
/// Extend a behandelaar's already-locally-recorded decision (WP-65b's
|
||||||
|
|||||||
@@ -24,8 +24,8 @@ public sealed class LocalZaakSource : IZaakSource
|
|||||||
|
|
||||||
/// <summary>No external zaak to create — the aanvraag's local submit already IS the record
|
/// <summary>No external zaak to create — the aanvraag's local submit already IS the record
|
||||||
/// of truth, exactly as before this seam existed (WP-50). Zero behaviour change.</summary>
|
/// of truth, exactly as before this seam existed (WP-50). Zero behaviour change.</summary>
|
||||||
public (string Referentie, AanvraagStatusDto Status, string? ZaakUrl) CreateZaak(Aanvraag aanvraag, DateTimeOffset now, CallerIdentity caller) =>
|
public (string Referentie, AanvraagStatusDto Status, string? ZaakUrl) CreateZaak(Aanvraag.Submitted aanvraag, DateTimeOffset now, CallerIdentity caller) =>
|
||||||
(aanvraag.Referentie!, aanvraag.ToStatusDto(now), null);
|
(aanvraag.Referentie, aanvraag.ToStatusDto(now), null);
|
||||||
|
|
||||||
/// <summary>No external zaak to update — the recorded decision already IS the record of
|
/// <summary>No external zaak to update — the recorded decision already IS the record of
|
||||||
/// truth locally (WP-66). Zero behaviour change.</summary>
|
/// truth locally (WP-66). Zero behaviour change.</summary>
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ public static class SeedData
|
|||||||
Beroep: "Arts",
|
Beroep: "Arts",
|
||||||
Registratiedatum: new DateOnly(2012, 9, 1),
|
Registratiedatum: new DateOnly(2012, 9, 1),
|
||||||
Geboortedatum: new DateOnly(1985, 3, 14),
|
Geboortedatum: new DateOnly(1985, 3, 14),
|
||||||
Status: new RegistrationStatus(StatusTag.Geregistreerd, HerregistratieDatum: new DateOnly(2027, 3, 1)));
|
Status: new RegistrationStatus.Geregistreerd(HerregistratieDatum: new DateOnly(2027, 3, 1)));
|
||||||
|
|
||||||
public static readonly Person Person = new(
|
public static readonly Person Person = new(
|
||||||
Naam: "Dr. A. (Anna) de Vries",
|
Naam: "Dr. A. (Anna) de Vries",
|
||||||
|
|||||||
@@ -0,0 +1,103 @@
|
|||||||
|
using System.Text.Json;
|
||||||
|
|
||||||
|
namespace BigRegister.Domain.Applications;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The aanvraag lifecycle as a closed union (WP-73): <see cref="Concept"/> (the pre-submission
|
||||||
|
/// wizard draft) → <see cref="Submitted"/> (awaiting a behandelaar's decision, or already
|
||||||
|
/// auto-rejected at submission time — see <see cref="Submitted.Reden"/>) → <see cref="Decided"/>
|
||||||
|
/// (a behandelaar's outcome recorded). Each variant carries only the fields that make sense for
|
||||||
|
/// it; the private base constructor closes the hierarchy to the nested sealed records below, so
|
||||||
|
/// a caller can never construct a fourth variant, a <see cref="Decided"/> with no referentie, or
|
||||||
|
/// an Afwijzen/MeerInfoGevraagd with no toelichting — each is a compile error (a missing
|
||||||
|
/// `required` member, CS9035), not a runtime null-check the way the old flat, mutable
|
||||||
|
/// <c>Aanvraag</c> needed one.
|
||||||
|
///
|
||||||
|
/// <see cref="Api.Data.AanvraagEntity"/> is the EF-mapped persistence row this maps to/from
|
||||||
|
/// (<c>Api.Data.AanvraagMapper</c>'s <c>ToDomain</c>/<c>ApplyTo</c>/<c>ToEntity</c>) — it stays a
|
||||||
|
/// flat, mutable bag with no invariants of its own (SQLite needs exactly that shape); this type
|
||||||
|
/// is what production code actually reads and writes everywhere else. The wire-facing,
|
||||||
|
/// point-in-time <see cref="AanvraagStatus"/> a screen renders is a further, time-dependent
|
||||||
|
/// projection (<c>StatusAt</c>, in <c>Api.Data</c>) — the auto-approval window is a function of
|
||||||
|
/// wall-clock time, not of this stored shape, so it stays a derived read rather than a fourth
|
||||||
|
/// member of this union.
|
||||||
|
/// </summary>
|
||||||
|
public abstract record Aanvraag
|
||||||
|
{
|
||||||
|
public required string Id { get; init; }
|
||||||
|
public required string Type { get; init; } // registratie | herregistratie | intake
|
||||||
|
public required string Owner { get; init; }
|
||||||
|
public required IReadOnlyList<string> DocumentIds { get; init; }
|
||||||
|
public required DateTimeOffset CreatedAt { get; init; }
|
||||||
|
public required DateTimeOffset UpdatedAt { get; init; }
|
||||||
|
|
||||||
|
/// <summary>The OpenZaak zaak's URL, set once CreateZaak (WP-50) registers one — null under
|
||||||
|
/// the local source, or before a zaak has been registered at all.</summary>
|
||||||
|
public string? ZaakUrl { get; init; }
|
||||||
|
|
||||||
|
/// <summary>WP-60: non-null means the ZGW side of this aanvraag's last write did not
|
||||||
|
/// complete — see <c>Api.Data.ApplicationStore.SetZgwError</c>.</summary>
|
||||||
|
public string? ZgwError { get; init; }
|
||||||
|
|
||||||
|
private Aanvraag() { }
|
||||||
|
|
||||||
|
/// <summary>Pre-submission wizard draft.</summary>
|
||||||
|
public sealed record Concept : Aanvraag
|
||||||
|
{
|
||||||
|
public JsonElement? Draft { get; init; }
|
||||||
|
public int StepIndex { get; }
|
||||||
|
public int StepCount { get; }
|
||||||
|
|
||||||
|
/// <summary>0 <= <paramref name="stepIndex"/> <= <paramref name="stepCount"/> — the
|
||||||
|
/// non-strict upper bound, not the strict "<" a wizard's own step cursor uses, because
|
||||||
|
/// <c>ApplicationStore.CreateConcept</c>'s freshly-created row is (StepIndex: 0, StepCount:
|
||||||
|
/// 0) before the wizard's first draft sync ever runs, and that has to stay constructible.
|
||||||
|
/// </summary>
|
||||||
|
public Concept(int stepIndex, int stepCount)
|
||||||
|
{
|
||||||
|
if (stepIndex < 0 || stepCount < 0 || stepIndex > stepCount)
|
||||||
|
throw new ArgumentOutOfRangeException(
|
||||||
|
nameof(stepIndex), stepIndex, $"StepIndex must be within [0, StepCount ({stepCount})].");
|
||||||
|
StepIndex = stepIndex;
|
||||||
|
StepCount = stepCount;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Submitted, no behandelaar decision recorded yet. <see cref="Reden"/> non-null
|
||||||
|
/// means <c>SubmissionRules</c> rejected it automatically at submission time (e.g. a manually
|
||||||
|
/// entered diploma) — terminal in practice (<c>BeoordelingRules.CanDecide</c> refuses a
|
||||||
|
/// besluit once the projected status is already Afgewezen) but structurally still "no besluit
|
||||||
|
/// was ever recorded", hence it lives here rather than in <see cref="Decided"/>.</summary>
|
||||||
|
public sealed record Submitted : Aanvraag
|
||||||
|
{
|
||||||
|
public required string Referentie { get; init; }
|
||||||
|
public required DateTimeOffset SubmittedAt { get; init; }
|
||||||
|
public required bool AutoApprovable { get; init; }
|
||||||
|
public string? Reden { get; init; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>A behandelaar's decision (WP-65b/68) — closed by besluit: only
|
||||||
|
/// <see cref="Afgewezen"/>/<see cref="MeerInfoGevraagd"/> require a toelichting
|
||||||
|
/// (<c>BeoordelingRules.RequiresToelichting</c>'s rule, now also a type, not just an endpoint
|
||||||
|
/// check) — omitting it is a compile error, not merely a 400 the type happens to also let
|
||||||
|
/// slip through at runtime.</summary>
|
||||||
|
public abstract record Decided : Aanvraag
|
||||||
|
{
|
||||||
|
public required string Referentie { get; init; }
|
||||||
|
public required DateTimeOffset SubmittedAt { get; init; }
|
||||||
|
|
||||||
|
private Decided() { }
|
||||||
|
|
||||||
|
public sealed record Goedgekeurd : Decided;
|
||||||
|
|
||||||
|
public sealed record Afgewezen : Decided
|
||||||
|
{
|
||||||
|
public required string Toelichting { get; init; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed record MeerInfoGevraagd : Decided
|
||||||
|
{
|
||||||
|
public required string Toelichting { get; init; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,10 +5,10 @@ namespace BigRegister.Domain.Intake;
|
|||||||
/// scholing question is required. The frontend receives this value
|
/// scholing question is required. The frontend receives this value
|
||||||
/// (<c>GET /intake/policy</c>) and applies it for instant UX feedback
|
/// (<c>GET /intake/policy</c>) and applies it for instant UX feedback
|
||||||
/// (<c>intake.machine.ts</c>'s <c>lageUren</c>); <see cref="RejectIncompleteScholing"/> is the
|
/// (<c>intake.machine.ts</c>'s <c>lageUren</c>); <see cref="RejectIncompleteScholing"/> is the
|
||||||
/// backend re-validating it as the authority on submit (WP-69) — both
|
/// backend re-validating it as the authority on submit (WP-69) —
|
||||||
/// <c>POST /applications/{id}/submit</c> (intake-typed aanvragen only) and the legacy
|
/// <c>POST /applications/{id}/submit</c> (intake-typed aanvragen only) calls it before
|
||||||
/// <c>POST /intakes</c> call it before writing anything, and a violation 400s
|
/// writing anything, and a violation 400s (<c>ProblemDetails</c>), never silently accepts
|
||||||
/// (<c>ProblemDetails</c>), never silently accepts an incomplete answer.
|
/// an incomplete answer.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static class IntakePolicy
|
public static class IntakePolicy
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ public static class HerregistratieRule
|
|||||||
public const int WindowMonths = 12;
|
public const int WindowMonths = 12;
|
||||||
|
|
||||||
public static DateOnly? Deadline(Registration reg) =>
|
public static DateOnly? Deadline(Registration reg) =>
|
||||||
reg.Status.Tag == StatusTag.Geregistreerd ? reg.Status.HerregistratieDatum : null;
|
reg.Status is RegistrationStatus.Geregistreerd g ? g.HerregistratieDatum : null;
|
||||||
|
|
||||||
public static (bool Eligible, string? Reason) Evaluate(
|
public static (bool Eligible, string? Reason) Evaluate(
|
||||||
Registration reg, DateOnly today, int windowMonths = WindowMonths)
|
Registration reg, DateOnly today, int windowMonths = WindowMonths)
|
||||||
@@ -25,8 +25,4 @@ public static class HerregistratieRule
|
|||||||
? (true, $"Registratie verloopt binnen {windowMonths} maanden ({deadline:yyyy-MM-dd}).")
|
? (true, $"Registratie verloopt binnen {windowMonths} maanden ({deadline:yyyy-MM-dd}).")
|
||||||
: (false, $"Herregistratie kan vanaf {windowStart:yyyy-MM-dd}.");
|
: (false, $"Herregistratie kan vanaf {windowStart:yyyy-MM-dd}.");
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Invariant: a non-active status must not carry a herregistratie date.</summary>
|
|
||||||
public static bool IsStatusConsistent(RegistrationStatus s) =>
|
|
||||||
s.Tag != StatusTag.Geregistreerd || s.HerregistratieDatum is not null;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,15 +9,37 @@ public enum StatusTag
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Status as a flat record: only <see cref="StatusTag.Geregistreerd"/> carries a
|
/// Status as a closed union: each variant carries exactly the data that makes sense for it
|
||||||
/// herregistratie deadline. The frontend mirrors this as a discriminated union.
|
/// (WP-73). Only <see cref="Geregistreerd"/> carries a herregistratie deadline; only
|
||||||
|
/// <see cref="Geschorst"/> and <see cref="Doorgehaald"/> carry a reden — and there it is
|
||||||
|
/// required, not nullable (the old flat record left <c>Reden</c> nullable on every tag,
|
||||||
|
/// diverging from the frontend union, which has always required it on those two variants —
|
||||||
|
/// see <c>registratie/domain/registration.ts</c>). The private base constructor closes the
|
||||||
|
/// hierarchy: only the three nested sealed records below can ever inherit from
|
||||||
|
/// <see cref="RegistrationStatus"/>, so a caller can never construct e.g. a
|
||||||
|
/// <see cref="Geschorst"/> with a herregistratie date, or a fourth variant.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed record RegistrationStatus(
|
public abstract record RegistrationStatus
|
||||||
StatusTag Tag,
|
{
|
||||||
DateOnly? HerregistratieDatum = null,
|
public abstract StatusTag Tag { get; }
|
||||||
DateOnly? GeschorstTot = null,
|
|
||||||
string? Reden = null,
|
private RegistrationStatus() { }
|
||||||
DateOnly? DoorgehaaldOp = null);
|
|
||||||
|
public sealed record Geregistreerd(DateOnly HerregistratieDatum) : RegistrationStatus
|
||||||
|
{
|
||||||
|
public override StatusTag Tag => StatusTag.Geregistreerd;
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed record Geschorst(DateOnly GeschorstTot, string Reden) : RegistrationStatus
|
||||||
|
{
|
||||||
|
public override StatusTag Tag => StatusTag.Geschorst;
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed record Doorgehaald(DateOnly DoorgehaaldOp, string Reden) : RegistrationStatus
|
||||||
|
{
|
||||||
|
public override StatusTag Tag => StatusTag.Doorgehaald;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public sealed record Registration(
|
public sealed record Registration(
|
||||||
string BigNummer,
|
string BigNummer,
|
||||||
|
|||||||
@@ -189,24 +189,6 @@ api.MapPost("/registrations", (RegistratieRequest req, HttpContext ctx) =>
|
|||||||
.Produces<ReferentieResponse>()
|
.Produces<ReferentieResponse>()
|
||||||
.ProducesProblem(StatusCodes.Status422UnprocessableEntity);
|
.ProducesProblem(StatusCodes.Status422UnprocessableEntity);
|
||||||
|
|
||||||
api.MapPost("/herregistraties", (HerregistratieRequest req, HttpContext ctx) =>
|
|
||||||
Submit(ctx, "herregistratie", SubmissionRules.RejectZeroUren(req.Uren), req.Documents))
|
|
||||||
.Produces<ReferentieResponse>()
|
|
||||||
.ProducesProblem(StatusCodes.Status422UnprocessableEntity);
|
|
||||||
|
|
||||||
api.MapPost("/intakes", (IntakeRequest req, HttpContext ctx) =>
|
|
||||||
{
|
|
||||||
// WP-69: completeness check outside Submit(...) — deliberately not folded into `reject`,
|
|
||||||
// so this 400 is never cached in IdempotencyStore the way a 422 rejection would be.
|
|
||||||
var reject = SubmissionRules.RejectZeroUren(req.Uren);
|
|
||||||
if (reject is null && IntakePolicy.RejectIncompleteScholing(req.Uren, req.AanvullendeScholing, req.ScholingPunten) is { } incomplete)
|
|
||||||
return Results.Problem(detail: incomplete, statusCode: StatusCodes.Status400BadRequest);
|
|
||||||
return Submit(ctx, "intake", reject);
|
|
||||||
})
|
|
||||||
.Produces<ReferentieResponse>()
|
|
||||||
.ProducesProblem(StatusCodes.Status400BadRequest)
|
|
||||||
.ProducesProblem(StatusCodes.Status422UnprocessableEntity);
|
|
||||||
|
|
||||||
api.MapPost("/change-requests", (ChangeRequestRequest req, HttpContext ctx) =>
|
api.MapPost("/change-requests", (ChangeRequestRequest req, HttpContext ctx) =>
|
||||||
Submit(ctx, "telefoonwijziging", SubmissionRules.RejectPhoneChange(req.Telefoon)))
|
Submit(ctx, "telefoonwijziging", SubmissionRules.RejectPhoneChange(req.Telefoon)))
|
||||||
.Produces<ReferentieResponse>()
|
.Produces<ReferentieResponse>()
|
||||||
@@ -344,7 +326,7 @@ api.MapDelete("/applications/{id}", (string id, HttpContext ctx) =>
|
|||||||
{
|
{
|
||||||
var a = ApplicationStore.Get(id, ctx.Zorgverlener().Bsn);
|
var a = ApplicationStore.Get(id, ctx.Zorgverlener().Bsn);
|
||||||
if (a is null) return Results.NotFound();
|
if (a is null) return Results.NotFound();
|
||||||
if (a.Submitted)
|
if (a is not Aanvraag.Concept)
|
||||||
return Results.Problem(detail: "Een ingediende aanvraag kan niet worden geannuleerd.", statusCode: StatusCodes.Status409Conflict);
|
return Results.Problem(detail: "Een ingediende aanvraag kan niet worden geannuleerd.", statusCode: StatusCodes.Status409Conflict);
|
||||||
ApplicationStore.Delete(id, ctx.Zorgverlener().Bsn);
|
ApplicationStore.Delete(id, ctx.Zorgverlener().Bsn);
|
||||||
return Results.NoContent();
|
return Results.NoContent();
|
||||||
@@ -359,7 +341,7 @@ api.MapPost("/applications/{id}/submit", (string id, SubmitApplicationRequest re
|
|||||||
{
|
{
|
||||||
var existing = ApplicationStore.Get(id, ctx.Zorgverlener().Bsn);
|
var existing = ApplicationStore.Get(id, ctx.Zorgverlener().Bsn);
|
||||||
if (existing is null) return Results.NotFound();
|
if (existing is null) return Results.NotFound();
|
||||||
if (existing.Submitted)
|
if (existing is not Aanvraag.Concept)
|
||||||
return Results.Problem(detail: "Aanvraag is al ingediend.", statusCode: StatusCodes.Status409Conflict);
|
return Results.Problem(detail: "Aanvraag is al ingediend.", statusCode: StatusCodes.Status409Conflict);
|
||||||
|
|
||||||
// Per wizard type: what rejects the submission (→ Afgewezen) and whether it auto-approves.
|
// Per wizard type: what rejects the submission (→ Afgewezen) and whether it auto-approves.
|
||||||
@@ -404,7 +386,7 @@ api.MapPost("/applications/{id}/submit", (string id, SubmitApplicationRequest re
|
|||||||
// failure (an orphan zaak from a rolled-back-then-retried submit is worse than a flagged
|
// failure (an orphan zaak from a rolled-back-then-retried submit is worse than a flagged
|
||||||
// one, see openzaak-integration.md's "Write resilience" section). Each ZGW half is caught
|
// one, see openzaak-integration.md's "Write resilience" section). Each ZGW half is caught
|
||||||
// separately so a create-zaak failure doesn't also skip the (still-local) document link.
|
// separately so a create-zaak failure doesn't also skip the (still-local) document link.
|
||||||
var referentie = submitted.Referentie!;
|
var referentie = submitted.Referentie;
|
||||||
var status = submitted.ToStatusDto(DateTimeOffset.UtcNow);
|
var status = submitted.ToStatusDto(DateTimeOffset.UtcNow);
|
||||||
string? zaakUrl = null;
|
string? zaakUrl = null;
|
||||||
try
|
try
|
||||||
@@ -525,7 +507,8 @@ api.MapPost("/beoordeling/{id}/besluit", (string id, RecordBesluitRequest req, H
|
|||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
RecordZgwDivergence(ctx, a.Id, updated!.Referentie ?? a.Id, ex);
|
// WP-73: Aanvraag.Decided's Referentie is required/non-null — no `?? a.Id` fallback needed.
|
||||||
|
RecordZgwDivergence(ctx, a.Id, updated!.Referentie, ex);
|
||||||
}
|
}
|
||||||
|
|
||||||
return Results.Ok(new RecordBesluitResponse(updated!.ToStatusDto(now)));
|
return Results.Ok(new RecordBesluitResponse(updated!.ToStatusDto(now)));
|
||||||
|
|||||||
@@ -94,10 +94,10 @@ public sealed class OpenZaakZaakSource(HttpClient http, ZgwTokenProvider tokens,
|
|||||||
/// succeeded. The caller (Program.cs's submit endpoint) catches this and records it as a
|
/// succeeded. The caller (Program.cs's submit endpoint) catches this and records it as a
|
||||||
/// flagged divergence (Aanvraag.ZgwError) instead of letting it fail (or diverge) silently —
|
/// flagged divergence (Aanvraag.ZgwError) instead of letting it fail (or diverge) silently —
|
||||||
/// see openzaak-integration.md's "Write resilience" section.
|
/// see openzaak-integration.md's "Write resilience" section.
|
||||||
public (string Referentie, AanvraagStatusDto Status, string? ZaakUrl) CreateZaak(Aanvraag aanvraag, DateTimeOffset now, CallerIdentity caller) =>
|
public (string Referentie, AanvraagStatusDto Status, string? ZaakUrl) CreateZaak(Aanvraag.Submitted aanvraag, DateTimeOffset now, CallerIdentity caller) =>
|
||||||
CreateZaakAsync(aanvraag, now, caller).GetAwaiter().GetResult();
|
CreateZaakAsync(aanvraag, now, caller).GetAwaiter().GetResult();
|
||||||
|
|
||||||
private async Task<(string Referentie, AanvraagStatusDto Status, string? ZaakUrl)> CreateZaakAsync(Aanvraag aanvraag, DateTimeOffset now, CallerIdentity caller)
|
private async Task<(string Referentie, AanvraagStatusDto Status, string? ZaakUrl)> CreateZaakAsync(Aanvraag.Submitted aanvraag, DateTimeOffset now, CallerIdentity caller)
|
||||||
{
|
{
|
||||||
if (!options.ZaaktypeUrls.TryGetValue(aanvraag.Type, out var zaaktypeUrl))
|
if (!options.ZaaktypeUrls.TryGetValue(aanvraag.Type, out var zaaktypeUrl))
|
||||||
throw new InvalidOperationException(
|
throw new InvalidOperationException(
|
||||||
@@ -108,8 +108,9 @@ public sealed class OpenZaakZaakSource(HttpClient http, ZgwTokenProvider tokens,
|
|||||||
Bronorganisatie: options.Bronorganisatie,
|
Bronorganisatie: options.Bronorganisatie,
|
||||||
VerantwoordelijkeOrganisatie: options.VerantwoordelijkeOrganisatie,
|
VerantwoordelijkeOrganisatie: options.VerantwoordelijkeOrganisatie,
|
||||||
Startdatum: DateOnly.FromDateTime(now.UtcDateTime),
|
Startdatum: DateOnly.FromDateTime(now.UtcDateTime),
|
||||||
Identificatie: aanvraag.Referentie
|
// WP-73: Aanvraag.Submitted's Referentie is a required, non-nullable member — a
|
||||||
?? throw new InvalidOperationException("Aanvraag has no Referentie yet — submit it locally first.")), caller);
|
// just-submitted aanvraag always has one, so there is nothing left to null-check here.
|
||||||
|
Identificatie: aanvraag.Referentie), caller);
|
||||||
|
|
||||||
var statustypeUrl = await FirstStatustypeUrlAsync(zaaktypeUrl);
|
var statustypeUrl = await FirstStatustypeUrlAsync(zaaktypeUrl);
|
||||||
await zgw.PostAsync<JsonElement>($"{options.ZrcBaseUrl}/statussen",
|
await zgw.PostAsync<JsonElement>($"{options.ZrcBaseUrl}/statussen",
|
||||||
|
|||||||
@@ -249,94 +249,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"/api/v1/herregistraties": {
|
|
||||||
"post": {
|
|
||||||
"tags": [
|
|
||||||
"BigRegister.Api, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null"
|
|
||||||
],
|
|
||||||
"requestBody": {
|
|
||||||
"content": {
|
|
||||||
"application/json": {
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/components/schemas/HerregistratieRequest"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"required": true
|
|
||||||
},
|
|
||||||
"responses": {
|
|
||||||
"200": {
|
|
||||||
"description": "OK",
|
|
||||||
"content": {
|
|
||||||
"application/json": {
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/components/schemas/ReferentieResponse"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"422": {
|
|
||||||
"description": "Unprocessable Content",
|
|
||||||
"content": {
|
|
||||||
"application/problem+json": {
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/components/schemas/ProblemDetails"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"/api/v1/intakes": {
|
|
||||||
"post": {
|
|
||||||
"tags": [
|
|
||||||
"BigRegister.Api, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null"
|
|
||||||
],
|
|
||||||
"requestBody": {
|
|
||||||
"content": {
|
|
||||||
"application/json": {
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/components/schemas/IntakeRequest"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"required": true
|
|
||||||
},
|
|
||||||
"responses": {
|
|
||||||
"200": {
|
|
||||||
"description": "OK",
|
|
||||||
"content": {
|
|
||||||
"application/json": {
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/components/schemas/ReferentieResponse"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"400": {
|
|
||||||
"description": "Bad Request",
|
|
||||||
"content": {
|
|
||||||
"application/problem+json": {
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/components/schemas/ProblemDetails"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"422": {
|
|
||||||
"description": "Unprocessable Content",
|
|
||||||
"content": {
|
|
||||||
"application/problem+json": {
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/components/schemas/ProblemDetails"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"/api/v1/change-requests": {
|
"/api/v1/change-requests": {
|
||||||
"post": {
|
"post": {
|
||||||
"tags": [
|
"tags": [
|
||||||
@@ -2149,23 +2061,6 @@
|
|||||||
},
|
},
|
||||||
"additionalProperties": false
|
"additionalProperties": false
|
||||||
},
|
},
|
||||||
"HerregistratieRequest": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"uren": {
|
|
||||||
"type": "integer",
|
|
||||||
"format": "int32"
|
|
||||||
},
|
|
||||||
"documents": {
|
|
||||||
"type": "array",
|
|
||||||
"items": {
|
|
||||||
"$ref": "#/components/schemas/DocumentRefDto"
|
|
||||||
},
|
|
||||||
"nullable": true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"additionalProperties": false
|
|
||||||
},
|
|
||||||
"IntakePolicyDto": {
|
"IntakePolicyDto": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
@@ -2176,25 +2071,6 @@
|
|||||||
},
|
},
|
||||||
"additionalProperties": false
|
"additionalProperties": false
|
||||||
},
|
},
|
||||||
"IntakeRequest": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"uren": {
|
|
||||||
"type": "integer",
|
|
||||||
"format": "int32"
|
|
||||||
},
|
|
||||||
"aanvullendeScholing": {
|
|
||||||
"type": "boolean",
|
|
||||||
"nullable": true
|
|
||||||
},
|
|
||||||
"scholingPunten": {
|
|
||||||
"type": "integer",
|
|
||||||
"format": "int32",
|
|
||||||
"nullable": true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"additionalProperties": false
|
|
||||||
},
|
|
||||||
"LetterBlockDto": {
|
"LetterBlockDto": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ public class BesluitLifecycleTests(TestWebApplicationFactory factory) : IClassFi
|
|||||||
private static void Persist(Aanvraag aanvraag)
|
private static void Persist(Aanvraag aanvraag)
|
||||||
{
|
{
|
||||||
using var db = Db.Create();
|
using var db = Db.Create();
|
||||||
db.Applications.Add(aanvraag);
|
db.Applications.Add(aanvraag.ToEntity());
|
||||||
db.SaveChanges();
|
db.SaveChanges();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -74,7 +74,7 @@ public class BesluitLifecycleTests(TestWebApplicationFactory factory) : IClassFi
|
|||||||
// When the status is read long after the auto-approve window has passed — the instant an
|
// When the status is read long after the auto-approve window has passed — the instant an
|
||||||
// undecided auto-approvable case of the same shape WOULD read Goedgekeurd (see
|
// undecided auto-approvable case of the same shape WOULD read Goedgekeurd (see
|
||||||
// ApplicationTests.AutoApprovable_flips_to_goedgekeurd_after_the_window)...
|
// ApplicationTests.AutoApprovable_flips_to_goedgekeurd_after_the_window)...
|
||||||
var longAfterTheWindow = aanvraag.SubmittedAt!.Value + ApplicationStore.ProcessingWindow + TimeSpan.FromDays(1);
|
var longAfterTheWindow = aanvraag.SubmittedAt + ApplicationStore.ProcessingWindow + TimeSpan.FromDays(1);
|
||||||
var status = ApplicationStore.GetAny(aanvraag.Id)!.StatusAt(longAfterTheWindow);
|
var status = ApplicationStore.GetAny(aanvraag.Id)!.StatusAt(longAfterTheWindow);
|
||||||
|
|
||||||
// Then the recorded decision still wins — Afgewezen, never Goedgekeurd.
|
// Then the recorded decision still wins — Afgewezen, never Goedgekeurd.
|
||||||
|
|||||||
@@ -8,13 +8,13 @@ using BigRegister.Tests.Builders;
|
|||||||
namespace BigRegister.Tests.Acceptance;
|
namespace BigRegister.Tests.Acceptance;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Behaviour-level tests for the scholing-threshold enforcement (WP-69) over both live HTTP
|
/// Behaviour-level tests for the scholing-threshold enforcement (WP-69) over
|
||||||
/// paths — <c>POST /applications/{id}/submit</c> (the wizard's real path) and the legacy
|
/// <c>POST /applications/{id}/submit</c> (the wizard's real path — WP-72 deleted the legacy
|
||||||
/// <c>POST /intakes</c> (dead from the UI, still a live crafted-POST surface). Built through
|
/// <c>POST /intakes</c> endpoint this once also covered). Built through the <see
|
||||||
/// the <see cref="Given"/> type-state builder, mirroring <see cref="BesluitLifecycleTests"/>
|
/// cref="Given"/> type-state builder, mirroring <see cref="BesluitLifecycleTests"/> rather
|
||||||
/// rather than the full wizard/upload dance — the builder's default owner IS
|
/// than the full wizard/upload dance — the builder's default owner IS <see
|
||||||
/// <see cref="BigRegister.Api.Domain.Authorization.StubIdentityProvider"/>'s default caller,
|
/// cref="BigRegister.Api.Domain.Authorization.StubIdentityProvider"/>'s default caller, so
|
||||||
/// so no header juggling.
|
/// no header juggling.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class IntakeSubmissionTests(TestWebApplicationFactory factory) : IClassFixture<TestWebApplicationFactory>
|
public class IntakeSubmissionTests(TestWebApplicationFactory factory) : IClassFixture<TestWebApplicationFactory>
|
||||||
{
|
{
|
||||||
@@ -23,7 +23,7 @@ public class IntakeSubmissionTests(TestWebApplicationFactory factory) : IClassFi
|
|||||||
private static void Persist(Aanvraag aanvraag)
|
private static void Persist(Aanvraag aanvraag)
|
||||||
{
|
{
|
||||||
using var db = Db.Create();
|
using var db = Db.Create();
|
||||||
db.Applications.Add(aanvraag);
|
db.Applications.Add(aanvraag.ToEntity());
|
||||||
db.SaveChanges();
|
db.SaveChanges();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -44,8 +44,7 @@ public class IntakeSubmissionTests(TestWebApplicationFactory factory) : IClassFi
|
|||||||
Assert.Equal(HttpStatusCode.BadRequest, res.StatusCode);
|
Assert.Equal(HttpStatusCode.BadRequest, res.StatusCode);
|
||||||
|
|
||||||
// ...and the aanvraag is left a retryable Concept, never marked Submitted.
|
// ...and the aanvraag is left a retryable Concept, never marked Submitted.
|
||||||
var stillConcept = ApplicationStore.GetAny(aanvraag.Id)!;
|
Assert.IsType<Aanvraag.Concept>(ApplicationStore.GetAny(aanvraag.Id));
|
||||||
Assert.False(stillConcept.Submitted);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
@@ -121,16 +120,4 @@ public class IntakeSubmissionTests(TestWebApplicationFactory factory) : IClassFi
|
|||||||
var body = (await res.Content.ReadFromJsonAsync<SubmitApplicationResponse>())!;
|
var body = (await res.Content.ReadFromJsonAsync<SubmitApplicationResponse>())!;
|
||||||
Assert.Equal("Afgewezen", body.Status.Tag);
|
Assert.Equal("Afgewezen", body.Status.Tag);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public async Task Legacy_intakes_endpoint_enforces_it_too()
|
|
||||||
{
|
|
||||||
// Given no aanvraag needed — the legacy endpoint mints its own reference.
|
|
||||||
// When a crafted POST hits the dead-from-the-UI /intakes endpoint below threshold,
|
|
||||||
// with no scholing answer...
|
|
||||||
var res = await _client.PostAsJsonAsync("/api/v1/intakes", new { uren = 500 });
|
|
||||||
|
|
||||||
// Then it is rejected too — the crafted-POST surface this WP closes.
|
|
||||||
Assert.Equal(HttpStatusCode.BadRequest, res.StatusCode);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ using System.Net.Http.Json;
|
|||||||
using BigRegister.Api.Contracts;
|
using BigRegister.Api.Contracts;
|
||||||
using BigRegister.Api.Data;
|
using BigRegister.Api.Data;
|
||||||
using BigRegister.Domain.Applications;
|
using BigRegister.Domain.Applications;
|
||||||
|
using BigRegister.Tests.Builders;
|
||||||
using Microsoft.AspNetCore.Mvc.Testing;
|
using Microsoft.AspNetCore.Mvc.Testing;
|
||||||
|
|
||||||
namespace BigRegister.Tests;
|
namespace BigRegister.Tests;
|
||||||
@@ -227,25 +228,14 @@ public class ApplicationTests(TestWebApplicationFactory factory) : IClassFixture
|
|||||||
|
|
||||||
// --- Auto-approval is computed on read: exercise the window boundary without waiting. ---
|
// --- Auto-approval is computed on read: exercise the window boundary without waiting. ---
|
||||||
|
|
||||||
private static Aanvraag Accepted(bool autoApprovable) => new()
|
private static Aanvraag.Submitted Accepted(bool autoApprovable) =>
|
||||||
{
|
Given.Concept(type: "registratie", owner: "test").Submitted(autoApprovable).Build();
|
||||||
Id = "x",
|
|
||||||
Type = "registratie",
|
|
||||||
Owner = "test",
|
|
||||||
Submitted = true,
|
|
||||||
AutoApprovable = autoApprovable,
|
|
||||||
Referentie = "BIG-2026-1",
|
|
||||||
SubmittedAt = DateTimeOffset.UtcNow,
|
|
||||||
CreatedAt = DateTimeOffset.UtcNow,
|
|
||||||
UpdatedAt = DateTimeOffset.UtcNow,
|
|
||||||
};
|
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void AutoApprovable_flips_to_goedgekeurd_after_the_window()
|
public void AutoApprovable_flips_to_goedgekeurd_after_the_window()
|
||||||
{
|
{
|
||||||
var a = Accepted(autoApprovable: true);
|
var a = Accepted(autoApprovable: true);
|
||||||
Assert.NotNull(a.SubmittedAt);
|
var t0 = a.SubmittedAt;
|
||||||
var t0 = a.SubmittedAt.Value;
|
|
||||||
Assert.Equal("InBehandeling", a.ToStatusDto(t0 + ApplicationStore.ProcessingWindow - TimeSpan.FromSeconds(1)).Tag);
|
Assert.Equal("InBehandeling", a.ToStatusDto(t0 + ApplicationStore.ProcessingWindow - TimeSpan.FromSeconds(1)).Tag);
|
||||||
Assert.Equal("Goedgekeurd", a.ToStatusDto(t0 + ApplicationStore.ProcessingWindow + TimeSpan.FromSeconds(1)).Tag);
|
Assert.Equal("Goedgekeurd", a.ToStatusDto(t0 + ApplicationStore.ProcessingWindow + TimeSpan.FromSeconds(1)).Tag);
|
||||||
}
|
}
|
||||||
@@ -254,8 +244,7 @@ public class ApplicationTests(TestWebApplicationFactory factory) : IClassFixture
|
|||||||
public void Manual_case_never_auto_advances()
|
public void Manual_case_never_auto_advances()
|
||||||
{
|
{
|
||||||
var a = Accepted(autoApprovable: false);
|
var a = Accepted(autoApprovable: false);
|
||||||
Assert.NotNull(a.SubmittedAt);
|
var far = a.SubmittedAt + ApplicationStore.ProcessingWindow + TimeSpan.FromDays(1);
|
||||||
var far = a.SubmittedAt.Value + ApplicationStore.ProcessingWindow + TimeSpan.FromDays(1);
|
|
||||||
var status = a.ToStatusDto(far);
|
var status = a.ToStatusDto(far);
|
||||||
Assert.Equal("InBehandeling", status.Tag);
|
Assert.Equal("InBehandeling", status.Tag);
|
||||||
Assert.True(status.Manual);
|
Assert.True(status.Manual);
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ file sealed class IdMismatchZaakSource : IZaakSource
|
|||||||
inner.ListMyCases(caller, now).Select(Rekey).ToList();
|
inner.ListMyCases(caller, now).Select(Rekey).ToList();
|
||||||
|
|
||||||
public (string Referentie, AanvraagStatusDto Status, string? ZaakUrl) CreateZaak(
|
public (string Referentie, AanvraagStatusDto Status, string? ZaakUrl) CreateZaak(
|
||||||
Aanvraag aanvraag, DateTimeOffset now, CallerIdentity caller) => inner.CreateZaak(aanvraag, now, caller);
|
Aanvraag.Submitted aanvraag, DateTimeOffset now, CallerIdentity caller) => inner.CreateZaak(aanvraag, now, caller);
|
||||||
|
|
||||||
public void RecordBesluit(Aanvraag aanvraag, Besluit besluit, string? toelichting, DateTimeOffset now, CallerIdentity caller) =>
|
public void RecordBesluit(Aanvraag aanvraag, Besluit besluit, string? toelichting, DateTimeOffset now, CallerIdentity caller) =>
|
||||||
inner.RecordBesluit(aanvraag, besluit, toelichting, now, caller);
|
inner.RecordBesluit(aanvraag, besluit, toelichting, now, caller);
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
using System.Threading;
|
using System.Threading;
|
||||||
using BigRegister.Api.Data;
|
using BigRegister.Api.Data;
|
||||||
using BigRegister.Domain.Applications;
|
using BigRegister.Domain.Applications;
|
||||||
using BigRegister.Domain.Beoordeling;
|
|
||||||
|
|
||||||
namespace BigRegister.Tests.Builders;
|
namespace BigRegister.Tests.Builders;
|
||||||
|
|
||||||
@@ -15,18 +14,16 @@ public static class TestIdentities
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Type-state test-data builder for <see cref="Aanvraag"/> (WP-70). "Build test data through the
|
/// Type-state test-data builder for <see cref="Aanvraag"/> (WP-70; simplified at WP-73). "Build
|
||||||
/// same door production code uses" — a Concept can only ever become Submitted, and only a
|
/// test data through the same door production code uses" — <see cref="Aanvraag"/> itself is now
|
||||||
/// Submitted aanvraag can be Decided, so the compiler refuses a fixture built through an illegal
|
/// the closed Concept/Submitted/Decided union WP-73 introduced, so this builder no longer needs
|
||||||
/// path (e.g. deciding a still-Concept aanvraag) instead of that being a runtime assertion nobody
|
/// to mirror production's guards (step-index bounds, "Afwijzen needs a toelichting") by hand —
|
||||||
/// wrote. Start at <see cref="Given.Concept"/>.
|
/// it just calls the real nested constructors/required members, which enforce them. A call that
|
||||||
///
|
/// would build an illegal Aanvraag (e.g. deciding a still-Concept aanvraag, or an Afwijzen with
|
||||||
/// ponytail: <see cref="Aanvraag"/> itself stays exactly what it always was — a mutable,
|
/// no toelichting) is refused the same way production refuses it: a still-Concept aanvraag has
|
||||||
/// EF-backed bag with no invariants of its own (that's Data/ApplicationStore.cs's job in
|
/// no <c>.Decided(...)</c> to call in the first place, and a missing toelichting is a runtime
|
||||||
/// production, via its own lock + <see cref="BeoordelingRules"/> checks). This builder does not
|
/// guard identical to <c>ApplicationStore.RecordBesluit</c>'s own. Start at
|
||||||
/// refactor it into an immutable aggregate; it's the one enforced DOOR through which TEST code
|
/// <see cref="Given.Concept"/>.
|
||||||
/// builds one, so the invariants a real request path enforces don't quietly go missing from a
|
|
||||||
/// fixture assembled by hand.
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static class Given
|
public static class Given
|
||||||
{
|
{
|
||||||
@@ -52,60 +49,50 @@ public sealed class ConceptAanvraag
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// The wizard's current position — step <paramref name="index"/> of <paramref name="of"/>.
|
/// The wizard's current position — step <paramref name="index"/> of <paramref name="of"/>.
|
||||||
/// Guarded the same way a real cursor is (`STEPS[Math.min(cursor, STEPS.length - 1)]` on the
|
/// Bounds are <see cref="Aanvraag.Concept"/>'s OWN constructor's to enforce, not this
|
||||||
/// frontend): <paramref name="of"/> must be at least 1, and <paramref name="index"/> must fall
|
/// builder's — an out-of-range pair fails at <see cref="Build"/>, the same
|
||||||
/// within <c>[0, of)</c> — <c>AtStep(9, 2)</c> is not a position any real wizard can reach, so
|
/// <see cref="ArgumentOutOfRangeException"/> production throws, not a guard restated here.
|
||||||
/// the builder refuses it instead of silently building an impossible fixture.
|
|
||||||
public ConceptAanvraag AtStep(int index, int of)
|
public ConceptAanvraag AtStep(int index, int of)
|
||||||
{
|
{
|
||||||
if (of < 1)
|
|
||||||
throw new ArgumentOutOfRangeException(nameof(of), of, "Step count must be at least 1.");
|
|
||||||
if (index < 0 || index >= of)
|
|
||||||
throw new ArgumentOutOfRangeException(nameof(index), index, $"Step index must be within [0, {of}).");
|
|
||||||
_stepIndex = index;
|
_stepIndex = index;
|
||||||
_stepCount = of;
|
_stepCount = of;
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Submits the draft — always assigns a Referentie AND SubmittedAt together (mirrors
|
/// Submits the draft — always assigns a Referentie AND SubmittedAt together (mirrors
|
||||||
/// <c>ApplicationStore.Submit</c>), so <c>Aanvraag.StatusAt</c>'s <c>Referentie!</c> is honest
|
/// <c>ApplicationStore.Submit</c>), so a fixture built this way can never hit the
|
||||||
/// for every fixture built this way, never a null-ref waiting to happen.
|
/// null-forgiving derefs the pre-WP-73 flat Aanvraag needed (there's nothing to force any
|
||||||
public SubmittedAanvraag Submitted(bool autoApprovable = false) =>
|
/// more: both are required, non-null members of <see cref="Aanvraag.Submitted"/>).
|
||||||
new(_type, _owner, _stepIndex, _stepCount, autoApprovable);
|
public SubmittedAanvraag Submitted(bool autoApprovable = false) => new(_type, _owner, autoApprovable);
|
||||||
|
|
||||||
public Aanvraag Build() => new()
|
public Aanvraag.Concept Build() => new(_stepIndex, _stepCount)
|
||||||
{
|
{
|
||||||
Id = Guid.NewGuid().ToString(),
|
Id = Guid.NewGuid().ToString(),
|
||||||
Type = _type,
|
Type = _type,
|
||||||
Owner = _owner,
|
Owner = _owner,
|
||||||
StepIndex = _stepIndex,
|
DocumentIds = Array.Empty<string>(),
|
||||||
StepCount = _stepCount,
|
|
||||||
CreatedAt = DateTimeOffset.UtcNow,
|
CreatedAt = DateTimeOffset.UtcNow,
|
||||||
UpdatedAt = DateTimeOffset.UtcNow,
|
UpdatedAt = DateTimeOffset.UtcNow,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>A submitted aanvraag, open for a behandelaar's decision. The only next step is
|
/// <summary>A submitted aanvraag, open for a behandelaar's decision. The only next step is
|
||||||
/// <see cref="Decided"/> — there is no way back to <c>ConceptAanvraag</c>.</summary>
|
/// <see cref="Decided"/> — there is no way back to <see cref="ConceptAanvraag"/>.</summary>
|
||||||
public sealed class SubmittedAanvraag
|
public sealed class SubmittedAanvraag
|
||||||
{
|
{
|
||||||
private static int _referentieSeq;
|
private static int _referentieSeq;
|
||||||
|
|
||||||
private readonly string _type;
|
private readonly string _type;
|
||||||
private readonly string _owner;
|
private readonly string _owner;
|
||||||
private readonly int _stepIndex;
|
|
||||||
private readonly int _stepCount;
|
|
||||||
private readonly bool _autoApprovable;
|
private readonly bool _autoApprovable;
|
||||||
private readonly string _referentie;
|
private readonly string _referentie;
|
||||||
private readonly DateTimeOffset _submittedAt;
|
private readonly DateTimeOffset _submittedAt;
|
||||||
private string? _zaakUrl;
|
private string? _zaakUrl;
|
||||||
|
|
||||||
internal SubmittedAanvraag(string type, string owner, int stepIndex, int stepCount, bool autoApprovable)
|
internal SubmittedAanvraag(string type, string owner, bool autoApprovable)
|
||||||
{
|
{
|
||||||
_type = type;
|
_type = type;
|
||||||
_owner = owner;
|
_owner = owner;
|
||||||
_stepIndex = stepIndex;
|
|
||||||
_stepCount = stepCount;
|
|
||||||
_autoApprovable = autoApprovable;
|
_autoApprovable = autoApprovable;
|
||||||
// A plausible reference in SubmissionRules.NewReference's shape ("BIG-2026-" + a number) —
|
// A plausible reference in SubmissionRules.NewReference's shape ("BIG-2026-" + a number) —
|
||||||
// sequential (not random) so a fixture's value is reproducible across a test run.
|
// sequential (not random) so a fixture's value is reproducible across a test run.
|
||||||
@@ -114,67 +101,97 @@ public sealed class SubmittedAanvraag
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Registers this aanvraag's already-known OpenZaak zaak URL — mirrors
|
/// <summary>Registers this aanvraag's already-known OpenZaak zaak URL — mirrors
|
||||||
/// <see cref="Api.Data.ApplicationStore.SetZaakUrl"/>, the one production writer of this
|
/// <see cref="ApplicationStore.SetZaakUrl"/>, the one production writer of this field, so a
|
||||||
/// field, so a fixture that needs a pre-existing zaak doesn't reach past <c>Build()</c> to
|
/// fixture that needs a pre-existing zaak doesn't reach past <c>Build()</c> to mutate the
|
||||||
/// mutate the result by hand.</summary>
|
/// result by hand.</summary>
|
||||||
public SubmittedAanvraag WithZaakUrl(string zaakUrl)
|
public SubmittedAanvraag WithZaakUrl(string zaakUrl)
|
||||||
{
|
{
|
||||||
_zaakUrl = zaakUrl;
|
_zaakUrl = zaakUrl;
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Records a behandelaar's decision — reusing <see cref="BeoordelingRules.RequiresToelichting"/>,
|
/// <summary>Records a behandelaar's decision. Unlike the pre-WP-73 builder, there is no
|
||||||
/// the SAME rule production's besluit endpoint runs, rather than restating it here where it
|
/// hand-written toelichting guard mirroring <c>BeoordelingRules.RequiresToelichting</c> any
|
||||||
/// could quietly drift. Throws <see cref="ArgumentException"/> for an Afwijzen/MeerInfoOpvragen
|
/// more — <see cref="Aanvraag.Decided.Afgewezen"/>/<see cref="Aanvraag.Decided.MeerInfoGevraagd"/>
|
||||||
/// with a null/blank <paramref name="toelichting"/> — exactly what that endpoint rejects with
|
/// simply have a `required string Toelichting` member; the null-coalescing throw below is the
|
||||||
/// a 400, just caught here at fixture-build time instead.</summary>
|
/// one place a null has to turn into an exception (this method's own parameter is still the
|
||||||
public DecidedAanvraag Decided(Besluit besluit, string? toelichting = null)
|
/// nullable <c>string?</c> a wire request would carry), same failure production's own
|
||||||
|
/// <c>ApplicationStore.RecordBesluit</c> raises for the identical input.</summary>
|
||||||
|
public DecidedAanvraag Decided(Besluit besluit, string? toelichting = null) => new(BuildDecided(besluit, toelichting));
|
||||||
|
|
||||||
|
private Aanvraag.Decided BuildDecided(Besluit besluit, string? toelichting)
|
||||||
{
|
{
|
||||||
if (BeoordelingRules.RequiresToelichting(besluit) && string.IsNullOrWhiteSpace(toelichting))
|
var (id, createdAt) = (Guid.NewGuid().ToString(), _submittedAt);
|
||||||
throw new ArgumentException($"{besluit} requires a toelichting.", nameof(toelichting));
|
return besluit switch
|
||||||
return new DecidedAanvraag(this, besluit, toelichting);
|
{
|
||||||
|
Besluit.Goedkeuren => new Aanvraag.Decided.Goedgekeurd
|
||||||
|
{
|
||||||
|
Id = id,
|
||||||
|
Type = _type,
|
||||||
|
Owner = _owner,
|
||||||
|
DocumentIds = Array.Empty<string>(),
|
||||||
|
CreatedAt = createdAt,
|
||||||
|
UpdatedAt = createdAt,
|
||||||
|
ZaakUrl = _zaakUrl,
|
||||||
|
Referentie = _referentie,
|
||||||
|
SubmittedAt = _submittedAt,
|
||||||
|
},
|
||||||
|
Besluit.Afwijzen => new Aanvraag.Decided.Afgewezen
|
||||||
|
{
|
||||||
|
Id = id,
|
||||||
|
Type = _type,
|
||||||
|
Owner = _owner,
|
||||||
|
DocumentIds = Array.Empty<string>(),
|
||||||
|
CreatedAt = createdAt,
|
||||||
|
UpdatedAt = createdAt,
|
||||||
|
ZaakUrl = _zaakUrl,
|
||||||
|
Referentie = _referentie,
|
||||||
|
SubmittedAt = _submittedAt,
|
||||||
|
Toelichting = toelichting ?? throw new ArgumentException("Afwijzen requires a toelichting.", nameof(toelichting)),
|
||||||
|
},
|
||||||
|
Besluit.MeerInfoOpvragen => new Aanvraag.Decided.MeerInfoGevraagd
|
||||||
|
{
|
||||||
|
Id = id,
|
||||||
|
Type = _type,
|
||||||
|
Owner = _owner,
|
||||||
|
DocumentIds = Array.Empty<string>(),
|
||||||
|
CreatedAt = createdAt,
|
||||||
|
UpdatedAt = createdAt,
|
||||||
|
ZaakUrl = _zaakUrl,
|
||||||
|
Referentie = _referentie,
|
||||||
|
SubmittedAt = _submittedAt,
|
||||||
|
Toelichting = toelichting ?? throw new ArgumentException("MeerInfoOpvragen requires a toelichting.", nameof(toelichting)),
|
||||||
|
},
|
||||||
|
_ => throw new ArgumentOutOfRangeException(nameof(besluit), besluit, "Unknown besluit."),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public Aanvraag Build() => new()
|
public Aanvraag.Submitted Build() => new()
|
||||||
{
|
{
|
||||||
Id = Guid.NewGuid().ToString(),
|
Id = Guid.NewGuid().ToString(),
|
||||||
Type = _type,
|
Type = _type,
|
||||||
Owner = _owner,
|
Owner = _owner,
|
||||||
StepIndex = _stepIndex,
|
DocumentIds = Array.Empty<string>(),
|
||||||
StepCount = _stepCount,
|
|
||||||
Submitted = true,
|
|
||||||
Referentie = _referentie,
|
|
||||||
AutoApprovable = _autoApprovable,
|
|
||||||
SubmittedAt = _submittedAt,
|
|
||||||
CreatedAt = _submittedAt,
|
CreatedAt = _submittedAt,
|
||||||
UpdatedAt = _submittedAt,
|
UpdatedAt = _submittedAt,
|
||||||
ZaakUrl = _zaakUrl,
|
ZaakUrl = _zaakUrl,
|
||||||
|
Referentie = _referentie,
|
||||||
|
SubmittedAt = _submittedAt,
|
||||||
|
AutoApprovable = _autoApprovable,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>A submitted aanvraag with a behandelaar's decision already recorded. Terminal in the
|
/// <summary>A submitted aanvraag with a behandelaar's decision already recorded — terminal in
|
||||||
/// builder too — there's nothing past <see cref="Build"/>, matching Goedgekeurd/Afgewezen being
|
/// the builder too, matching Goedgekeurd/Afgewezen being terminal in the domain
|
||||||
/// terminal in the domain (<see cref="BeoordelingRules.CanDecide"/>); a fixture that needs a
|
/// (<see cref="BigRegister.Domain.Beoordeling.BeoordelingRules.CanDecide"/>); a fixture that
|
||||||
/// SECOND besluit (the MeerInfoGevraagd "still decidable" case) builds fresh from
|
/// needs a SECOND besluit (the MeerInfoGevraagd "still decidable" case) builds fresh from
|
||||||
/// <see cref="Given.Concept"/> again, exactly as a real second request would.</summary>
|
/// <see cref="Given.Concept"/> again, exactly as a real second request would. Just a one-line
|
||||||
public sealed class DecidedAanvraag
|
/// wrapper around the already-fully-built <see cref="Aanvraag.Decided"/> value — WP-73 moved
|
||||||
|
/// all the actual construction (and its invariant enforcement) into
|
||||||
|
/// <see cref="SubmittedAanvraag.Decided"/> itself, so there's nothing left for this type to do
|
||||||
|
/// except keep <c>.Decided(...).Build()</c> a valid two-call chain for the existing test
|
||||||
|
/// suite.</summary>
|
||||||
|
public sealed class DecidedAanvraag(Aanvraag.Decided value)
|
||||||
{
|
{
|
||||||
private readonly SubmittedAanvraag _submitted;
|
public Aanvraag.Decided Build() => value;
|
||||||
private readonly Besluit _besluit;
|
|
||||||
private readonly string? _toelichting;
|
|
||||||
|
|
||||||
internal DecidedAanvraag(SubmittedAanvraag submitted, Besluit besluit, string? toelichting)
|
|
||||||
{
|
|
||||||
_submitted = submitted;
|
|
||||||
_besluit = besluit;
|
|
||||||
_toelichting = toelichting;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Aanvraag Build()
|
|
||||||
{
|
|
||||||
var aanvraag = _submitted.Build();
|
|
||||||
aanvraag.BesluitStatus = _besluit;
|
|
||||||
aanvraag.BesluitToelichting = _toelichting;
|
|
||||||
return aanvraag;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
using BigRegister.Api.Data;
|
||||||
using BigRegister.Domain.Applications;
|
using BigRegister.Domain.Applications;
|
||||||
using BigRegister.Domain.Beoordeling;
|
using BigRegister.Domain.Beoordeling;
|
||||||
using BigRegister.Tests.Builders;
|
using BigRegister.Tests.Builders;
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ public class HerregistratieRuleTests
|
|||||||
private static Registration Active(DateOnly deadline) => new(
|
private static Registration Active(DateOnly deadline) => new(
|
||||||
"19012345601", "Test", "Arts",
|
"19012345601", "Test", "Arts",
|
||||||
new DateOnly(2012, 9, 1), new DateOnly(1985, 3, 14),
|
new DateOnly(2012, 9, 1), new DateOnly(1985, 3, 14),
|
||||||
new RegistrationStatus(StatusTag.Geregistreerd, HerregistratieDatum: deadline));
|
new RegistrationStatus.Geregistreerd(HerregistratieDatum: deadline));
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void Eligible_within_window()
|
public void Eligible_within_window()
|
||||||
@@ -40,18 +40,9 @@ public class HerregistratieRuleTests
|
|||||||
{
|
{
|
||||||
var reg = Active(new DateOnly(2027, 3, 1)) with
|
var reg = Active(new DateOnly(2027, 3, 1)) with
|
||||||
{
|
{
|
||||||
Status = new RegistrationStatus(StatusTag.Geschorst, GeschorstTot: new DateOnly(2027, 1, 1), Reden: "x"),
|
Status = new RegistrationStatus.Geschorst(GeschorstTot: new DateOnly(2027, 1, 1), Reden: "x"),
|
||||||
};
|
};
|
||||||
var (eligible, _) = HerregistratieRule.Evaluate(reg, today: new DateOnly(2026, 6, 26));
|
var (eligible, _) = HerregistratieRule.Evaluate(reg, today: new DateOnly(2026, 6, 26));
|
||||||
Assert.False(eligible);
|
Assert.False(eligible);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void Status_consistency_invariant()
|
|
||||||
{
|
|
||||||
Assert.True(HerregistratieRule.IsStatusConsistent(
|
|
||||||
new RegistrationStatus(StatusTag.Geregistreerd, HerregistratieDatum: new DateOnly(2027, 3, 1))));
|
|
||||||
Assert.False(HerregistratieRule.IsStatusConsistent(
|
|
||||||
new RegistrationStatus(StatusTag.Geregistreerd)));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -89,27 +89,6 @@ public class EndpointTests(TestWebApplicationFactory factory) : IClassFixture<Te
|
|||||||
Assert.Contains("application/problem+json", contentType.ToString());
|
Assert.Contains("application/problem+json", contentType.ToString());
|
||||||
}
|
}
|
||||||
|
|
||||||
[Theory]
|
|
||||||
[InlineData("/api/v1/intakes")]
|
|
||||||
[InlineData("/api/v1/herregistraties")]
|
|
||||||
public async Task Zero_hours_submission_is_rejected(string route)
|
|
||||||
{
|
|
||||||
var res = await _client.PostAsJsonAsync(route, new { uren = 0 });
|
|
||||||
Assert.Equal(HttpStatusCode.UnprocessableEntity, res.StatusCode);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Theory]
|
|
||||||
[InlineData("/api/v1/intakes")]
|
|
||||||
[InlineData("/api/v1/herregistraties")]
|
|
||||||
public async Task Worked_hours_submission_succeeds(string route)
|
|
||||||
{
|
|
||||||
// WP-69: 40 is below IntakePolicy.ScholingThreshold, so /intakes now requires the
|
|
||||||
// scholing question answered — `aanvullendeScholing` is unknown to (and ignored by)
|
|
||||||
// HerregistratieRequest, so this one extra field keeps serving both rows unchanged.
|
|
||||||
var res = await _client.PostAsJsonAsync(route, new { uren = 40, aanvullendeScholing = false });
|
|
||||||
res.EnsureSuccessStatusCode();
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task Change_request_with_valid_phone_succeeds()
|
public async Task Change_request_with_valid_phone_succeeds()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -156,7 +156,7 @@ public class OpenZaakZaakSourceTests
|
|||||||
var zaakBody = handler.BodyOf($"{ZrcBase}/zaken");
|
var zaakBody = handler.BodyOf($"{ZrcBase}/zaken");
|
||||||
Assert.Contains(zaaktypeUrl, zaakBody);
|
Assert.Contains(zaaktypeUrl, zaakBody);
|
||||||
Assert.Contains("123443210", zaakBody);
|
Assert.Contains("123443210", zaakBody);
|
||||||
Assert.Contains(aanvraag.Referentie!, zaakBody);
|
Assert.Contains(aanvraag.Referentie, zaakBody);
|
||||||
|
|
||||||
// Status: points at the created zaak's URL and the resolved statustype.
|
// Status: points at the created zaak's URL and the resolved statustype.
|
||||||
var statusBody = handler.BodyOf($"{ZrcBase}/statussen");
|
var statusBody = handler.BodyOf($"{ZrcBase}/statussen");
|
||||||
@@ -310,7 +310,7 @@ public class OpenZaakZaakSourceTests
|
|||||||
|
|
||||||
// --- WP-60: bounded retry in ZgwHttpClient, exercised through the create-zaak write path ---
|
// --- WP-60: bounded retry in ZgwHttpClient, exercised through the create-zaak write path ---
|
||||||
|
|
||||||
private static (ZgwOptions options, Aanvraag aanvraag, CallerIdentity caller) CreateZaakFixture()
|
private static (ZgwOptions options, Aanvraag.Submitted aanvraag, CallerIdentity caller) CreateZaakFixture()
|
||||||
{
|
{
|
||||||
const string zaaktypeUrl = $"{ZtBase}/zaaktypen/zt-registratie";
|
const string zaaktypeUrl = $"{ZtBase}/zaaktypen/zt-registratie";
|
||||||
var options = new ZgwOptions
|
var options = new ZgwOptions
|
||||||
|
|||||||
@@ -405,104 +405,6 @@ export class ApiClient {
|
|||||||
return Promise.resolve<ReferentieResponse>(null as any);
|
return Promise.resolve<ReferentieResponse>(null as any);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @return OK
|
|
||||||
*/
|
|
||||||
herregistraties(body: HerregistratieRequest): Promise<ReferentieResponse> {
|
|
||||||
let url_ = this.baseUrl + "/api/v1/herregistraties";
|
|
||||||
url_ = url_.replace(/[?&]$/, "");
|
|
||||||
|
|
||||||
const content_ = JSON.stringify(body);
|
|
||||||
|
|
||||||
let options_: RequestInit = {
|
|
||||||
body: content_,
|
|
||||||
method: "POST",
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
"Accept": "application/json"
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return this.http.fetch(url_, options_).then((_response: Response) => {
|
|
||||||
return this.processHerregistraties(_response);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
protected processHerregistraties(response: Response): Promise<ReferentieResponse> {
|
|
||||||
const status = response.status;
|
|
||||||
let _headers: any = {}; if (response.headers && response.headers.forEach) { response.headers.forEach((v: any, k: any) => _headers[k] = v); };
|
|
||||||
if (status === 200) {
|
|
||||||
return response.text().then((_responseText) => {
|
|
||||||
let result200: any = null;
|
|
||||||
result200 = _responseText === "" ? null : JSON.parse(_responseText, this.jsonParseReviver) as ReferentieResponse;
|
|
||||||
return result200;
|
|
||||||
});
|
|
||||||
} else if (status === 422) {
|
|
||||||
return response.text().then((_responseText) => {
|
|
||||||
let result422: any = null;
|
|
||||||
result422 = _responseText === "" ? null : JSON.parse(_responseText, this.jsonParseReviver) as ProblemDetails;
|
|
||||||
return throwException("Unprocessable Content", status, _responseText, _headers, result422);
|
|
||||||
});
|
|
||||||
} else if (status !== 200 && status !== 204) {
|
|
||||||
return response.text().then((_responseText) => {
|
|
||||||
return throwException("An unexpected server error occurred.", status, _responseText, _headers);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return Promise.resolve<ReferentieResponse>(null as any);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return OK
|
|
||||||
*/
|
|
||||||
intakes(body: IntakeRequest): Promise<ReferentieResponse> {
|
|
||||||
let url_ = this.baseUrl + "/api/v1/intakes";
|
|
||||||
url_ = url_.replace(/[?&]$/, "");
|
|
||||||
|
|
||||||
const content_ = JSON.stringify(body);
|
|
||||||
|
|
||||||
let options_: RequestInit = {
|
|
||||||
body: content_,
|
|
||||||
method: "POST",
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
"Accept": "application/json"
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return this.http.fetch(url_, options_).then((_response: Response) => {
|
|
||||||
return this.processIntakes(_response);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
protected processIntakes(response: Response): Promise<ReferentieResponse> {
|
|
||||||
const status = response.status;
|
|
||||||
let _headers: any = {}; if (response.headers && response.headers.forEach) { response.headers.forEach((v: any, k: any) => _headers[k] = v); };
|
|
||||||
if (status === 200) {
|
|
||||||
return response.text().then((_responseText) => {
|
|
||||||
let result200: any = null;
|
|
||||||
result200 = _responseText === "" ? null : JSON.parse(_responseText, this.jsonParseReviver) as ReferentieResponse;
|
|
||||||
return result200;
|
|
||||||
});
|
|
||||||
} else if (status === 400) {
|
|
||||||
return response.text().then((_responseText) => {
|
|
||||||
let result400: any = null;
|
|
||||||
result400 = _responseText === "" ? null : JSON.parse(_responseText, this.jsonParseReviver) as ProblemDetails;
|
|
||||||
return throwException("Bad Request", status, _responseText, _headers, result400);
|
|
||||||
});
|
|
||||||
} else if (status === 422) {
|
|
||||||
return response.text().then((_responseText) => {
|
|
||||||
let result422: any = null;
|
|
||||||
result422 = _responseText === "" ? null : JSON.parse(_responseText, this.jsonParseReviver) as ProblemDetails;
|
|
||||||
return throwException("Unprocessable Content", status, _responseText, _headers, result422);
|
|
||||||
});
|
|
||||||
} else if (status !== 200 && status !== 204) {
|
|
||||||
return response.text().then((_responseText) => {
|
|
||||||
return throwException("An unexpected server error occurred.", status, _responseText, _headers);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return Promise.resolve<ReferentieResponse>(null as any);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return OK
|
* @return OK
|
||||||
*/
|
*/
|
||||||
@@ -2205,21 +2107,10 @@ export interface HerregistratieDecisionsDto {
|
|||||||
herregistratieReason?: string | undefined;
|
herregistratieReason?: string | undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface HerregistratieRequest {
|
|
||||||
uren?: number;
|
|
||||||
documents?: DocumentRefDto[] | undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface IntakePolicyDto {
|
export interface IntakePolicyDto {
|
||||||
scholingThreshold?: number;
|
scholingThreshold?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IntakeRequest {
|
|
||||||
uren?: number;
|
|
||||||
aanvullendeScholing?: boolean | undefined;
|
|
||||||
scholingPunten?: number | undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface LetterBlockDto {
|
export interface LetterBlockDto {
|
||||||
type?: string | undefined;
|
type?: string | undefined;
|
||||||
blockId?: string | undefined;
|
blockId?: string | undefined;
|
||||||
|
|||||||
Reference in New Issue
Block a user