refactor(backend): move aanvraag status lifecycle into the domain (WP-68 F3)

The status was derived in Contracts/Mappers.ToStatusDto, not the domain; Concept was
a magic "Concept" string with no AanvraagStatusTag member; and the besluit endpoint
re-derived its own guard by reading the status back out of the DTO and Enum.Parse-ing
it. New Domain/Applications/AanvraagStatus.cs models the full status (Concept
included, via a null Tag rather than a sixth enum member) as a closed type,
constructible only through its factories. Aanvraag.StatusAt(now) carries the logic
verbatim; Mappers.ToStatusDto and ZgwZaakMapper's two status producers become
one-line projections onto the same wire DTO, so the wire shape is unchanged (gen:api
shows zero diff beyond F1's). The one remaining Enum.Parse (the beoordeling GET,
which crosses the IZaakSource wire boundary) is now non-throwing on an unrecognised
tag.

Also, WP-68 F2: the besluit transition-legality check now runs inside
ApplicationStore.RecordBesluit's write lock instead of in the endpoint beforehand —
two concurrent besluiten used to both pass the check before either wrote, letting
the second silently overwrite a terminal decision. RecordBesluit returns an
Ok/NotFound/Conflict outcome, mirroring DocumentStore.DeleteResult.

Also, WP-68 F6: the "toelichting required" rule moves from an inline endpoint check
into BeoordelingRules.RequiresToelichting, alongside CanDecide.

The three tests naming this refactor's regression net
(AanvraagStatusTag_covers_the_published_lifecycle,
AutoApprovable_flips_to_goedgekeurd_after_the_window, ZgwZaakMapperTests) pass
unmodified.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
eho
2026-08-05 15:34:10 +02:00
co-authored by Claude Opus 5
parent fd04221d2f
commit fc6e73806a
13 changed files with 159 additions and 78 deletions
@@ -1,4 +1,5 @@
using BigRegister.Api.Data;
using BigRegister.Domain.Applications;
using BigRegister.Domain.Diplomas;
using BigRegister.Domain.Documents;
using BigRegister.Domain.People;
@@ -36,31 +37,17 @@ public static class Mappers
public static DocumentCategoryDto ToDto(this DocumentCategory c) => new(
c.CategoryId, c.Label, c.Description, c.Required, c.AcceptedTypes, c.MaxSizeMb, c.Multiple, c.AllowPostDelivery);
// Aanvraag status is COMPUTED ON READ: an auto-approvable submission reports
// Goedgekeurd once past the processing window, else In behandeling; a manual case
// stays In behandeling until a behandelaar records a decision (WP-65b — before that
// WP, it stayed In behandeling forever, awaiting the then-unbuilt backoffice). Pure —
// testable by passing different `now` values without waiting for the wall clock.
public static AanvraagStatusDto ToStatusDto(this Aanvraag a, DateTimeOffset now)
{
if (!a.Submitted)
return new("Concept", StepIndex: a.StepIndex, StepCount: a.StepCount);
if (a.Reden is not null)
return new(AanvraagStatusTag.Afgewezen.ToString(), Referentie: a.Referentie, Reden: a.Reden);
// A recorded decision (WP-65b) wins over the auto-approve computation below — a
// behandelaar's explicit besluit is authoritative once made.
if (a.BesluitStatus is { } besluit)
return besluit switch
{
Besluit.Goedkeuren => new(AanvraagStatusTag.Goedgekeurd.ToString(), Referentie: a.Referentie),
Besluit.Afwijzen => new(AanvraagStatusTag.Afgewezen.ToString(), Referentie: a.Referentie, Reden: a.BesluitToelichting),
Besluit.MeerInfoOpvragen => new(AanvraagStatusTag.MeerInfoGevraagd.ToString(), Referentie: a.Referentie, Reden: a.BesluitToelichting),
_ => throw new InvalidOperationException($"Unknown besluit {besluit}"),
};
if (a.AutoApprovable && now > a.SubmittedAt!.Value + ApplicationStore.ProcessingWindow)
return new(AanvraagStatusTag.Goedgekeurd.ToString(), Referentie: a.Referentie);
return new(AanvraagStatusTag.InBehandeling.ToString(), Referentie: a.Referentie, Manual: !a.AutoApprovable);
}
/// <summary>Wire projection of an <see cref="AanvraagStatus"/> — the null "Concept" case
/// is the one place a status has no <see cref="AanvraagStatusTag"/>, so it becomes the wire
/// convention's magic string here at the boundary rather than living inside the domain type.
/// Shared by <see cref="ToStatusDto"/> and <c>ZgwZaakMapper</c>, so both status producers
/// agree on the projection (WP-68 F3).</summary>
public static AanvraagStatusDto ToDto(this AanvraagStatus s) => new(
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
// projection of that domain method onto the wire DTO (WP-68 F3).
public static AanvraagStatusDto ToStatusDto(this Aanvraag a, DateTimeOffset now) => a.StatusAt(now).ToDto();
public static ApplicationSummaryDto ToSummaryDto(this Aanvraag a, DateTimeOffset now) => new(
a.Id, a.Type, a.ToStatusDto(now), a.DocumentIds,
@@ -1,31 +1,15 @@
using System.Text.Json;
using BigRegister.Domain.Applications;
using BigRegister.Domain.Beoordeling;
using BigRegister.Domain.Submissions;
namespace BigRegister.Api.Data;
/// <summary>
/// The post-submission aanvraag status lifecycle (ADR-0002, WP-63): Ingediend → In
/// behandeling → (Meer info gevraagd ⇄) → Goedgekeurd/Afgewezen. Concept (pre-submission,
/// the wizard draft) isn't part of this enum — see <see cref="Aanvraag.Submitted"/>.
/// <see cref="Ingediend"/> and <see cref="MeerInfoGevraagd"/> are not reachable yet: no
/// endpoint sets them (that's WP-65's behandelaar-facing mutation) — modelled here so the
/// contract is ready when it does.
/// </summary>
public enum AanvraagStatusTag { Ingediend, InBehandeling, MeerInfoGevraagd, Goedgekeurd, Afgewezen }
/// <summary>
/// A behandelaar's recorded decision (WP-65b) — the three actions the beoordeling screen
/// offers, each advancing <see cref="Aanvraag.BesluitStatus"/> and (via
/// <see cref="BigRegister.Api.Contracts.Mappers.ToStatusDto"/>) the published
/// <see cref="AanvraagStatusTag"/> the FE renders.
/// </summary>
public enum Besluit { Goedkeuren, Afwijzen, MeerInfoOpvragen }
/// <summary>
/// An application (aanvraag) — the system of record the dashboard reads. A wizard
/// creates one as a Concept on its first step, syncs its draft snapshot per step,
/// then submits it into the Concept → In behandeling → Goedgekeurd/Afgewezen
/// lifecycle (ADR-0002). Status is COMPUTED ON READ (see Mappers.ToStatusDto) so
/// lifecycle (ADR-0002). Status is COMPUTED ON READ (see <see cref="StatusAt"/>) so
/// auto-approval is purely a function of stored timestamps — no timers, no jobs.
/// </summary>
public sealed class Aanvraag
@@ -69,6 +53,26 @@ public sealed class Aanvraag
/// <summary>The behandelaar's toelichting — required for Afwijzen/MeerInfoOpvragen (becomes
/// the published status's Reden), optional for Goedkeuren.</summary>
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>
@@ -270,23 +274,30 @@ public static class ApplicationStore
}
}
/// <summary>Record a behandelaar's decision (WP-65b). The endpoint has already checked
/// <see cref="BigRegister.Domain.Beoordeling.BeoordelingRules.CanDecide"/> against the
/// freshly-read status before calling this — cross-owner like <see cref="DeleteAny"/>,
/// since a behandelaar decides on any citizen's case. Returns null only if the aanvraag
/// is gone (shouldn't happen — this runs right after the endpoint's own read found it).</summary>
public static Aanvraag? RecordBesluit(string id, Besluit besluit, string? toelichting)
public enum RecordBesluitOutcome { Ok, NotFound, Conflict }
/// <summary>Record a behandelaar's decision (WP-65b) — cross-owner like
/// <see cref="DeleteAny"/>, since a behandelaar decides on any citizen's case.
/// WP-68 (F2): the transition-legality check (<see cref="BeoordelingRules.CanDecide"/>)
/// 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
/// check before either wrote, letting the second silently overwrite a terminal decision.
/// </summary>
public static (RecordBesluitOutcome Outcome, Aanvraag? Aanvraag) RecordBesluit(string id, Besluit besluit, string? toelichting, DateTimeOffset now)
{
lock (_gate)
{
using var db = Db.Create();
var a = db.Applications.Find(id);
if (a is null) return null;
if (a is null) return (RecordBesluitOutcome.NotFound, null);
var current = a.StatusAt(now).Tag;
if (current is null || !BeoordelingRules.CanDecide(current.Value))
return (RecordBesluitOutcome.Conflict, null);
a.BesluitStatus = besluit;
a.BesluitToelichting = toelichting;
a.UpdatedAt = DateTimeOffset.UtcNow;
db.SaveChanges();
return a;
return (RecordBesluitOutcome.Ok, a);
}
}
}
@@ -1,4 +1,5 @@
using BigRegister.Api.Contracts;
using BigRegister.Domain.Applications;
using BigRegister.Domain.Authorization;
namespace BigRegister.Api.Data;
@@ -1,4 +1,5 @@
using BigRegister.Api.Contracts;
using BigRegister.Domain.Applications;
using BigRegister.Domain.Authorization;
namespace BigRegister.Api.Data;
@@ -0,0 +1,62 @@
namespace BigRegister.Domain.Applications;
/// <summary>
/// The post-submission aanvraag status lifecycle (ADR-0002, WP-63): Ingediend → In
/// behandeling → (Meer info gevraagd ⇄) → Goedgekeurd/Afgewezen. Concept (pre-submission,
/// the wizard draft) is deliberately NOT a member here — see <see cref="AanvraagStatus.Tag"/>,
/// which is null exactly when the aanvraag hasn't been submitted yet, instead of a sixth
/// "magic string" tag with no enum member to match it (WP-68 F3).
/// <see cref="Ingediend"/> is reserved: no endpoint sets it yet (there is no state between
/// "just submitted" and "in behandeling" in this POC) — kept because the FE's status union
/// and $localize catalogue already declare it, and removing it would ripple into both.
/// </summary>
public enum AanvraagStatusTag { Ingediend, InBehandeling, MeerInfoGevraagd, Goedgekeurd, Afgewezen }
/// <summary>
/// A behandelaar's recorded decision (WP-65b) — the three actions the beoordeling screen
/// offers, each advancing an aanvraag's <see cref="AanvraagStatus"/>.
/// </summary>
public enum Besluit { Goedkeuren, Afwijzen, MeerInfoOpvragen }
/// <summary>
/// The domain projection of an aanvraag's status at a point in time (WP-68 F3) — the type
/// <c>Aanvraag.StatusAt(now)</c> returns, replacing the logic that used to live directly in
/// <c>Contracts.Mappers.ToStatusDto</c>. Constructible only via the factories below, so a
/// caller can never build e.g. a Referentie-less Goedgekeurd. <see cref="Tag"/> is null only
/// for <see cref="Concept"/> — the one construction path that used to be a bare "Concept"
/// string with no corresponding <see cref="AanvraagStatusTag"/> member.
/// </summary>
public sealed class AanvraagStatus
{
public AanvraagStatusTag? Tag { get; }
public int? StepIndex { get; }
public int? StepCount { get; }
public string? Referentie { get; }
public bool? Manual { get; }
public string? Reden { get; }
private AanvraagStatus(AanvraagStatusTag? tag, int? stepIndex, int? stepCount, string? referentie, bool? manual, string? reden)
{
Tag = tag;
StepIndex = stepIndex;
StepCount = stepCount;
Referentie = referentie;
Manual = manual;
Reden = reden;
}
public static AanvraagStatus Concept(int stepIndex, int stepCount) =>
new(null, stepIndex, stepCount, null, null, null);
public static AanvraagStatus InBehandeling(string referentie, bool manual) =>
new(AanvraagStatusTag.InBehandeling, null, null, referentie, manual, null);
public static AanvraagStatus Goedgekeurd(string referentie) =>
new(AanvraagStatusTag.Goedgekeurd, null, null, referentie, null, null);
public static AanvraagStatus Afgewezen(string referentie, string? reden) =>
new(AanvraagStatusTag.Afgewezen, null, null, referentie, null, reden);
public static AanvraagStatus MeerInfoGevraagd(string referentie, string? reden) =>
new(AanvraagStatusTag.MeerInfoGevraagd, null, null, referentie, null, reden);
}
@@ -1,19 +1,26 @@
using BigRegister.Api.Data;
using BigRegister.Domain.Applications;
namespace BigRegister.Domain.Beoordeling;
/// <summary>
/// SERVER-OWNED rules for the behandelportal's case-treatment decision (WP-65). Read-side
/// today (<see cref="CanDecide"/> only, backing the beoordeling detail screen's decision
/// flag) the decision-recording rules (which besluit is legal, whether it needs a
/// toelichting) land alongside the mutation endpoint in this WP's second half.
/// SERVER-OWNED rules for the behandelportal's case-treatment decision (WP-65). Used from
/// both the beoordeling read side (<see cref="CanDecide"/> backs the `canBesluiten` decision
/// flag) and the besluit write side (the SAME `CanDecide` gates the mutation, and — since
/// WP-68 F2 — runs inside the write lock, so the two can never drift and a concurrent besluit
/// can't race past the check).
/// </summary>
public static class BeoordelingRules
{
/// A behandelaar may record a decision while the aanvraag is in an open, non-terminal
/// status. Concept never reaches here (the endpoint 404s it before calling this); a case
/// already `Goedgekeurd`/`Afgewezen` is final.
/// status. Concept never reaches here (a null <see cref="AanvraagStatus.Tag"/> is checked
/// separately by callers); a case already `Goedgekeurd`/`Afgewezen` is final.
public static bool CanDecide(AanvraagStatusTag current) =>
current is AanvraagStatusTag.Ingediend or AanvraagStatusTag.InBehandeling
or AanvraagStatusTag.MeerInfoGevraagd;
/// <summary>WP-68 F6: moved here from an inline check in the besluit endpoint. The
/// toelichting (behandelaar's explanation) is required for every besluit except an
/// approval — Afwijzen/MeerInfoOpvragen must justify why (becomes the published status's
/// Reden).</summary>
public static bool RequiresToelichting(Besluit besluit) => besluit != Besluit.Goedkeuren;
}
+21 -15
View File
@@ -4,6 +4,7 @@ using System.Text.Json;
using System.Text.Json.Serialization;
using BigRegister.Api.Contracts;
using BigRegister.Api.Data;
using BigRegister.Domain.Applications;
using BigRegister.Domain.Authorization;
using BigRegister.Domain.Beoordeling;
using BigRegister.Domain.Diplomas;
@@ -452,8 +453,10 @@ api.MapGet("/beoordeling/{id}", (string id, HttpContext ctx, IZaakSource zaken)
var docs = DocumentStore.ByIds(c.DocumentIds)
.Select(d => new BeoordelingDocumentDto(d.DocumentId, d.CategoryId, d.FileName)).ToList();
var masked = c with { Owner = MaskTail(c.Owner!, 3) };
var decisions = new BeoordelingDecisionsDto(
BeoordelingRules.CanDecide(Enum.Parse<AanvraagStatusTag>(c.Status.Tag)));
// WP-68 (F3): non-throwing — c.Status.Tag crosses the IZaakSource wire boundary, so an
// unrecognised tag degrades to "cannot decide" instead of a 500.
var canBesluiten = Enum.TryParse<AanvraagStatusTag>(c.Status.Tag, out var tag) && BeoordelingRules.CanDecide(tag);
var decisions = new BeoordelingDecisionsDto(canBesluiten);
return Results.Ok(new BeoordelingViewDto(masked, docs, decisions));
}))
.Produces<BeoordelingViewDto>()
@@ -464,14 +467,19 @@ api.MapGet("/beoordeling/{id}", (string id, HttpContext ctx, IZaakSource zaken)
// lifecycle. The local write runs against ApplicationStore directly (not the IZaakSource
// seam) — same reasoning as the GET above. The transition-legality check
// (BeoordelingRules.CanDecide) is the SAME function the GET's canBesluiten flag uses,
// so the two can never drift. WP-66: once the local decision has committed, IZaakSource
// also gets a chance to advance the ZGW-side zaak status — LocalZaakSource no-ops,
// OpenZaakZaakSource POSTs a new Statussen entry (see its RecordBesluit).
// so the two can never drift — and (WP-68 F2) it now runs inside ApplicationStore.RecordBesluit's
// write lock rather than here, so two concurrent besluiten can't both pass it before either
// writes. WP-66: once the local decision has committed, IZaakSource also gets a chance to
// advance the ZGW-side zaak status — LocalZaakSource no-ops, OpenZaakZaakSource POSTs a new
// Statussen entry (see its RecordBesluit).
api.MapPost("/beoordeling/{id}/besluit", (string id, RecordBesluitRequest req, HttpContext ctx, IZaakSource zaken) =>
Beoordelen(ctx, $"aanvraag/{id}/besluit", () =>
{
if (!Enum.TryParse<Besluit>(req.Besluit, out var besluit))
return Results.Problem(detail: $"Onbekend besluit '{req.Besluit}'.", statusCode: StatusCodes.Status400BadRequest);
// WP-68 (F6): moved to BeoordelingRules.RequiresToelichting — same rule, now unit-testable.
if (BeoordelingRules.RequiresToelichting(besluit) && string.IsNullOrWhiteSpace(req.Toelichting))
return Results.Problem(detail: "Toelichting is verplicht bij dit besluit.", statusCode: StatusCodes.Status400BadRequest);
var now = DateTimeOffset.UtcNow;
// Real bug fix (WP-66): `id` is the FE-facing case id from IZaakSource.ListCases — under
@@ -481,17 +489,15 @@ api.MapPost("/beoordeling/{id}/besluit", (string id, RecordBesluitRequest req, H
// (see ApplicationStore.GetByReferentie).
var c = zaken.ListCases(now).FirstOrDefault(x => x.Id == id);
var a = c?.Status.Referentie is { } referentie ? ApplicationStore.GetByReferentie(referentie) : null;
var statusTag = a?.ToStatusDto(now).Tag;
if (a is null || statusTag == "Concept") return Results.NotFound();
var current = Enum.Parse<AanvraagStatusTag>(statusTag!);
if (!BeoordelingRules.CanDecide(current))
if (a is null) return Results.NotFound();
var (outcome, updated) = ApplicationStore.RecordBesluit(a.Id, besluit, req.Toelichting, now);
if (outcome == ApplicationStore.RecordBesluitOutcome.NotFound) return Results.NotFound();
if (outcome == ApplicationStore.RecordBesluitOutcome.Conflict)
return Results.Problem(
detail: "Deze aanvraag staat geen besluit meer toe in de huidige status.",
statusCode: StatusCodes.Status409Conflict);
if (besluit != Besluit.Goedkeuren && string.IsNullOrWhiteSpace(req.Toelichting))
return Results.Problem(detail: "Toelichting is verplicht bij dit besluit.", statusCode: StatusCodes.Status400BadRequest);
var updated = ApplicationStore.RecordBesluit(a.Id, besluit, req.Toelichting)!;
app.Logger.LogInformation("aanvraag besluit id={Id} besluit={Besluit}", a.Id, besluit);
// WP-60: the local decision above already committed — a ZGW failure here is caught and
@@ -499,14 +505,14 @@ api.MapPost("/beoordeling/{id}/besluit", (string id, RecordBesluitRequest req, H
// and document-link writes.
try
{
zaken.RecordBesluit(updated, besluit, req.Toelichting, now, ctx.Caller());
zaken.RecordBesluit(updated!, besluit, req.Toelichting, now, ctx.Caller());
}
catch (Exception ex)
{
RecordZgwDivergence(ctx, a.Id, updated.Referentie ?? a.Id, ex);
RecordZgwDivergence(ctx, a.Id, updated!.Referentie ?? a.Id, ex);
}
return Results.Ok(new RecordBesluitResponse(updated.ToStatusDto(now)));
return Results.Ok(new RecordBesluitResponse(updated!.ToStatusDto(now)));
}))
.Produces<RecordBesluitResponse>()
.ProducesProblem(StatusCodes.Status400BadRequest)
@@ -2,6 +2,7 @@ using System.Text.Json;
using System.Text.Json.Serialization;
using BigRegister.Api.Contracts;
using BigRegister.Api.Data;
using BigRegister.Domain.Applications;
using BigRegister.Domain.Authorization;
namespace BigRegister.Api.Zgw;
@@ -1,5 +1,6 @@
using System.Text.Json.Serialization;
using BigRegister.Api.Contracts;
using BigRegister.Domain.Applications;
namespace BigRegister.Api.Zgw;
@@ -33,9 +34,9 @@ public static class ZgwZaakMapper
// ponytail: coarse status map — an open zaak (no einddatum) is In behandeling, a closed
// one is Goedgekeurd. Real fidelity (statustype/resultaat lookups) is a later slice; the
// Afgewezen path needs the resultaat resource. Enough to prove the seam end-to-end.
var status = z.Einddatum is null
? new AanvraagStatusDto("InBehandeling", Referentie: z.Identificatie, Manual: true)
: new AanvraagStatusDto("Goedgekeurd", Referentie: z.Identificatie);
var status = (z.Einddatum is null
? AanvraagStatus.InBehandeling(z.Identificatie, manual: true)
: AanvraagStatus.Goedgekeurd(z.Identificatie)).ToDto();
var created = Iso(z.Registratiedatum ?? z.Startdatum);
var updated = Iso(z.Einddatum ?? z.Registratiedatum ?? z.Startdatum);
@@ -59,5 +60,5 @@ public static class ZgwZaakMapper
/// <summary>Status for a zaak that was JUST created (WP-50) — always the open/InBehandeling
/// coarse status (no einddatum yet), same convention as <see cref="ToSummaryDto"/>.</summary>
public static AanvraagStatusDto ToCreatedStatusDto(string identificatie) =>
new("InBehandeling", Referentie: identificatie, Manual: true);
AanvraagStatus.InBehandeling(identificatie, manual: true).ToDto();
}
@@ -2,6 +2,7 @@ using System.Net;
using System.Net.Http.Json;
using BigRegister.Api.Contracts;
using BigRegister.Api.Data;
using BigRegister.Domain.Applications;
using Microsoft.AspNetCore.Mvc.Testing;
namespace BigRegister.Tests;
@@ -1,6 +1,7 @@
using System.Net.Http.Json;
using BigRegister.Api.Contracts;
using BigRegister.Api.Data;
using BigRegister.Domain.Applications;
using BigRegister.Domain.Authorization;
using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.AspNetCore.TestHost;
@@ -1,6 +1,7 @@
using System.Net;
using BigRegister.Api.Data;
using BigRegister.Api.Zgw;
using BigRegister.Domain.Applications;
using BigRegister.Domain.Authorization;
namespace BigRegister.Tests;
@@ -1,4 +1,5 @@
using BigRegister.Api.Data;
using BigRegister.Domain.Applications;
using BigRegister.Domain.Beoordeling;
using BigRegister.Domain.Diplomas;
using BigRegister.Domain.Documents;