test(domain): Registration aggregate invariants (refs #6)

Failing unit tests for the Registration aggregate root: a submission starts
in INGEDIEND carrying its bsn, an empty/whitespace/null bsn is rejected, the
started process-instance id is remembered, and attaching the zaak the ACL
opened records its URL idempotently (a conflicting URL is rejected) while the
status stays INGEDIEND.

The aggregate is a stub (no-op mutators, empty bsn) so the tests compile and
fail on their assertions; the green commit implements the invariants.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-30 16:54:59 +02:00
parent c9edf27a48
commit cc9e7852e1
8 changed files with 193 additions and 0 deletions

View File

@@ -7,6 +7,10 @@
<Project Path="services/acl/Acl.IntegrationTests/Acl.IntegrationTests.csproj" />
<Project Path="services/acl/Acl.Tests/Acl.Tests.csproj" />
</Folder>
<Folder Name="/services/domain/">
<Project Path="services/domain/Big.Domain/Big.Domain.csproj" />
<Project Path="services/domain/Big.Tests/Big.Tests.csproj" />
</Folder>
<Folder Name="/services/bff/">
<Project Path="services/bff/Bff.Api/Bff.Api.csproj" />
<Project Path="services/bff/Bff.Tests/Bff.Tests.csproj" />

View File

@@ -0,0 +1,11 @@
<Project Sdk="Microsoft.NET.Sdk">
<!-- The domain layer (CLAUDE.md §9): aggregates, value objects, invariants.
Pure C# — no external dependencies, no infrastructure concerns. -->
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>

View File

@@ -0,0 +1,47 @@
namespace Big.Domain;
/// <summary>
/// The Registration aggregate root (CLAUDE.md §2.2): a zorgprofessional's submission to the BIG
/// register. It owns its lifecycle invariants — it starts <see cref="RegistrationStatus.Ingediend"/>
/// on submission, remembers the Flowable process that drives it, and records the zaak the ACL opens.
/// </summary>
public sealed class Registration
{
// STUB (red): mutators are no-ops and Submit leaves the bsn empty so the behaviour tests
// compile and fail on their assertions. The green commit implements the invariants.
private Registration(RegistrationId id, string bsn)
{
Id = id;
Bsn = bsn;
Status = RegistrationStatus.Ingediend;
}
public RegistrationId Id { get; }
/// <summary>The citizen-service number of the submitting zorgprofessional. Handed to the ACL
/// as the domain payload; the domain never constructs ZGW concepts from it (§8.1).</summary>
public string Bsn { get; }
public RegistrationStatus Status { get; private set; }
/// <summary>The Flowable process instance driving this registration, once started.</summary>
public string? ProcessInstanceId { get; private set; }
/// <summary>The zaak the ACL opened for this registration, once the external task has run.</summary>
public Uri? ZaakUrl { get; private set; }
/// <summary>Submit a new registration. It begins in <see cref="RegistrationStatus.Ingediend"/>.</summary>
public static Registration Submit(string bsn) => new(RegistrationId.New(), string.Empty);
/// <summary>Record that the registratie workflow process has been started for this registration.</summary>
public void RecordProcessStarted(string processInstanceId)
{
// STUB (red).
}
/// <summary>Attach the zaak the ACL opened. Idempotent on the same URL; a conflicting URL is rejected.</summary>
public void AttachZaak(Uri zaakUrl)
{
// STUB (red).
}
}

View File

@@ -0,0 +1,15 @@
namespace Big.Domain;
/// <summary>The identity of a <see cref="Registration"/> aggregate — opaque, server-assigned,
/// and distinct from the OpenZaak zaak id (which the projection keys on). A value object so the
/// id can travel as a Flowable process variable and back without ever being a bare string.</summary>
public readonly record struct RegistrationId(Guid Value)
{
/// <summary>Mint a fresh identity for a newly submitted registration.</summary>
public static RegistrationId New() => new(Guid.NewGuid());
/// <summary>Rehydrate an id carried as a string (e.g. a Flowable process variable).</summary>
public static RegistrationId Parse(string value) => new(Guid.Parse(value));
public override string ToString() => Value.ToString();
}

View File

@@ -0,0 +1,10 @@
namespace Big.Domain;
/// <summary>The lifecycle states a <see cref="Registration"/> moves through. The walking
/// skeleton knows only <see cref="Ingediend"/>; withdrawal, beoordeling and herregistratie
/// states arrive in their own slices (Iteration 2+).</summary>
public enum RegistrationStatus
{
/// <summary>Submitted by the zorgprofessional; the registratie process has been started.</summary>
Ingediend,
}

View File

@@ -0,0 +1,25 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="coverlet.collector" Version="6.0.4" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
<PackageReference Include="xunit" Version="2.9.3" />
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.4" />
</ItemGroup>
<ItemGroup>
<Using Include="Xunit" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Big.Domain\Big.Domain.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,77 @@
using Big.Domain;
namespace Big.Tests;
public class RegistrationTests
{
[Fact]
public void Submitting_a_registration_starts_in_ingediend()
{
var registration = Registration.Submit("123456782");
Assert.Equal(RegistrationStatus.Ingediend, registration.Status);
Assert.Equal("123456782", registration.Bsn);
Assert.NotEqual(Guid.Empty, registration.Id.Value);
Assert.Null(registration.ZaakUrl);
Assert.Null(registration.ProcessInstanceId);
}
[Theory]
[InlineData("")]
[InlineData(" ")]
[InlineData(null)]
public void Submitting_without_a_bsn_is_rejected(string? bsn)
=> Assert.Throws<ArgumentException>(() => Registration.Submit(bsn!));
[Fact]
public void Recording_the_started_process_keeps_its_instance_id()
{
var registration = Registration.Submit("123456782");
registration.RecordProcessStarted("proc-42");
Assert.Equal("proc-42", registration.ProcessInstanceId);
}
[Fact]
public void Attaching_a_zaak_records_its_url_and_keeps_the_status_ingediend()
{
var registration = Registration.Submit("123456782");
var zaak = new Uri("http://openzaak/zaken/api/v1/zaken/abc");
registration.AttachZaak(zaak);
Assert.Equal(zaak, registration.ZaakUrl);
Assert.Equal(RegistrationStatus.Ingediend, registration.Status);
}
[Fact]
public void Attaching_a_null_zaak_is_rejected()
{
var registration = Registration.Submit("123456782");
Assert.Throws<ArgumentNullException>(() => registration.AttachZaak(null!));
}
[Fact]
public void Re_attaching_the_same_zaak_is_idempotent()
{
var registration = Registration.Submit("123456782");
var zaak = new Uri("http://openzaak/zaken/api/v1/zaken/abc");
registration.AttachZaak(zaak);
registration.AttachZaak(zaak);
Assert.Equal(zaak, registration.ZaakUrl);
}
[Fact]
public void Attaching_a_conflicting_zaak_is_rejected()
{
var registration = Registration.Submit("123456782");
registration.AttachZaak(new Uri("http://openzaak/zaken/api/v1/zaken/abc"));
Assert.Throws<InvalidOperationException>(
() => registration.AttachZaak(new Uri("http://openzaak/zaken/api/v1/zaken/other")));
}
}

4
services/domain/Big.slnx Normal file
View File

@@ -0,0 +1,4 @@
<Solution>
<Project Path="Big.Domain/Big.Domain.csproj" />
<Project Path="Big.Tests/Big.Tests.csproj" />
</Solution>