diff --git a/PostalRegistry.sln b/PostalRegistry.sln index f6b9d772..747c21db 100755 --- a/PostalRegistry.sln +++ b/PostalRegistry.sln @@ -30,10 +30,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PostalRegistry.Tests", "tes EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PostalRegistry.Projections.Legacy", "src\PostalRegistry.Projections.Legacy\PostalRegistry.Projections.Legacy.csproj", "{0E08B66D-9320-47D8-980C-68753A0C0574}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PostalRegistry.BPostReader", "src\PostalRegistry.BPostReader\PostalRegistry.BPostReader.csproj", "{13568DA1-586D-4AD0-8834-8DBADD95207C}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PostalRegistry.EventGenerator.CrabPostinfo", "src\PostalRegistry.EventGenerator.CrabPostinfo\PostalRegistry.EventGenerator.CrabPostinfo.csproj", "{8CF8A3F9-253A-40FD-BBA8-9E4FCA74BFBC}" -EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "docs", "docs", "{50D64CB2-C9C2-4786-A350-DA4239162FBA}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PostalRegistry.Structurizr", "docs\PostalRegistry.Structurizr\PostalRegistry.Structurizr.csproj", "{73D40942-1B6E-4BCF-9F4F-332DEAC48A67}" @@ -87,14 +83,6 @@ Global {0E08B66D-9320-47D8-980C-68753A0C0574}.Debug|Any CPU.Build.0 = Debug|Any CPU {0E08B66D-9320-47D8-980C-68753A0C0574}.Release|Any CPU.ActiveCfg = Release|Any CPU {0E08B66D-9320-47D8-980C-68753A0C0574}.Release|Any CPU.Build.0 = Release|Any CPU - {13568DA1-586D-4AD0-8834-8DBADD95207C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {13568DA1-586D-4AD0-8834-8DBADD95207C}.Debug|Any CPU.Build.0 = Debug|Any CPU - {13568DA1-586D-4AD0-8834-8DBADD95207C}.Release|Any CPU.ActiveCfg = Release|Any CPU - {13568DA1-586D-4AD0-8834-8DBADD95207C}.Release|Any CPU.Build.0 = Release|Any CPU - {8CF8A3F9-253A-40FD-BBA8-9E4FCA74BFBC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {8CF8A3F9-253A-40FD-BBA8-9E4FCA74BFBC}.Debug|Any CPU.Build.0 = Debug|Any CPU - {8CF8A3F9-253A-40FD-BBA8-9E4FCA74BFBC}.Release|Any CPU.ActiveCfg = Release|Any CPU - {8CF8A3F9-253A-40FD-BBA8-9E4FCA74BFBC}.Release|Any CPU.Build.0 = Release|Any CPU {73D40942-1B6E-4BCF-9F4F-332DEAC48A67}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {73D40942-1B6E-4BCF-9F4F-332DEAC48A67}.Debug|Any CPU.Build.0 = Debug|Any CPU {73D40942-1B6E-4BCF-9F4F-332DEAC48A67}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -145,8 +133,6 @@ Global {488B07C6-FEDE-4F8D-A6E2-A7DFED79BBA1} = {81BE6CDF-434E-42AB-9CB1-B5E7A26DC317} {8F656DE8-12B1-4FFB-8404-16C17558E448} = {A82FA45A-B77F-48AA-8E30-C2928DCAF7EF} {0E08B66D-9320-47D8-980C-68753A0C0574} = {81BE6CDF-434E-42AB-9CB1-B5E7A26DC317} - {13568DA1-586D-4AD0-8834-8DBADD95207C} = {81BE6CDF-434E-42AB-9CB1-B5E7A26DC317} - {8CF8A3F9-253A-40FD-BBA8-9E4FCA74BFBC} = {81BE6CDF-434E-42AB-9CB1-B5E7A26DC317} {73D40942-1B6E-4BCF-9F4F-332DEAC48A67} = {50D64CB2-C9C2-4786-A350-DA4239162FBA} {1A0113BC-1F80-4213-B85D-DC97B85A2BA9} = {81BE6CDF-434E-42AB-9CB1-B5E7A26DC317} {A2902925-92FC-47D3-A040-CD06FCFC34CF} = {81BE6CDF-434E-42AB-9CB1-B5E7A26DC317} diff --git a/src/PostalRegistry.Api.Import/BPostImport/BPostImportController.cs b/src/PostalRegistry.Api.Import/BPostImport/BPostImportController.cs deleted file mode 100644 index ece026ee..00000000 --- a/src/PostalRegistry.Api.Import/BPostImport/BPostImportController.cs +++ /dev/null @@ -1,67 +0,0 @@ -namespace PostalRegistry.Api.Import.BPostImport -{ - using System; - using System.Threading; - using System.Threading.Tasks; - using Asp.Versioning; - using Be.Vlaanderen.Basisregisters.Api; - using Be.Vlaanderen.Basisregisters.Api.Exceptions; - using Be.Vlaanderen.Basisregisters.CommandHandling; - using Be.Vlaanderen.Basisregisters.CommandHandling.Idempotency; - using Infrastructure; - using Microsoft.AspNetCore.Http; - using Microsoft.AspNetCore.Mvc; - using Newtonsoft.Json.Converters; - using Requests; - using Swashbuckle.AspNetCore.Filters; - using ProblemDetails = Be.Vlaanderen.Basisregisters.BasicApiProblem.ProblemDetails; - - [ApiVersion("1.0")] - [AdvertiseApiVersions("1.0")] - [ApiRoute("bpostimport")] - [ApiExplorerSettings(GroupName = "BPost Import")] - public class BPostImportController : ApiBusController - { - public BPostImportController(ICommandHandlerResolver bus) : base(bus) { } - - /// - /// Import een bpost item. - /// - /// - /// Optionele unieke id voor het verzoek. - /// - /// - /// Als het verzoek aanvaard is. - /// Als het verzoek ongeldige data bevat. - /// Als er een interne fout is opgetreden. - /// - [HttpPost] - [ProducesResponseType(typeof(void), StatusCodes.Status202Accepted)] - [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)] - [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status500InternalServerError)] - [SwaggerRequestExample(typeof(RegisterBPostImportRequestExample), typeof(RegisterBPostImportRequestExample))] - [SwaggerResponseExample(StatusCodes.Status202Accepted, typeof(RegisterBPostImportResponseExamples))] - [SwaggerResponseExample(StatusCodes.Status400BadRequest, typeof(BadRequestResponseExamples))] - [SwaggerResponseExample(StatusCodes.Status500InternalServerError, typeof(InternalServerErrorResponseExamples))] - public async Task Post( - [FromServices] IdempotencyContext context, - [FromCommandId] Guid commandId, - [FromBody] RegisterBPostImportRequest registerBPostImport, - CancellationToken cancellationToken) - { - if (!ModelState.IsValid) - return BadRequest(ModelState); // TODO: Check what this returns in the response - - return await IdempotentCommandHandlerDispatch( - context, - commandId, - () => RegisterBPostImportRequestMapping.Map(registerBPostImport), - cancellationToken); - } - } - - public class RegisterBPostImportResponseExamples : IExamplesProvider - { - public object GetExamples() => new { }; - } -} diff --git a/src/PostalRegistry.Api.Import/BPostImport/Requests/RegisterBPostImportRequest.cs b/src/PostalRegistry.Api.Import/BPostImport/Requests/RegisterBPostImportRequest.cs deleted file mode 100644 index 6986b354..00000000 --- a/src/PostalRegistry.Api.Import/BPostImport/Requests/RegisterBPostImportRequest.cs +++ /dev/null @@ -1,39 +0,0 @@ -namespace PostalRegistry.Api.Import.BPostImport.Requests -{ - using System.ComponentModel.DataAnnotations; - using Newtonsoft.Json; - using PostalInformation; - using Swashbuckle.AspNetCore.Filters; - - public class RegisterBPostImportRequest - { - /// Type van het bpost item. - [Required] - public string Type { get; set; } - - /// Het bpost item. - [Required] - public string BPostItem { get; set; } - } - - public class RegisterBPostImportRequestExample : IExamplesProvider - { - public RegisterBPostImportRequest GetExamples() - => new RegisterBPostImportRequest - { - Type = "PostalRegistry.PostalInformation.Commands.BPost.ImportPostalInformationFromBPost", - BPostItem = "{}" - }; - } - - public static class RegisterBPostImportRequestMapping - { - public static dynamic Map(RegisterBPostImportRequest message) - { - var assembly = typeof(PostalInformation).Assembly; - var type = assembly.GetType(message.Type); - - return JsonConvert.DeserializeObject(message.BPostItem, type); - } - } -} diff --git a/src/PostalRegistry.Api.Import/CrabImport/CrabImportController.cs b/src/PostalRegistry.Api.Import/CrabImport/CrabImportController.cs deleted file mode 100644 index d3ef4852..00000000 --- a/src/PostalRegistry.Api.Import/CrabImport/CrabImportController.cs +++ /dev/null @@ -1,67 +0,0 @@ -namespace PostalRegistry.Api.Import.CrabImport -{ - using System; - using System.Threading; - using System.Threading.Tasks; - using Asp.Versioning; - using Be.Vlaanderen.Basisregisters.Api; - using Be.Vlaanderen.Basisregisters.Api.Exceptions; - using Be.Vlaanderen.Basisregisters.CommandHandling; - using Be.Vlaanderen.Basisregisters.CommandHandling.Idempotency; - using Infrastructure; - using Microsoft.AspNetCore.Http; - using Microsoft.AspNetCore.Mvc; - using Newtonsoft.Json.Converters; - using Requests; - using Swashbuckle.AspNetCore.Filters; - using ProblemDetails = Be.Vlaanderen.Basisregisters.BasicApiProblem.ProblemDetails; - - [ApiVersion("1.0")] - [AdvertiseApiVersions("1.0")] - [ApiRoute("crabimport")] - [ApiExplorerSettings(GroupName = "CRAB Import")] - public class CrabImportController : ApiBusController - { - public CrabImportController(ICommandHandlerResolver bus) : base(bus) { } - - /// - /// Import een CRAB item. - /// - /// - /// Optionele unieke id voor het verzoek. - /// - /// - /// Als het verzoek aanvaard is. - /// Als het verzoek ongeldige data bevat. - /// Als er een interne fout is opgetreden. - /// - [HttpPost] - [ProducesResponseType(typeof(void), StatusCodes.Status202Accepted)] - [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)] - [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status500InternalServerError)] - [SwaggerRequestExample(typeof(RegisterCrabImportRequest), typeof(RegisterCrabImportRequestExample))] - [SwaggerResponseExample(StatusCodes.Status202Accepted, typeof(RegisterCrabImportResponseExamples))] - [SwaggerResponseExample(StatusCodes.Status400BadRequest, typeof(BadRequestResponseExamples))] - [SwaggerResponseExample(StatusCodes.Status500InternalServerError, typeof(InternalServerErrorResponseExamples))] - public async Task Post( - [FromServices] IdempotencyContext context, - [FromCommandId] Guid commandId, - [FromBody] RegisterCrabImportRequest registerCrabImport, - CancellationToken cancellationToken = default) - { - if (!ModelState.IsValid) - return BadRequest(ModelState); // TODO: Check what this returns in the response - - return await IdempotentCommandHandlerDispatch( - context, - commandId, - () => RegisterCrabImportRequestMapping.Map(registerCrabImport), - cancellationToken); - } - } - - public class RegisterCrabImportResponseExamples : IExamplesProvider - { - public object GetExamples() => new { }; - } -} diff --git a/src/PostalRegistry.Api.Import/CrabImport/Requests/RegisterCrabImportRequest.cs b/src/PostalRegistry.Api.Import/CrabImport/Requests/RegisterCrabImportRequest.cs deleted file mode 100644 index 3e0d7a36..00000000 --- a/src/PostalRegistry.Api.Import/CrabImport/Requests/RegisterCrabImportRequest.cs +++ /dev/null @@ -1,39 +0,0 @@ -namespace PostalRegistry.Api.Import.CrabImport.Requests -{ - using System.ComponentModel.DataAnnotations; - using Newtonsoft.Json; - using PostalInformation; - using Swashbuckle.AspNetCore.Filters; - - public class RegisterCrabImportRequest - { - /// Type van het CRAB item. - [Required] - public string Type { get; set; } - - /// Het CRAB item. - [Required] - public string CrabItem { get; set; } - } - - public class RegisterCrabImportRequestExample : IExamplesProvider - { - public RegisterCrabImportRequest GetExamples() - => new RegisterCrabImportRequest - { - Type = "PostalRegistry.PostalInformation.Commands.Crab.ImportPostalInformationFromCrab", - CrabItem = "{}" - }; - } - - public static class RegisterCrabImportRequestMapping - { - public static dynamic Map(RegisterCrabImportRequest message) - { - var assembly = typeof(PostalInformation).Assembly; - var type = assembly.GetType(message.Type); - - return JsonConvert.DeserializeObject(message.CrabItem, type); - } - } -} diff --git a/src/PostalRegistry.Api.Import/PostalInformationController-UpdatePostalNames.cs b/src/PostalRegistry.Api.Import/PostalInformationController-UpdatePostalNames.cs new file mode 100644 index 00000000..8cec8e48 --- /dev/null +++ b/src/PostalRegistry.Api.Import/PostalInformationController-UpdatePostalNames.cs @@ -0,0 +1,70 @@ +namespace PostalRegistry.Api.Import +{ + using System; + using System.Collections.Generic; + using System.Linq; + 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 Be.Vlaanderen.Basisregisters.GrAr.Legacy; + using Be.Vlaanderen.Basisregisters.GrAr.Legacy.PostInfo; + using FluentValidation; + using Microsoft.AspNetCore.Http; + using Microsoft.AspNetCore.Mvc; + using UpdatePostalNames; + + public sealed partial class PostalInformationController + { + [HttpPost("{postcode}/update-names")] + public async Task UpdatePostalNames( + [FromRoute(Name = "postcode")]string? postalCode, + [FromBody] UpdatePostalNamesRequest request, + [FromServices] IValidator validator, + [FromServices] IIdempotentCommandHandler idempotentCommandHandler, + CancellationToken cancellationToken = default) + { + request.PostalCode = postalCode; + await validator.ValidateAndThrowAsync(request, cancellationToken: cancellationToken); + + try + { + var command = new PostalInformation.Commands.UpdatePostalNames( + new PostalCode(postalCode!) + , request.PostalNamesToAdd.Select(MapPostalName) + , request.PostalNamesToRemove.Select(MapPostalName) + , 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); + } + } + + private static PostalName MapPostalName(Postnaam postnaam) + { + switch (postnaam.GeografischeNaam.Taal) + { + case Taal.NL: + return new PostalName(postnaam.GeografischeNaam.Spelling, Language.Dutch); + case Taal.FR: + return new PostalName(postnaam.GeografischeNaam.Spelling, Language.French); + case Taal.DE: + return new PostalName(postnaam.GeografischeNaam.Spelling, Language.German); + case Taal.EN: + return new PostalName(postnaam.GeografischeNaam.Spelling, Language.English); + default: + throw new ArgumentOutOfRangeException(nameof(postnaam.GeografischeNaam.Taal), postnaam.GeografischeNaam.Taal, null); + } + } + } +} diff --git a/src/PostalRegistry.Api.Import/UpdatePostalNames/UpdatePostalNamesRequest.cs b/src/PostalRegistry.Api.Import/UpdatePostalNames/UpdatePostalNamesRequest.cs new file mode 100644 index 00000000..a98ee700 --- /dev/null +++ b/src/PostalRegistry.Api.Import/UpdatePostalNames/UpdatePostalNamesRequest.cs @@ -0,0 +1,15 @@ +namespace PostalRegistry.Api.Import.UpdatePostalNames +{ + using System.Collections.Generic; + using Be.Vlaanderen.Basisregisters.GrAr.Legacy.PostInfo; + + public sealed class UpdatePostalNamesRequest + { + public string? PostalCode { get; set; } + + public List PostalNamesToAdd { get; set; } = new List(); + public List PostalNamesToRemove { get; set; } = new List(); + + public string? Reason { get; set; } + } +} diff --git a/src/PostalRegistry.Api.Import/UpdatePostalNames/UpdatePostalNamesRequestValidator.cs b/src/PostalRegistry.Api.Import/UpdatePostalNames/UpdatePostalNamesRequestValidator.cs new file mode 100644 index 00000000..4933ac89 --- /dev/null +++ b/src/PostalRegistry.Api.Import/UpdatePostalNames/UpdatePostalNamesRequestValidator.cs @@ -0,0 +1,45 @@ +namespace PostalRegistry.Api.Import.UpdatePostalNames +{ + using System.Linq; + using Be.Vlaanderen.Basisregisters.GrAr.Legacy.PostInfo; + using FluentValidation; + + public sealed class UpdatePostalNamesRequestValidator : AbstractValidator + { + public UpdatePostalNamesRequestValidator() + { + RuleFor(request => request.PostalCode) + .NotEmpty(); + + RuleFor(request => request.PostalNamesToAdd.Concat(request.PostalNamesToRemove)) + .NotEmpty() + .OverridePropertyName(nameof(UpdatePostalNamesRequest.PostalNamesToAdd)); + + RuleFor(request => request.PostalNamesToAdd) + .Must(x => + { + return x.Select(y => (y.GeografischeNaam.Spelling.ToLower(), y.GeografischeNaam.Taal)) + .Distinct() + .Count() == x.Count; + }); + + RuleForEach(x => x.PostalNamesToAdd) + .SetValidator(new PostnaamValidator()); + + RuleForEach(x => x.PostalNamesToRemove) + .SetValidator(new PostnaamValidator()); + } + } + + public sealed class PostnaamValidator : AbstractValidator + { + public PostnaamValidator() + { + RuleFor(request => request.GeografischeNaam) + .NotNull(); + + RuleFor(request => request.GeografischeNaam.Spelling) + .NotEmpty(); + } + } +} diff --git a/src/PostalRegistry.Api.Import/paket.references b/src/PostalRegistry.Api.Import/paket.references index 7869eaee..20d69ba5 100644 --- a/src/PostalRegistry.Api.Import/paket.references +++ b/src/PostalRegistry.Api.Import/paket.references @@ -2,6 +2,7 @@ Be.Vlaanderen.Basisregisters.Api Be.Vlaanderen.Basisregisters.CommandHandling.Idempotency Be.Vlaanderen.Basisregisters.EventHandling.Autofac Be.Vlaanderen.Basisregisters.ProjectionHandling.SqlStreamStore.Autofac +Be.Vlaanderen.Basisregisters.GrAr.Legacy AspNetCore.HealthChecks.SqlServer diff --git a/src/PostalRegistry/PostalInformation/Commands/UpdatePostalNames.cs b/src/PostalRegistry/PostalInformation/Commands/UpdatePostalNames.cs new file mode 100644 index 00000000..c7615a50 --- /dev/null +++ b/src/PostalRegistry/PostalInformation/Commands/UpdatePostalNames.cs @@ -0,0 +1,48 @@ +namespace PostalRegistry.PostalInformation.Commands +{ + using System; + using System.Collections.Generic; + using System.Linq; + using Be.Vlaanderen.Basisregisters.Generators.Guid; + using Be.Vlaanderen.Basisregisters.GrAr.Provenance; + using Be.Vlaanderen.Basisregisters.Utilities; + + public sealed class UpdatePostalNames : IHasCommandProvenance + { + private static readonly Guid Namespace = new Guid("6af54580-27cc-4b16-a6c9-d14eee229ed8"); + + public PostalCode PostalCode { get; } + + public IReadOnlyCollection PostalNamesToAdd { get; } + public IReadOnlyCollection PostalNamesToRemove { get; } + public Provenance Provenance { get; } + + public UpdatePostalNames( + PostalCode postalCode, + IEnumerable postalNamesToAdd, + IEnumerable postalNamesToRemove, + Provenance provenance) + { + PostalCode = postalCode; + PostalNamesToAdd = postalNamesToAdd.ToList(); + PostalNamesToRemove = postalNamesToRemove.ToList(); + Provenance = provenance; + } + + public Guid CreateCommandId() + => Deterministic.Create(Namespace, $"UpdatePostalNames-{ToString()}"); + + public override string? ToString() + => ToStringBuilder.ToString(IdentityFields()); + + private IEnumerable IdentityFields() + { + yield return PostalCode; + + foreach (var field in Provenance.GetIdentityFields()) + { + yield return field; + } + } + } +} diff --git a/src/PostalRegistry/PostalInformation/Exceptions/PostalNameAlreadyExistsException.cs b/src/PostalRegistry/PostalInformation/Exceptions/PostalNameAlreadyExistsException.cs new file mode 100644 index 00000000..3b4c8569 --- /dev/null +++ b/src/PostalRegistry/PostalInformation/Exceptions/PostalNameAlreadyExistsException.cs @@ -0,0 +1,20 @@ +namespace PostalRegistry.PostalInformation.Exceptions +{ + using System; + using System.Runtime.Serialization; + + [Serializable] + public sealed class PostalNameAlreadyExistsException : PostalRegistryException + { + public PostalName PostalName { get; } + + public PostalNameAlreadyExistsException(PostalName postalName) + { + PostalName = postalName; + } + + private PostalNameAlreadyExistsException(SerializationInfo info, StreamingContext context) + : base(info, context) + { } + } +} diff --git a/src/PostalRegistry/PostalInformation/PostalInformation.cs b/src/PostalRegistry/PostalInformation/PostalInformation.cs index c2e7ea47..2b1491e7 100755 --- a/src/PostalRegistry/PostalInformation/PostalInformation.cs +++ b/src/PostalRegistry/PostalInformation/PostalInformation.cs @@ -93,5 +93,21 @@ public void RelinkMunicipality(NisCode newNisCode) ApplyChange(new MunicipalityWasRelinked(PostalCode, newNisCode, NisCode!)); } + + public void UpdatePostalNames( + IReadOnlyCollection postalNamesToAdd, + IReadOnlyCollection postalNamesToRemove) + { + foreach (var postalName in postalNamesToRemove.Where(_postalNames.Contains)) + ApplyChange(new PostalInformationPostalNameWasRemoved(PostalCode, postalName)); + + foreach (var postalName in postalNamesToAdd) + { + if(_postalNames.Contains(postalName)) + throw new PostalNameAlreadyExistsException(postalName); + + ApplyChange(new PostalInformationPostalNameWasAdded(PostalCode, postalName)); + } + } } } diff --git a/src/PostalRegistry/PostalInformation/PostalInformationCommandHandlerModule.cs b/src/PostalRegistry/PostalInformation/PostalInformationCommandHandlerModule.cs index 1989ae06..1ec9fd13 100755 --- a/src/PostalRegistry/PostalInformation/PostalInformationCommandHandlerModule.cs +++ b/src/PostalRegistry/PostalInformation/PostalInformationCommandHandlerModule.cs @@ -82,6 +82,17 @@ public PostalInformationCommandHandlerModule( postalInformation.RelinkMunicipality(message.Command.NewNisCode); }); + + 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.UpdatePostalNames(message.Command.PostalNamesToAdd, message.Command.PostalNamesToRemove); + }); } } } diff --git a/src/PostalRegistry/PostalInformation/PostalInformationState.cs b/src/PostalRegistry/PostalInformation/PostalInformationState.cs index ddec7b12..e1c8a258 100755 --- a/src/PostalRegistry/PostalInformation/PostalInformationState.cs +++ b/src/PostalRegistry/PostalInformation/PostalInformationState.cs @@ -8,11 +8,11 @@ namespace PostalRegistry.PostalInformation public partial class PostalInformation { - public PostalCode PostalCode { get; private set; } private PostalInformationStatus? _status; - private readonly List _postalNames = new List(); + public PostalCode PostalCode { get; private set; } + public IReadOnlyCollection PostalNames => _postalNames.AsReadOnly(); public NisCode? NisCode { get; set; } public Modification LastModification { get; private set; } diff --git a/test/PostalRegistry.Tests/AggregateTests/WhenUpdatingPostalNames/GivenNoPostalInformation.cs b/test/PostalRegistry.Tests/AggregateTests/WhenUpdatingPostalNames/GivenNoPostalInformation.cs new file mode 100644 index 00000000..0e0ec699 --- /dev/null +++ b/test/PostalRegistry.Tests/AggregateTests/WhenUpdatingPostalNames/GivenNoPostalInformation.cs @@ -0,0 +1,35 @@ +namespace PostalRegistry.Tests.AggregateTests.WhenUpdatingPostalNames +{ + 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/WhenUpdatingPostalNames/GivenPostalInformation.cs b/test/PostalRegistry.Tests/AggregateTests/WhenUpdatingPostalNames/GivenPostalInformation.cs new file mode 100644 index 00000000..0e993266 --- /dev/null +++ b/test/PostalRegistry.Tests/AggregateTests/WhenUpdatingPostalNames/GivenPostalInformation.cs @@ -0,0 +1,98 @@ +namespace PostalRegistry.Tests.AggregateTests.WhenUpdatingPostalNames +{ + using System.Collections.Generic; + using AutoFixture; + using Be.Vlaanderen.Basisregisters.AggregateSource; + using Be.Vlaanderen.Basisregisters.AggregateSource.Testing; + using Be.Vlaanderen.Basisregisters.GrAr.Provenance; + using Builders; + 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 GivenNameAlreadyExists_ThenPostalNameAlreadyExistsExceptionIsThrown() + { + var named = _fixture.Create(); + var postalName = new PostalName(named.Name, named.Language); + var command = new UpdatePostalNames(_fixture.Create(), + [postalName], + new List(), + _fixture.Create()); + + Assert( + new Scenario() + .Given( + command.PostalCode, + _fixture.Create(), + named) + .When(command) + .Throws(new PostalNameAlreadyExistsException(postalName))); + } + + [Fact] + public void ThenPostalNamesAreUpdated() + { + var named = _fixture.Create(); + var nameToAdd = new PostalName(_fixture.Create(), Language.Dutch); + var command = new UpdatePostalNames(_fixture.Create(), + [nameToAdd], + [new PostalName(named.Name, named.Language)], + _fixture.Create()); + + Assert( + new Scenario() + .Given( + command.PostalCode, + _fixture.Create(), + named) + .When(command) + .Then(new Fact(command.PostalCode, + new PostalInformationPostalNameWasRemoved(command.PostalCode, new PostalName(named.Name, named.Language))), + new Fact(command.PostalCode, + new PostalInformationPostalNameWasAdded(command.PostalCode, nameToAdd)))); + } + + [Fact] + public void StateCheck() + { + // Arrange + var postalInformationPostalNameWasAdded = _fixture.Create(); + var postalInformationPostalNameWasRemoved = new PostalInformationPostalNameWasRemovedBuilder(_fixture) + .WithName(postalInformationPostalNameWasAdded.Name, postalInformationPostalNameWasAdded.Language) + .Build(); + var postalInformationPostalNameWasAdded2 = _fixture.Create(); + + // Act + var sut = PostalInformation.Factory(); + sut.Initialize([ + _fixture.Create(), + _fixture.Create(), + postalInformationPostalNameWasAdded, + postalInformationPostalNameWasRemoved, + postalInformationPostalNameWasAdded2 + ]); + + // Assert + sut.PostalNames.Count.Should().Be(1); + sut.PostalNames.Should().Contain(x => x.Name == postalInformationPostalNameWasAdded2.Name && x.Language == postalInformationPostalNameWasAdded2.Language); + } + } +} diff --git a/test/PostalRegistry.Tests/Import/ImportApiTest.cs b/test/PostalRegistry.Tests/Import/ImportApiTest.cs index 5315efbf..0ae28934 100644 --- a/test/PostalRegistry.Tests/Import/ImportApiTest.cs +++ b/test/PostalRegistry.Tests/Import/ImportApiTest.cs @@ -3,7 +3,9 @@ using System; using System.Collections.Generic; using System.Security.Claims; + using Autofac; using Be.Vlaanderen.Basisregisters.Api; + using Be.Vlaanderen.Basisregisters.CommandHandling; using Be.Vlaanderen.Basisregisters.EventHandling; using Microsoft.AspNetCore.Http; using Newtonsoft.Json; @@ -36,5 +38,12 @@ protected T CreateMergerControllerWithUser() where T : ApiController throw new Exception("Could not find controller type"); } + + protected 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/Import/RelinkMunicipality/WhenRelinkingMunicipality.cs b/test/PostalRegistry.Tests/Import/RelinkMunicipality/WhenRelinkingMunicipality.cs index c6cf77f4..c985322a 100644 --- a/test/PostalRegistry.Tests/Import/RelinkMunicipality/WhenRelinkingMunicipality.cs +++ b/test/PostalRegistry.Tests/Import/RelinkMunicipality/WhenRelinkingMunicipality.cs @@ -1,6 +1,5 @@ namespace PostalRegistry.Tests.Import.RelinkMunicipality { - using System; using System.Threading; using System.Threading.Tasks; using Api.Import; @@ -8,7 +7,6 @@ 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; @@ -98,12 +96,5 @@ await _controller.RelinkMunicipality( 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/Import/UpdatePostalNames/UpdatePostalNamesValidatorTests.cs b/test/PostalRegistry.Tests/Import/UpdatePostalNames/UpdatePostalNamesValidatorTests.cs new file mode 100644 index 00000000..a69a593d --- /dev/null +++ b/test/PostalRegistry.Tests/Import/UpdatePostalNames/UpdatePostalNamesValidatorTests.cs @@ -0,0 +1,79 @@ +namespace PostalRegistry.Tests.Import.UpdatePostalNames +{ + using System.Collections.Generic; + using System.Linq; + using Api.Import.UpdatePostalNames; + using Be.Vlaanderen.Basisregisters.GrAr.Legacy; + using Be.Vlaanderen.Basisregisters.GrAr.Legacy.PostInfo; + using FluentAssertions; + using Xunit; + + public sealed class UpdatePostalNamesValidatorTests + { + private readonly UpdatePostalNamesRequestValidator _validator; + + public UpdatePostalNamesValidatorTests() + { + _validator = new UpdatePostalNamesRequestValidator(); + } + + [Fact] + public void When_postal_code_is_empty_then_validation_fails() + { + var request = new UpdatePostalNamesRequest { PostalCode = string.Empty }; + + 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_postal_names_to_add_and_remove_are_empty_then_validation_fails() + { + var request = new UpdatePostalNamesRequest { PostalCode = "9000" }; + + var result = _validator.Validate(request); + + result.IsValid.Should().BeFalse(); + result.Errors.Where(x => x.PropertyName == nameof(request.PostalNamesToAdd)).Should().HaveCount(1); + } + + [Fact] + public void When_postal_names_to_add_contains_duplicates_then_validation_fails() + { + var request = new UpdatePostalNamesRequest + { + PostalCode = "9000", + PostalNamesToAdd = new List + { + new Postnaam(new GeografischeNaam ("Gent", Taal.NL)), + new Postnaam(new GeografischeNaam ("Gent", Taal.NL)) + } + }; + + var result = _validator.Validate(request); + + result.IsValid.Should().BeFalse(); + result.Errors.Where(x => x.PropertyName == nameof(request.PostalNamesToAdd)).Should().HaveCount(1); + } + + [Fact] + public void When_postal_name_has_empty_spelling_then_validation_fails() + { + var request = new UpdatePostalNamesRequest + { + PostalCode = "9000", + PostalNamesToAdd = new List + { + new Postnaam(new GeografischeNaam (string.Empty, Taal.NL)) + } + }; + + var result = _validator.Validate(request); + + result.IsValid.Should().BeFalse(); + result.Errors.Where(x => x.PropertyName == $"{nameof(request.PostalNamesToAdd)}[0].GeografischeNaam.Spelling").Should().HaveCount(1); + } + } +} diff --git a/test/PostalRegistry.Tests/Import/UpdatePostalNames/WhenUpdatingPostalNames.cs b/test/PostalRegistry.Tests/Import/UpdatePostalNames/WhenUpdatingPostalNames.cs new file mode 100644 index 00000000..b8a23d3d --- /dev/null +++ b/test/PostalRegistry.Tests/Import/UpdatePostalNames/WhenUpdatingPostalNames.cs @@ -0,0 +1,116 @@ +namespace PostalRegistry.Tests.Import.UpdatePostalNames +{ + using System.Collections.Generic; + using System.ComponentModel.DataAnnotations; + using System.Linq; + using System.Threading; + using System.Threading.Tasks; + using Api.Import; + using Api.Import.UpdatePostalNames; + using Autofac; + using AutoFixture; + using Be.Vlaanderen.Basisregisters.Api.Exceptions; + using Be.Vlaanderen.Basisregisters.CommandHandling.Idempotency; + using Be.Vlaanderen.Basisregisters.Crab; + using Be.Vlaanderen.Basisregisters.GrAr.Legacy; + using Be.Vlaanderen.Basisregisters.GrAr.Legacy.PostInfo; + using FluentAssertions; + using global::AutoFixture; + using PostalInformation.Commands.BPost; + using PostalInformation.Commands.Crab; + using PostalInformation.Events; + using SqlStreamStore; + using SqlStreamStore.Streams; + using Xunit; + using Xunit.Abstractions; + + public sealed class WhenUpdatingPostalNames : ImportApiTest + { + private readonly PostalInformationController _controller; + private readonly Fixture _fixture; + + public WhenUpdatingPostalNames(ITestOutputHelper testOutputHelper) : base(testOutputHelper) + { + _controller = CreateMergerControllerWithUser(); + _fixture = new Fixture(); + _fixture.Customize(new InfrastructureCustomization()); + _fixture.Customize(new WithFixedPostalCode()); + } + + [Fact] + public void GivenInvalidRequest_ThenValidationErrorIsThrown() + { + var act = async () => await _controller.UpdatePostalNames( + "9000", + new UpdatePostalNamesRequest(), + new UpdatePostalNamesRequestValidator(), + Container.Resolve(), + CancellationToken.None); + + act + .Should() + .ThrowAsync(); + } + + [Fact] + public void GivenPostalCodeDoesNotExist_ThenApiExceptionIsThrown() + { + var act = async () => + await _controller.UpdatePostalNames( + "9000", + new UpdatePostalNamesRequest { PostalNamesToAdd = new List{_fixture.Create()}}, + new UpdatePostalNamesRequestValidator(), + Container.Resolve(), + CancellationToken.None); + + act + .Should() + .ThrowAsync() + .Result + .Where(x => x.Message == "Onbestaande postcode"); + } + + [Fact] + public async Task GivenValidRequest_ThenPostalNamesAreUpdated() + { + //Arrange + _fixture.Register(() => Language.Dutch); + var importPostalInformationFromBPost = _fixture.Create(); + DispatchArrangeCommand(importPostalInformationFromBPost, () => importPostalInformationFromBPost.CreateCommandId()); + + var importPostalInformationFromCrab = _fixture.Create() + .WithSubCantonCode(new CrabSubCantonCode(importPostalInformationFromBPost.PostalCode)); + DispatchArrangeCommand(importPostalInformationFromCrab, () => importPostalInformationFromCrab.CreateCommandId()); + + var updatePostalNamesRequest = new UpdatePostalNamesRequest + { + PostalCode = importPostalInformationFromCrab.PostalCode, + PostalNamesToRemove = new List + { + new Postnaam(new GeografischeNaam(importPostalInformationFromBPost.PostalNames[0].Name, Taal.NL)) + }, + PostalNamesToAdd = new List + { + new Postnaam(new GeografischeNaam("Ghent", Taal.FR)) + } + }; + + //Act + await _controller.UpdatePostalNames( + importPostalInformationFromCrab.PostalCode, + updatePostalNamesRequest, + new UpdatePostalNamesRequestValidator(), + Container.Resolve(), + CancellationToken.None); + + //Assert + var streamStore = Container.Resolve(); + + var newMessages = await streamStore.ReadStreamBackwards(new StreamId(importPostalInformationFromBPost.PostalCode), 9, 2); + newMessages.Messages.Length.Should().Be(2); + var messages = newMessages.Messages.Reverse().ToList(); + messages[0].Type.Should().Be(nameof(PostalInformationPostalNameWasRemoved)); + messages[1].Type.Should().Be(nameof(PostalInformationPostalNameWasAdded)); + } + } +}