diff --git a/paket.dependencies b/paket.dependencies index 445535c2..8d364ef4 100755 --- a/paket.dependencies +++ b/paket.dependencies @@ -64,13 +64,13 @@ nuget Be.Vlaanderen.Basisregisters.Projector 15.0.0 nuget Be.Vlaanderen.Basisregisters.Crab 4.0.0 -nuget Be.Vlaanderen.Basisregisters.GrAr.Common 21.0.0 -nuget Be.Vlaanderen.Basisregisters.GrAr.Contracts 21.0.0 -nuget Be.Vlaanderen.Basisregisters.GrAr.Import 21.0.0 -nuget Be.Vlaanderen.Basisregisters.GrAr.Legacy 21.0.0 -nuget Be.Vlaanderen.Basisregisters.GrAr.Oslo 21.0.0 -nuget Be.Vlaanderen.Basisregisters.GrAr.Provenance 21.0.0 -nuget Be.Vlaanderen.Basisregisters.GrAr.Extracts 21.0.0 +nuget Be.Vlaanderen.Basisregisters.GrAr.Common 21.10.0 +nuget Be.Vlaanderen.Basisregisters.GrAr.Contracts 21.10.0 +nuget Be.Vlaanderen.Basisregisters.GrAr.Import 21.10.0 +nuget Be.Vlaanderen.Basisregisters.GrAr.Legacy 21.10.0 +nuget Be.Vlaanderen.Basisregisters.GrAr.Oslo 21.10.0 +nuget Be.Vlaanderen.Basisregisters.GrAr.Provenance 21.10.0 +nuget Be.Vlaanderen.Basisregisters.GrAr.Extracts 21.10.0 nuget Be.Vlaanderen.Basisregisters.MessageHandling.Kafka.Producer 5.0.1 diff --git a/paket.lock b/paket.lock index b2913f53..0cdc2ff9 100644 --- a/paket.lock +++ b/paket.lock @@ -217,18 +217,16 @@ NUGET Autofac.Extensions.DependencyInjection (>= 9.0) Be.Vlaanderen.Basisregisters.EventHandling (5.0) Be.Vlaanderen.Basisregisters.Generators.Guid.Deterministic (4.0) - Be.Vlaanderen.Basisregisters.GrAr.Common (21.0) + Be.Vlaanderen.Basisregisters.GrAr.Common (21.10) Be.Vlaanderen.Basisregisters.AggregateSource (>= 9.0.1) Be.Vlaanderen.Basisregisters.CommandHandling (>= 9.0.1) NetTopologySuite (>= 2.5) NodaTime (>= 3.1.11) - Be.Vlaanderen.Basisregisters.GrAr.Contracts (21.0) - Be.Vlaanderen.Basisregisters.AggregateSource (>= 9.0.1) - NodaTime (>= 3.1.11) - Be.Vlaanderen.Basisregisters.GrAr.Extracts (21.0) + Be.Vlaanderen.Basisregisters.GrAr.Contracts (21.10) + Be.Vlaanderen.Basisregisters.GrAr.Extracts (21.10) Be.Vlaanderen.Basisregisters.Api (>= 21.0) Be.Vlaanderen.Basisregisters.Shaperon (>= 10.0.2) - Be.Vlaanderen.Basisregisters.GrAr.Import (21.0) + Be.Vlaanderen.Basisregisters.GrAr.Import (21.10) Autofac (>= 8.0) Be.Vlaanderen.Basisregisters.AggregateSource.SqlStreamStore (>= 9.0.1) Be.Vlaanderen.Basisregisters.CommandHandling (>= 9.0.1) @@ -243,21 +241,21 @@ NUGET Serilog (>= 3.1.1) Serilog.Extensions.Logging (>= 8.0) System.Threading.Tasks.Dataflow (>= 8.0) - Be.Vlaanderen.Basisregisters.GrAr.Legacy (21.0) - Be.Vlaanderen.Basisregisters.GrAr.Common (21.0) + Be.Vlaanderen.Basisregisters.GrAr.Legacy (21.10) + Be.Vlaanderen.Basisregisters.GrAr.Common (21.10) Be.Vlaanderen.Basisregisters.Utilities.Rfc3339DateTimeOffset (>= 4.0) Newtonsoft.Json (>= 13.0.3) - Be.Vlaanderen.Basisregisters.GrAr.Oslo (21.0) + Be.Vlaanderen.Basisregisters.GrAr.Oslo (21.10) Be.Vlaanderen.Basisregisters.AspNetCore.Mvc.Formatters.Json (>= 5.0) - Be.Vlaanderen.Basisregisters.GrAr.Common (21.0) + Be.Vlaanderen.Basisregisters.GrAr.Common (21.10) Be.Vlaanderen.Basisregisters.Utilities.Rfc3339DateTimeOffset (>= 4.0) Microsoft.Extensions.Configuration (>= 8.0) Microsoft.Extensions.Http.Polly (>= 8.0.3) Newtonsoft.Json (>= 13.0.3) - Be.Vlaanderen.Basisregisters.GrAr.Provenance (21.0) + Be.Vlaanderen.Basisregisters.GrAr.Provenance (21.10) Be.Vlaanderen.Basisregisters.CommandHandling (>= 9.0.1) Be.Vlaanderen.Basisregisters.Crab (>= 4.0) - Be.Vlaanderen.Basisregisters.GrAr.Common (21.0) + Be.Vlaanderen.Basisregisters.GrAr.Common (21.10) Microsoft.CSharp (>= 4.7) Be.Vlaanderen.Basisregisters.MessageHandling.Kafka.Producer (5.0.1) Confluent.Kafka (>= 2.3) diff --git a/src/PostalRegistry.Api.Import/Infrastructure/Modules/ApiModule.cs b/src/PostalRegistry.Api.Import/Infrastructure/Modules/ApiModule.cs index 0970ba95..87529e78 100644 --- a/src/PostalRegistry.Api.Import/Infrastructure/Modules/ApiModule.cs +++ b/src/PostalRegistry.Api.Import/Infrastructure/Modules/ApiModule.cs @@ -44,6 +44,11 @@ protected override void Load(ContainerBuilder builder) new IdempotencyTableInfo(Schema.Import), _loggerFactory); + builder.RegisterType() + .As() + .AsSelf() + .InstancePerLifetimeScope(); + builder .RegisterType() .AsSelf(); diff --git a/src/PostalRegistry.Api.Import/PostalInformationController-RelinkMunicipality.cs b/src/PostalRegistry.Api.Import/PostalInformationController-RelinkMunicipality.cs new file mode 100644 index 00000000..62fa04dd --- /dev/null +++ b/src/PostalRegistry.Api.Import/PostalInformationController-RelinkMunicipality.cs @@ -0,0 +1,49 @@ +namespace PostalRegistry.Api.Import +{ + using System.Collections.Generic; + using System.Threading; + using System.Threading.Tasks; + using Be.Vlaanderen.Basisregisters.AggregateSource; + using Be.Vlaanderen.Basisregisters.Api.Exceptions; + using Be.Vlaanderen.Basisregisters.CommandHandling.Idempotency; + using FluentValidation; + using Microsoft.AspNetCore.Http; + using Microsoft.AspNetCore.Mvc; + using PostalInformation.Commands; + using Relink; + + public sealed partial class PostalInformationController + { + [HttpPost("{postcode}/relink-municipality")] + public async Task RelinkMunicipality( + [FromRoute(Name = "postcode")]string? postalCode, + [FromBody] RelinkMunicipalityRequest request, + [FromServices] IValidator validator, + [FromServices] IIdempotentCommandHandler idempotentCommandHandler, + CancellationToken cancellationToken = default) + { + request.PostalCode = postalCode; + await validator.ValidateAndThrowAsync(request, cancellationToken: cancellationToken); + + try + { + var command = new RelinkMunicipality( + new PostalCode(postalCode!) + , new NisCode(request.NewNisCode!) + , CreateProvenance(request.Reason ?? string.Empty)); + + await idempotentCommandHandler.Dispatch( + command.CreateCommandId(), + command, + new Dictionary(), + cancellationToken); + + return Ok(); + } + catch (AggregateNotFoundException) + { + throw new ApiException("Onbestaande postcode", StatusCodes.Status404NotFound); + } + } + } +} diff --git a/src/PostalRegistry.Api.Import/PostalInformationController.cs b/src/PostalRegistry.Api.Import/PostalInformationController.cs new file mode 100644 index 00000000..b24e8a30 --- /dev/null +++ b/src/PostalRegistry.Api.Import/PostalInformationController.cs @@ -0,0 +1,26 @@ +namespace PostalRegistry.Api.Import +{ + using Asp.Versioning; + using Be.Vlaanderen.Basisregisters.Api; + using Be.Vlaanderen.Basisregisters.GrAr.Provenance; + using Microsoft.AspNetCore.Mvc; + using NodaTime; + + [ApiVersion("1.0")] + [AdvertiseApiVersions("1.0")] + [ApiRoute("import")] + [ApiExplorerSettings(GroupName = "Import")] + public sealed partial class PostalInformationController : ApiController + { + private Provenance CreateProvenance(string reason, Modification modification = Modification.Update) + { + return new Provenance( + SystemClock.Instance.GetCurrentInstant(), + Application.PostalRegistry, + new Reason(reason), + new Operator("OVO002949"), + modification, + Organisation.DigitaalVlaanderen); + } + } +} diff --git a/src/PostalRegistry.Api.Import/PostalRegistry.Api.Import.csproj.DotSettings b/src/PostalRegistry.Api.Import/PostalRegistry.Api.Import.csproj.DotSettings deleted file mode 100644 index de1e8b04..00000000 --- a/src/PostalRegistry.Api.Import/PostalRegistry.Api.Import.csproj.DotSettings +++ /dev/null @@ -1,3 +0,0 @@ - - CSharp71 - diff --git a/src/PostalRegistry.Api.Import/Relink/RelinkMunicipalityRequest.cs b/src/PostalRegistry.Api.Import/Relink/RelinkMunicipalityRequest.cs new file mode 100644 index 00000000..e188c4fe --- /dev/null +++ b/src/PostalRegistry.Api.Import/Relink/RelinkMunicipalityRequest.cs @@ -0,0 +1,10 @@ +namespace PostalRegistry.Api.Import.Relink +{ + public sealed class RelinkMunicipalityRequest + { + public string? PostalCode { get; set; } + public string? NewNisCode { get; set; } + + public string? Reason { get; set; } + } +} diff --git a/src/PostalRegistry.Api.Import/Relink/RelinkMunicipalityRequestValidator.cs b/src/PostalRegistry.Api.Import/Relink/RelinkMunicipalityRequestValidator.cs new file mode 100644 index 00000000..f3f2915d --- /dev/null +++ b/src/PostalRegistry.Api.Import/Relink/RelinkMunicipalityRequestValidator.cs @@ -0,0 +1,17 @@ +namespace PostalRegistry.Api.Import.Relink +{ + using FluentValidation; + + public sealed class RelinkMunicipalityRequestValidator : AbstractValidator + { + public RelinkMunicipalityRequestValidator() + { + RuleFor(request => request.PostalCode) + .NotEmpty(); + + RuleFor(request => request.NewNisCode) + .NotEmpty() + .Length(5); + } + } +} diff --git a/src/PostalRegistry.Producer.Snapshot.Oslo/ProducerProjections.cs b/src/PostalRegistry.Producer.Snapshot.Oslo/ProducerProjections.cs index 3129f1bc..2af94c8b 100644 --- a/src/PostalRegistry.Producer.Snapshot.Oslo/ProducerProjections.cs +++ b/src/PostalRegistry.Producer.Snapshot.Oslo/ProducerProjections.cs @@ -106,6 +106,20 @@ await snapshotManager.FindMatchingSnapshot( message.Position, ct); }); + + When>(async (_, message, ct) => + { + await FindAndProduce(async () => + await snapshotManager.FindMatchingSnapshot( + message.Message.PostalCode, + message.Message.Provenance.Timestamp, + null, + message.Position, + throwStaleWhenGone: false, + ct), + message.Position, + ct); + }); } private async Task FindAndProduce( diff --git a/src/PostalRegistry.Producer/Extensions/MessageExtensions.cs b/src/PostalRegistry.Producer/Extensions/MessageExtensions.cs index 9a804d3c..0e299085 100644 --- a/src/PostalRegistry.Producer/Extensions/MessageExtensions.cs +++ b/src/PostalRegistry.Producer/Extensions/MessageExtensions.cs @@ -31,5 +31,8 @@ public static Contracts.PostalInformationPostalNameWasRemoved ToContract(this Do public static Contracts.MunicipalityWasAttached ToContract(this Domain.MunicipalityWasAttached message) => new Contracts.MunicipalityWasAttached(message.PostalCode, message.NisCode, message.Provenance.ToContract()); + + public static Contracts.MunicipalityWasRelinked ToContract(this Domain.MunicipalityWasRelinked message) => + new Contracts.MunicipalityWasRelinked(message.PostalCode, message.NewNisCode, message.PreviousNisCode, message.Provenance.ToContract()); } } diff --git a/src/PostalRegistry.Producer/ProducerProjections.cs b/src/PostalRegistry.Producer/ProducerProjections.cs index 34d37ad7..1e25145f 100644 --- a/src/PostalRegistry.Producer/ProducerProjections.cs +++ b/src/PostalRegistry.Producer/ProducerProjections.cs @@ -52,6 +52,11 @@ public ProducerProjections(IProducer producer) { await Produce(message.Message.PostalCode, message.Message.ToContract(), message.Position, ct); }); + + When>(async (_, message, ct) => + { + await Produce(message.Message.PostalCode, message.Message.ToContract(), message.Position, ct); + }); } private async Task Produce( diff --git a/src/PostalRegistry.Projections.Integration/PostalLatestItemProjections.cs b/src/PostalRegistry.Projections.Integration/PostalLatestItemProjections.cs index aa4f6156..77a0046c 100644 --- a/src/PostalRegistry.Projections.Integration/PostalLatestItemProjections.cs +++ b/src/PostalRegistry.Projections.Integration/PostalLatestItemProjections.cs @@ -43,6 +43,18 @@ await context.FindAndUpdatePostal( ct); }); + When>(async (context, message, ct) => + { + await context.FindAndUpdatePostal( + message.Message.PostalCode, + postalInformation => + { + postalInformation.NisCode = message.Message.NewNisCode; + UpdateVersionTimestamp(postalInformation, message.Message.Provenance.Timestamp); + }, + ct); + }); + When>(async (context, message, ct) => { await context.FindAndUpdatePostal( diff --git a/src/PostalRegistry.Projections.LastChangedList/LastChangedListProjections.cs b/src/PostalRegistry.Projections.LastChangedList/LastChangedListProjections.cs index e5202d97..00d6412a 100644 --- a/src/PostalRegistry.Projections.LastChangedList/LastChangedListProjections.cs +++ b/src/PostalRegistry.Projections.LastChangedList/LastChangedListProjections.cs @@ -50,7 +50,16 @@ public LastChangedListProjections() await GetLastChangedRecordsAndUpdatePosition(message.Message.PostalCode, message.Position, context, ct); }); - When>(async (context, message, ct) => await DoNothing()); + When>(async (context, message, ct) => + { + await GetLastChangedRecordsAndUpdatePosition(message.Message.PostalCode, message.Position, context, ct); + }); + + When>(async (context, message, ct) => + { + await GetLastChangedRecordsAndUpdatePosition(message.Message.PostalCode, message.Position, context, ct); + }); + When>(async (context, message, ct) => await DoNothing()); When>(async (context, message, ct) => await DoNothing()); } diff --git a/src/PostalRegistry.Projections.Legacy/PostalInformation/PostalInformationProjections.cs b/src/PostalRegistry.Projections.Legacy/PostalInformation/PostalInformationProjections.cs index de57d2d5..12026fc0 100755 --- a/src/PostalRegistry.Projections.Legacy/PostalInformation/PostalInformationProjections.cs +++ b/src/PostalRegistry.Projections.Legacy/PostalInformation/PostalInformationProjections.cs @@ -41,6 +41,18 @@ await context.FindAndUpdatePostalInformation( ct); }); + When>(async (context, message, ct) => + { + await context.FindAndUpdatePostalInformation( + message.Message.PostalCode, + postalInformation => + { + postalInformation.NisCode = message.Message.NewNisCode; + UpdateVersionTimestamp(postalInformation, message.Message.Provenance.Timestamp); + }, + ct); + }); + When>(async (context, message, ct) => { await context.FindAndUpdatePostalInformation( diff --git a/src/PostalRegistry.Projections.Legacy/PostalInformationSyndication/PostalInformationSyndicationProjections.cs b/src/PostalRegistry.Projections.Legacy/PostalInformationSyndication/PostalInformationSyndicationProjections.cs index e437be8d..2ea02746 100755 --- a/src/PostalRegistry.Projections.Legacy/PostalInformationSyndication/PostalInformationSyndicationProjections.cs +++ b/src/PostalRegistry.Projections.Legacy/PostalInformationSyndication/PostalInformationSyndicationProjections.cs @@ -84,6 +84,15 @@ await context.CreateNewPostalInformationSyndicationItem( ct); }); + When>(async (context, message, ct) => + { + await context.CreateNewPostalInformationSyndicationItem( + message.Message.PostalCode, + message, + x => x.MunicipalityNisCode = message.Message.NewNisCode, + ct); + }); + When>(async (context, message, ct) => await DoNothing()); When>(async (context, message, ct) => await DoNothing()); } diff --git a/src/PostalRegistry.Projections.Syndication/Municipality/MunicipalityEvent.cs b/src/PostalRegistry.Projections.Syndication/Municipality/MunicipalityEvent.cs index c7deb8e4..aa24806e 100644 --- a/src/PostalRegistry.Projections.Syndication/Municipality/MunicipalityEvent.cs +++ b/src/PostalRegistry.Projections.Syndication/Municipality/MunicipalityEvent.cs @@ -15,6 +15,7 @@ public enum MunicipalityEvent MunicipalityFacilityLanguageWasRemoved, MunicipalityWasDrawn, + MunicipalityWasMerged, MunicipalityGeometryWasCleared, MunicipalityGeometryWasCorrected, MunicipalityGeometryWasCorrectedToCleared, diff --git a/src/PostalRegistry.Projections.Syndication/Municipality/MunicipalityLatestProjections.cs b/src/PostalRegistry.Projections.Syndication/Municipality/MunicipalityLatestProjections.cs index adfa29f1..80fb3df2 100755 --- a/src/PostalRegistry.Projections.Syndication/Municipality/MunicipalityLatestProjections.cs +++ b/src/PostalRegistry.Projections.Syndication/Municipality/MunicipalityLatestProjections.cs @@ -32,6 +32,7 @@ public MunicipalityLatestProjections() When(MunicipalityEvent.MunicipalityGeometryWasCorrectedToCleared, DoNothing); When(MunicipalityEvent.MunicipalityBecameCurrent, DoNothing); When(MunicipalityEvent.MunicipalityWasRetired, DoNothing); + When(MunicipalityEvent.MunicipalityWasMerged, DoNothing); When(MunicipalityEvent.MunicipalityWasCorrectedToCurrent, DoNothing); When(MunicipalityEvent.MunicipalityWasCorrectedToRetired, DoNothing); } diff --git a/src/PostalRegistry/CommandHandlerModules.cs b/src/PostalRegistry/CommandHandlerModules.cs index 10555eba..a2b2d6e8 100755 --- a/src/PostalRegistry/CommandHandlerModules.cs +++ b/src/PostalRegistry/CommandHandlerModules.cs @@ -2,6 +2,7 @@ namespace PostalRegistry { using Autofac; using Be.Vlaanderen.Basisregisters.CommandHandling; + using Be.Vlaanderen.Basisregisters.GrAr.Provenance; using PostalInformation; public static class CommandHandlerModules @@ -16,6 +17,12 @@ public static void Register(ContainerBuilder containerBuilder) .RegisterType() .SingleInstance(); + containerBuilder + .RegisterType() + .As>() + .AsSelf() + .SingleInstance(); + containerBuilder .RegisterType() .Named(typeof(PostalInformationCommandHandlerModule).FullName) diff --git a/src/PostalRegistry/PostalInformation/Commands/RelinkMunicipality.cs b/src/PostalRegistry/PostalInformation/Commands/RelinkMunicipality.cs new file mode 100644 index 00000000..150baf64 --- /dev/null +++ b/src/PostalRegistry/PostalInformation/Commands/RelinkMunicipality.cs @@ -0,0 +1,41 @@ +namespace PostalRegistry.PostalInformation.Commands +{ + using System; + using System.Collections.Generic; + using Be.Vlaanderen.Basisregisters.Generators.Guid; + using Be.Vlaanderen.Basisregisters.GrAr.Provenance; + using Be.Vlaanderen.Basisregisters.Utilities; + + public sealed class RelinkMunicipality : IHasCommandProvenance + { + private static readonly Guid Namespace = new Guid("461f480c-75d9-4637-b98e-5fd66682d338"); + + public PostalCode PostalCode { get; } + public NisCode NewNisCode { get; } + public Provenance Provenance { get; } + + public RelinkMunicipality(PostalCode postalCode, NisCode newNisCode, Provenance provenance) + { + PostalCode = postalCode; + NewNisCode = newNisCode; + Provenance = provenance; + } + + public Guid CreateCommandId() + => Deterministic.Create(Namespace, $"RelinkMunicipality-{ToString()}"); + + public override string? ToString() + => ToStringBuilder.ToString(IdentityFields()); + + private IEnumerable IdentityFields() + { + yield return PostalCode; + yield return NewNisCode; + + foreach (var field in Provenance.GetIdentityFields()) + { + yield return field; + } + } + } +} diff --git a/src/PostalRegistry/PostalInformation/Events/MunicipalityWasRelinked.cs b/src/PostalRegistry/PostalInformation/Events/MunicipalityWasRelinked.cs new file mode 100644 index 00000000..66371173 --- /dev/null +++ b/src/PostalRegistry/PostalInformation/Events/MunicipalityWasRelinked.cs @@ -0,0 +1,47 @@ +namespace PostalRegistry.PostalInformation.Events +{ + using Be.Vlaanderen.Basisregisters.EventHandling; + using Be.Vlaanderen.Basisregisters.GrAr.Provenance; + using Newtonsoft.Json; + + [EventTags(EventTag.For.Sync)] + [EventName("MunicipalityWasRelinked")] + [EventDescription("Het PostInfo-object werd herkoppeld aan een andere gemeente.")] + public class MunicipalityWasRelinked : IHasProvenance, ISetProvenance, IMessage + { + [EventPropertyDescription("Postcode (= objectidentificator) van het PostInfo-object.")] + public string PostalCode { get; } + + [EventPropertyDescription("NIS-code (= objectidentificator) van de nieuwe gemeente aan dewelke het PostInfo-object is toegewezen.")] + public string NewNisCode { get; } + + [EventPropertyDescription("NIS-code (= objectidentificator) van de vorige gemeente aan dewelke het PostInfo-object was toegewezen.")] + public string PreviousNisCode { get; } + + [EventPropertyDescription("Metadata bij het event.")] + public ProvenanceData Provenance { get; private set; } + + public MunicipalityWasRelinked( + PostalCode postalCode, + NisCode newNisCode, + NisCode previousNisCode) + { + PostalCode = postalCode; + NewNisCode = newNisCode; + PreviousNisCode = previousNisCode; + } + + [JsonConstructor] + private MunicipalityWasRelinked( + string postalCode, + string newNisCode, + string previousNisCode, + ProvenanceData provenance) : + this( + new PostalCode(postalCode), + new NisCode(newNisCode), + new NisCode(previousNisCode)) => ((ISetProvenance)this).SetProvenance(provenance.ToProvenance()); + + void ISetProvenance.SetProvenance(Provenance provenance) => Provenance = new ProvenanceData(provenance); + } +} diff --git a/src/PostalRegistry/PostalInformation/Exceptions/InvalidNisCodeException.cs b/src/PostalRegistry/PostalInformation/Exceptions/InvalidNisCodeException.cs new file mode 100644 index 00000000..0bdbe04e --- /dev/null +++ b/src/PostalRegistry/PostalInformation/Exceptions/InvalidNisCodeException.cs @@ -0,0 +1,20 @@ +namespace PostalRegistry.PostalInformation.Exceptions +{ + using System; + using System.Runtime.Serialization; + + [Serializable] + public sealed class InvalidNisCodeException : PostalRegistryException + { + public NisCode? NewNisCode { get; } + + public InvalidNisCodeException(NisCode? newNisCode) + { + NewNisCode = newNisCode; + } + + private InvalidNisCodeException(SerializationInfo info, StreamingContext context) + : base(info, context) + { } + } +} diff --git a/src/PostalRegistry/PostalInformation/PostalInformation.cs b/src/PostalRegistry/PostalInformation/PostalInformation.cs index 41175c00..c2e7ea47 100755 --- a/src/PostalRegistry/PostalInformation/PostalInformation.cs +++ b/src/PostalRegistry/PostalInformation/PostalInformation.cs @@ -9,6 +9,7 @@ namespace PostalRegistry.PostalInformation using Events; using Events.BPost; using Events.Crab; + using Exceptions; public partial class PostalInformation : AggregateRootEntity { @@ -78,5 +79,19 @@ public void ImportPostalInformationFromCrab( modification, organisation)); } + + public void RelinkMunicipality(NisCode newNisCode) + { + if (newNisCode is null) + throw new InvalidNisCodeException(newNisCode); + + if(NisCode is null) + ApplyChange(new MunicipalityWasAttached(PostalCode, newNisCode)); + + if(newNisCode == NisCode!) + return; + + ApplyChange(new MunicipalityWasRelinked(PostalCode, newNisCode, NisCode!)); + } } } diff --git a/src/PostalRegistry/PostalInformation/PostalInformationCommandHandlerModule.cs b/src/PostalRegistry/PostalInformation/PostalInformationCommandHandlerModule.cs index eadfbd45..1989ae06 100755 --- a/src/PostalRegistry/PostalInformation/PostalInformationCommandHandlerModule.cs +++ b/src/PostalRegistry/PostalInformation/PostalInformationCommandHandlerModule.cs @@ -6,6 +6,7 @@ namespace PostalRegistry.PostalInformation using Be.Vlaanderen.Basisregisters.CommandHandling.SqlStreamStore; using Be.Vlaanderen.Basisregisters.EventHandling; using Be.Vlaanderen.Basisregisters.GrAr.Provenance; + using Commands; using Commands.BPost; using Commands.Crab; using SqlStreamStore; @@ -19,7 +20,8 @@ public PostalInformationCommandHandlerModule( EventMapping eventMapping, EventSerializer eventSerializer, BPostPostalInformationProvenanceFactory bpostProvenanceFactory, - CrabPostalInformationProvenanceFactory crabProvenanceFactory) + CrabPostalInformationProvenanceFactory crabProvenanceFactory, + PostalInformationProvenanceFactory postalInformationProvenanceFactory) { For() .AddSqlStreamStore(getStreamStore, getUnitOfWork, eventMapping, eventSerializer) @@ -47,27 +49,38 @@ public PostalInformationCommandHandlerModule( For() .AddSqlStreamStore(getStreamStore, getUnitOfWork, eventMapping, eventSerializer) .AddProvenance(getUnitOfWork, crabProvenanceFactory) - .Handle(async (mesage, ct) => + .Handle(async (message, ct) => { // need to use the subcanton => in crab postcode = 1030, subcanton = 1031 // in bpost postcode = 1031 - var postalCode = new PostalCode(mesage.Command.SubCantonCode); + var postalCode = new PostalCode(message.Command.SubCantonCode); var postalInformation = await getPostalInformationSet().GetOptionalAsync(postalCode, ct); if (!postalInformation.HasValue) // Crab has possible outdated postalcodes return; postalInformation.Value.ImportPostalInformationFromCrab( - mesage.Command.PostalCode, - mesage.Command.SubCantonId, - mesage.Command.SubCantonCode, - mesage.Command.NisCode, - mesage.Command.MunicipalityName, - mesage.Command.Lifetime, - mesage.Command.Timestamp, - mesage.Command.Operator, - mesage.Command.Modification, - mesage.Command.Organisation); + message.Command.PostalCode, + message.Command.SubCantonId, + message.Command.SubCantonCode, + message.Command.NisCode, + message.Command.MunicipalityName, + message.Command.Lifetime, + message.Command.Timestamp, + message.Command.Operator, + message.Command.Modification, + message.Command.Organisation); + }); + + For() + .AddSqlStreamStore(getStreamStore, getUnitOfWork, eventMapping, eventSerializer) + .AddProvenance(getUnitOfWork, postalInformationProvenanceFactory) + .Handle(async (message, ct) => + { + var postalCode = new PostalCode(message.Command.PostalCode); + var postalInformation = await getPostalInformationSet().GetAsync(postalCode, ct); + + postalInformation.RelinkMunicipality(message.Command.NewNisCode); }); } } diff --git a/src/PostalRegistry/PostalInformation/PostalInformationProvenanceFactory.cs b/src/PostalRegistry/PostalInformation/PostalInformationProvenanceFactory.cs new file mode 100644 index 00000000..18e29e4e --- /dev/null +++ b/src/PostalRegistry/PostalInformation/PostalInformationProvenanceFactory.cs @@ -0,0 +1,26 @@ +namespace PostalRegistry.PostalInformation +{ + using System; + using Be.Vlaanderen.Basisregisters.GrAr.Provenance; + using NodaTime; + + public class PostalInformationProvenanceFactory : IProvenanceFactory + { + public bool CanCreateFrom() => typeof(IHasProvenance).IsAssignableFrom(typeof(TCommand)); + public Provenance CreateFrom(object provenanceHolder, PostalInformation aggregate) + { + if (provenanceHolder is not IHasCommandProvenance provenance) + { + throw new InvalidOperationException($"Cannot create provenance from {provenanceHolder.GetType().Name}"); + } + + return new Provenance( + SystemClock.Instance.GetCurrentInstant(), + provenance.Provenance.Application, + provenance.Provenance.Reason, + provenance.Provenance.Operator, + provenance.Provenance.Modification, + provenance.Provenance.Organisation); + } + } +} diff --git a/src/PostalRegistry/PostalInformation/PostalInformationState.cs b/src/PostalRegistry/PostalInformation/PostalInformationState.cs index 9771dd1f..ddec7b12 100755 --- a/src/PostalRegistry/PostalInformation/PostalInformationState.cs +++ b/src/PostalRegistry/PostalInformation/PostalInformationState.cs @@ -8,11 +8,12 @@ namespace PostalRegistry.PostalInformation public partial class PostalInformation { - public PostalCode? PostalCode { get; private set; } + public PostalCode PostalCode { get; private set; } private PostalInformationStatus? _status; private readonly List _postalNames = new List(); + public NisCode? NisCode { get; set; } public Modification LastModification { get; private set; } public PostalInformation() @@ -23,6 +24,7 @@ public PostalInformation() Register(When); Register(When); Register(When); + Register(When); Register(@event => WhenCrabEventApplied()); Register(@event => WhenCrabEventApplied()); @@ -69,7 +71,12 @@ private void When(PostalInformationWasRegistered @event) private void When(MunicipalityWasAttached @event) { - _ = new NisCode(@event.NisCode); + NisCode = new NisCode(@event.NisCode); + } + + private void When(MunicipalityWasRelinked @event) + { + NisCode = new NisCode(@event.NewNisCode); } } } diff --git a/test/PostalRegistry.Tests/AggregateTests/WhenRelinkingMunicipality/GivenNoPostalInformation.cs b/test/PostalRegistry.Tests/AggregateTests/WhenRelinkingMunicipality/GivenNoPostalInformation.cs new file mode 100644 index 00000000..adfae0f9 --- /dev/null +++ b/test/PostalRegistry.Tests/AggregateTests/WhenRelinkingMunicipality/GivenNoPostalInformation.cs @@ -0,0 +1,35 @@ +namespace PostalRegistry.Tests.AggregateTests.WhenRelinkingMunicipality +{ + using AutoFixture; + using Be.Vlaanderen.Basisregisters.AggregateSource; + using Be.Vlaanderen.Basisregisters.AggregateSource.Testing; + using global::AutoFixture; + using PostalInformation; + using PostalInformation.Commands; + using Xunit; + using Xunit.Abstractions; + + public class GivenNoPostalInformation : PostalRegistryTest + { + private readonly Fixture _fixture; + + public GivenNoPostalInformation(ITestOutputHelper testOutputHelper) : base(testOutputHelper) + { + _fixture = new Fixture(); + _fixture.Customize(new WithFixedPostalCode()); + _fixture.Customize(new WithIntegerNisCode()); + } + + [Fact] + public void ThenAggregateNotFoundExceptionIsThrown() + { + var command = _fixture.Create(); + + Assert( + new Scenario() + .Given() + .When(command) + .Throws(new AggregateNotFoundException(command.PostalCode.ToString(), typeof(PostalInformation)))); + } + } +} diff --git a/test/PostalRegistry.Tests/AggregateTests/WhenRelinkingMunicipality/GivenPostalInformation.cs b/test/PostalRegistry.Tests/AggregateTests/WhenRelinkingMunicipality/GivenPostalInformation.cs new file mode 100644 index 00000000..21c6a50d --- /dev/null +++ b/test/PostalRegistry.Tests/AggregateTests/WhenRelinkingMunicipality/GivenPostalInformation.cs @@ -0,0 +1,111 @@ +namespace PostalRegistry.Tests.AggregateTests.WhenRelinkingMunicipality +{ + using AutoFixture; + using Be.Vlaanderen.Basisregisters.AggregateSource; + using Be.Vlaanderen.Basisregisters.AggregateSource.Testing; + using Be.Vlaanderen.Basisregisters.GrAr.Provenance; + using FluentAssertions; + using global::AutoFixture; + using PostalInformation; + using PostalInformation.Commands; + using PostalInformation.Events; + using PostalInformation.Exceptions; + using Xunit; + using Xunit.Abstractions; + + public class GivenPostalInformation : PostalRegistryTest + { + private readonly Fixture _fixture; + + public GivenPostalInformation(ITestOutputHelper testOutputHelper) : base(testOutputHelper) + { + _fixture = new Fixture(); + _fixture.Customize(new WithFixedPostalCode()); + _fixture.Customize(new WithIntegerNisCode()); + _fixture.Customize(new InfrastructureCustomization()); + } + + [Fact] + public void WithInvalidNewNisCode_ThenInvalidNisCodeExceptionIsThrown() + { + var command = new RelinkMunicipality(_fixture.Create(), null, _fixture.Create()); + + Assert( + new Scenario() + .Given( + command.PostalCode, + _fixture.Create()) + .When(command) + .Throws(new InvalidNisCodeException(null))); + } + + [Fact] + public void WithNoNisCodeAttached_ThenMunicipalityWasAttached() + { + var command = _fixture.Create(); + + Assert( + new Scenario() + .Given( + command.PostalCode, + _fixture.Create()) + .When(command) + .Then(new Fact(command.PostalCode, + new MunicipalityWasAttached(command.PostalCode, command.NewNisCode)))); + } + + [Fact] + public void WithNisCodeAlreadyAttached_ThenNone() + { + var command = _fixture.Create(); + + Assert( + new Scenario() + .Given( + command.PostalCode, + _fixture.Create(), + _fixture.Create()) + .When(command) + .ThenNone()); + } + + [Fact] + public void ThenMunicipalityWasRelinked() + { + var command = new RelinkMunicipality(_fixture.Create(), new NisCode("12345"), _fixture.Create()); + + var municipalityWasAttached = _fixture.Create(); + Assert( + new Scenario() + .Given( + command.PostalCode, + _fixture.Create(), + municipalityWasAttached) + .When(command) + .Then(new Fact(command.PostalCode, + new MunicipalityWasRelinked(command.PostalCode, command.NewNisCode, new NisCode(municipalityWasAttached.NisCode))))); + } + + [Fact] + public void StateCheck() + { + // Arrange + var municipalityWasRelinked = new MunicipalityWasRelinked( + _fixture.Create(), + new NisCode(_fixture.Create().ToString("00000")), + _fixture.Create()); + + // Act + var sut = PostalInformation.Factory(); + sut.Initialize(new object[] + { + _fixture.Create(), + _fixture.Create(), + municipalityWasRelinked + }); + + // Assert + sut.NisCode.Should().Be(new NisCode(municipalityWasRelinked.NewNisCode)); + } + } +} diff --git a/test/PostalRegistry.Tests/FakeIdempotencyContextFactory.cs b/test/PostalRegistry.Tests/FakeIdempotencyContextFactory.cs new file mode 100644 index 00000000..69602fb6 --- /dev/null +++ b/test/PostalRegistry.Tests/FakeIdempotencyContextFactory.cs @@ -0,0 +1,20 @@ +namespace PostalRegistry.Tests +{ + using System; + using Be.Vlaanderen.Basisregisters.CommandHandling.Idempotency; + using Microsoft.EntityFrameworkCore; + using Microsoft.EntityFrameworkCore.Design; + + public class FakeIdempotencyContextFactory : IDesignTimeDbContextFactory + { + public IdempotencyContext CreateDbContext(params string[] args) + { + var optionsBuilder = new DbContextOptionsBuilder(); + var tableInfo = new IdempotencyTableInfo("dbo"); + + optionsBuilder.UseInMemoryDatabase(Guid.NewGuid().ToString()); + + return new IdempotencyContext(optionsBuilder.Options, tableInfo); + } + } +} diff --git a/test/PostalRegistry.Tests/Import/ImportApiTest.cs b/test/PostalRegistry.Tests/Import/ImportApiTest.cs new file mode 100644 index 00000000..5315efbf --- /dev/null +++ b/test/PostalRegistry.Tests/Import/ImportApiTest.cs @@ -0,0 +1,40 @@ +namespace PostalRegistry.Tests.Import +{ + using System; + using System.Collections.Generic; + using System.Security.Claims; + using Be.Vlaanderen.Basisregisters.Api; + using Be.Vlaanderen.Basisregisters.EventHandling; + using Microsoft.AspNetCore.Http; + using Newtonsoft.Json; + using Xunit.Abstractions; + + public abstract class ImportApiTest : PostalRegistryTest + { + protected static JsonSerializerSettings EventsJsonSerializerSettings = EventsJsonSerializerSettingsProvider.CreateSerializerSettings(); + public ImportApiTest(ITestOutputHelper testOutputHelper) : base(testOutputHelper) + { } + + protected T CreateMergerControllerWithUser() where T : ApiController + { + var controller = Activator.CreateInstance(typeof(T)) as T; + + var claims = new List + { + new Claim(ClaimTypes.Name, "username"), + new Claim(ClaimTypes.NameIdentifier, "userId"), + new Claim("name", "Username"), + }; + var identity = new ClaimsIdentity(claims, "TestAuthType"); + var claimsPrincipal = new ClaimsPrincipal(identity); + if (controller != null) + { + controller.ControllerContext.HttpContext = new DefaultHttpContext { User = claimsPrincipal }; + + return controller; + } + + throw new Exception("Could not find controller type"); + } + } +} diff --git a/test/PostalRegistry.Tests/Import/ImportPostalInformationFromCrabExtensions.cs b/test/PostalRegistry.Tests/Import/ImportPostalInformationFromCrabExtensions.cs new file mode 100644 index 00000000..56329913 --- /dev/null +++ b/test/PostalRegistry.Tests/Import/ImportPostalInformationFromCrabExtensions.cs @@ -0,0 +1,23 @@ +namespace PostalRegistry.Tests.Import +{ + using Be.Vlaanderen.Basisregisters.Crab; + using PostalInformation.Commands.Crab; + + public static class ImportPostalInformationFromCrabExtensions + { + public static ImportPostalInformationFromCrab WithSubCantonCode(this ImportPostalInformationFromCrab command, CrabSubCantonCode subCantonCode) + { + return new ImportPostalInformationFromCrab( + command.PostalCode, + command.SubCantonId, + subCantonCode, + command.NisCode, + command.MunicipalityName, + command.Lifetime, + command.Timestamp, + command.Operator, + command.Modification, + command.Organisation); + } + } +} diff --git a/test/PostalRegistry.Tests/Import/RelinkMunicipality/RelinkMunicipalityValidatorTests.cs b/test/PostalRegistry.Tests/Import/RelinkMunicipality/RelinkMunicipalityValidatorTests.cs new file mode 100644 index 00000000..f0d1bc4f --- /dev/null +++ b/test/PostalRegistry.Tests/Import/RelinkMunicipality/RelinkMunicipalityValidatorTests.cs @@ -0,0 +1,50 @@ +namespace PostalRegistry.Tests.Import.RelinkMunicipality +{ + using System.Linq; + using Api.Import.Relink; + using FluentAssertions; + using Xunit; + + public sealed class RelinkMunicipalityValidatorTests + { + private readonly RelinkMunicipalityRequestValidator _validator; + + public RelinkMunicipalityValidatorTests() + { + _validator = new RelinkMunicipalityRequestValidator(); + } + + [Fact] + public void When_postal_code_is_empty_then_validation_fails() + { + var request = new RelinkMunicipalityRequest { PostalCode = string.Empty, NewNisCode = "11111" }; + + var result = _validator.Validate(request); + + result.IsValid.Should().BeFalse(); + result.Errors.Where(x => x.PropertyName == nameof(request.PostalCode)).Should().HaveCount(1); + } + + [Fact] + public void When_nis_code_is_empty_then_validation_fails() + { + var request = new RelinkMunicipalityRequest { PostalCode = "9000", NewNisCode = string.Empty }; + + var result = _validator.Validate(request); + + result.IsValid.Should().BeFalse(); + result.Errors.Where(x => x.PropertyName == nameof(request.NewNisCode)).Should().HaveCount(2); + } + + [Fact] + public void When_nis_code_is_not_5_characters_then_validation_fails() + { + var request = new RelinkMunicipalityRequest { PostalCode = "9000", NewNisCode = "112" }; + + var result = _validator.Validate(request); + + result.IsValid.Should().BeFalse(); + result.Errors.Where(x => x.PropertyName == nameof(request.NewNisCode)).Should().HaveCount(1); + } + } +} diff --git a/test/PostalRegistry.Tests/Import/RelinkMunicipality/WhenRelinkingMunicipality.cs b/test/PostalRegistry.Tests/Import/RelinkMunicipality/WhenRelinkingMunicipality.cs new file mode 100644 index 00000000..c6cf77f4 --- /dev/null +++ b/test/PostalRegistry.Tests/Import/RelinkMunicipality/WhenRelinkingMunicipality.cs @@ -0,0 +1,109 @@ +namespace PostalRegistry.Tests.Import.RelinkMunicipality +{ + using System; + using System.Threading; + using System.Threading.Tasks; + using Api.Import; + using Api.Import.Relink; + using Autofac; + using AutoFixture; + using Be.Vlaanderen.Basisregisters.Api.Exceptions; + using Be.Vlaanderen.Basisregisters.CommandHandling; + using Be.Vlaanderen.Basisregisters.CommandHandling.Idempotency; + using Be.Vlaanderen.Basisregisters.Crab; + using FluentAssertions; + using FluentValidation; + using global::AutoFixture; + using Newtonsoft.Json; + using PostalInformation.Commands.BPost; + using PostalInformation.Commands.Crab; + using PostalInformation.Events; + using SqlStreamStore; + using SqlStreamStore.Streams; + using Xunit; + using Xunit.Abstractions; + + public sealed class WhenRelinkingMunicipality : ImportApiTest + { + private readonly PostalInformationController _controller; + private readonly Fixture _fixture; + + public WhenRelinkingMunicipality(ITestOutputHelper testOutputHelper) : base(testOutputHelper) + { + _controller = CreateMergerControllerWithUser(); + _fixture = new Fixture(); + _fixture.Customize(new InfrastructureCustomization()); + _fixture.Customize(new WithFixedPostalCode()); + _fixture.Customize(new WithIntegerNisCode()); + } + + [Fact] + public void GivenInvalidRequest_ThenValidationErrorIsThrown() + { + var act = async () => await _controller.RelinkMunicipality( + "9000", + new RelinkMunicipalityRequest(), + new RelinkMunicipalityRequestValidator(), + Container.Resolve(), + CancellationToken.None); + + act + .Should() + .ThrowAsync(); + } + + [Fact] + public void GivenPostalCodeDoesNotExist_ThenApiExceptionIsThrown() + { + var act = async () => + await _controller.RelinkMunicipality( + "9000", + new RelinkMunicipalityRequest { NewNisCode = "10001" }, + new RelinkMunicipalityRequestValidator(), + Container.Resolve(), + CancellationToken.None); + + act + .Should() + .ThrowAsync() + .Result + .Where(x => x.Message == "Onbestaande postcode"); + } + + [Fact] + public async Task GivenValidRequest_ThenMunicipalityIsRelinked() + { + var importPostalInformationFromBPost = _fixture.Create(); + DispatchArrangeCommand(importPostalInformationFromBPost, () => importPostalInformationFromBPost.CreateCommandId()); + + var importPostalInformationFromCrab = _fixture.Create() + .WithSubCantonCode(new CrabSubCantonCode(importPostalInformationFromBPost.PostalCode)); + + DispatchArrangeCommand(importPostalInformationFromCrab, () => importPostalInformationFromCrab.CreateCommandId()); + + await _controller.RelinkMunicipality( + importPostalInformationFromCrab.PostalCode, + new RelinkMunicipalityRequest { NewNisCode = "10001" }, + new RelinkMunicipalityRequestValidator(), + Container.Resolve(), + CancellationToken.None); + + var streamStore = Container.Resolve(); + + var newMessages = await streamStore.ReadStreamBackwards(new StreamId(importPostalInformationFromBPost.PostalCode), 8, 1); + newMessages.Messages.Length.Should().Be(1); + newMessages.Messages[0].Type.Should().Be(nameof(MunicipalityWasRelinked)); + var municipalityWasRelinked = JsonConvert.DeserializeObject(await newMessages.Messages[0].GetJsonData(), EventsJsonSerializerSettings); + municipalityWasRelinked.Should().NotBeNull(); + municipalityWasRelinked.NewNisCode.Should().Be("10001"); + municipalityWasRelinked.PreviousNisCode.Should().Be(importPostalInformationFromCrab.NisCode); + } + + private void DispatchArrangeCommand(T command, Func createCommandId) + { + using var scope = Container.BeginLifetimeScope(); + var bus = scope.Resolve(); + bus.Dispatch(createCommandId(), command).GetAwaiter().GetResult(); + } + } +} diff --git a/test/PostalRegistry.Tests/PostalRegistry.Tests.csproj b/test/PostalRegistry.Tests/PostalRegistry.Tests.csproj index 30b7279b..4b808c6a 100644 --- a/test/PostalRegistry.Tests/PostalRegistry.Tests.csproj +++ b/test/PostalRegistry.Tests/PostalRegistry.Tests.csproj @@ -2,6 +2,7 @@ + diff --git a/test/PostalRegistry.Tests/PostalRegistryTest.cs b/test/PostalRegistry.Tests/PostalRegistryTest.cs index 91e797b6..a7e94c3b 100755 --- a/test/PostalRegistry.Tests/PostalRegistryTest.cs +++ b/test/PostalRegistry.Tests/PostalRegistryTest.cs @@ -5,6 +5,7 @@ namespace PostalRegistry.Tests using Be.Vlaanderen.Basisregisters.AggregateSource.Testing; using Be.Vlaanderen.Basisregisters.AggregateSource.Testing.Comparers; using Be.Vlaanderen.Basisregisters.AggregateSource.Testing.SqlStreamStore.Autofac; + using Be.Vlaanderen.Basisregisters.CommandHandling.Idempotency; using Be.Vlaanderen.Basisregisters.EventHandling; using Be.Vlaanderen.Basisregisters.EventHandling.Autofac; using Infrastructure.Modules; @@ -19,10 +20,19 @@ protected PostalRegistryTest(ITestOutputHelper testOutputHelper) : base(testOutp protected override void ConfigureCommandHandling(ContainerBuilder builder) { var configuration = new ConfigurationBuilder() - .AddInMemoryCollection(new Dictionary { { "ConnectionStrings:Events", "" } }) + .AddInMemoryCollection(new Dictionary { { "ConnectionStrings:Events", "x" } }!) .Build(); builder.RegisterModule(new CommandHandlingModule(configuration)); + + builder.Register(_ => new FakeIdempotencyContextFactory().CreateDbContext([])) + .As() + .InstancePerLifetimeScope(); + + builder.RegisterType() + .As() + .AsSelf() + .InstancePerLifetimeScope(); } protected override void ConfigureEventHandling(ContainerBuilder builder)