diff --git a/ParcelRegistry.sln.DotSettings b/ParcelRegistry.sln.DotSettings index c38e744a..37da9169 100644 --- a/ParcelRegistry.sln.DotSettings +++ b/ParcelRegistry.sln.DotSettings @@ -14,6 +14,7 @@ True True <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb"><ExtraRule Prefix="" Suffix="" Style="aa_bb" /></Policy> + <Policy><Descriptor Staticness="Any" AccessRightKinds="Any" Description="Types and namespaces"><ElementKinds><Kind Name="NAMESPACE" /><Kind Name="CLASS" /><Kind Name="STRUCT" /><Kind Name="ENUM" /><Kind Name="DELEGATE" /></ElementKinds></Descriptor><Policy Inspect="True" Prefix="" Suffix="" Style="AaBb"><ExtraRule Prefix="" Suffix="" Style="aa_bb" /></Policy></Policy> True Never Never @@ -26,4 +27,5 @@ True True True + True diff --git a/paket.dependencies b/paket.dependencies index 50282344..6331fee1 100755 --- a/paket.dependencies +++ b/paket.dependencies @@ -79,16 +79,16 @@ nuget Be.Vlaanderen.Basisregisters.Projector 15.0.0 nuget Be.Vlaanderen.Basisregisters.Crab 4.0.0 -nuget Be.Vlaanderen.Basisregisters.GrAr.Common 21.1.0 -nuget Be.Vlaanderen.Basisregisters.GrAr.Notifications 21.1.0 -nuget Be.Vlaanderen.Basisregisters.GrAr.Contracts 21.1.0 -nuget Be.Vlaanderen.Basisregisters.GrAr.Edit 21.1.0 -nuget Be.Vlaanderen.Basisregisters.GrAr.Import 21.1.0 -nuget Be.Vlaanderen.Basisregisters.GrAr.Legacy 21.1.0 -nuget Be.Vlaanderen.Basisregisters.GrAr.Provenance 21.1.0 -nuget Be.Vlaanderen.Basisregisters.GrAr.Provenance.AcmIdm 21.1.0 -nuget Be.Vlaanderen.Basisregisters.GrAr.Extracts 21.1.0 -nuget Be.Vlaanderen.Basisregisters.GrAr.Oslo 21.1.0 +nuget Be.Vlaanderen.Basisregisters.GrAr.Common 21.6.0 +nuget Be.Vlaanderen.Basisregisters.GrAr.Notifications 21.6.0 +nuget Be.Vlaanderen.Basisregisters.GrAr.Contracts 21.6.0 +nuget Be.Vlaanderen.Basisregisters.GrAr.Edit 21.6.0 +nuget Be.Vlaanderen.Basisregisters.GrAr.Import 21.6.0 +nuget Be.Vlaanderen.Basisregisters.GrAr.Legacy 21.6.0 +nuget Be.Vlaanderen.Basisregisters.GrAr.Provenance 21.6.0 +nuget Be.Vlaanderen.Basisregisters.GrAr.Provenance.AcmIdm 21.6.0 +nuget Be.Vlaanderen.Basisregisters.GrAr.Extracts 21.6.0 +nuget Be.Vlaanderen.Basisregisters.GrAr.Oslo 21.6.0 nuget Be.Vlaanderen.Basisregisters.MessageHandling.AwsSqs.Simple 5.0.1 nuget Be.Vlaanderen.Basisregisters.MessageHandling.Kafka.Simple 5.0.1 diff --git a/paket.lock b/paket.lock index d5c41856..632c6073 100644 --- a/paket.lock +++ b/paket.lock @@ -253,20 +253,18 @@ 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.1) + Be.Vlaanderen.Basisregisters.GrAr.Common (21.6) 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.1) - Be.Vlaanderen.Basisregisters.AggregateSource (>= 9.0.1) - NodaTime (>= 3.1.11) - Be.Vlaanderen.Basisregisters.GrAr.Edit (21.1) + Be.Vlaanderen.Basisregisters.GrAr.Contracts (21.6) + Be.Vlaanderen.Basisregisters.GrAr.Edit (21.6) NetTopologySuite (>= 2.5) - Be.Vlaanderen.Basisregisters.GrAr.Extracts (21.1) + Be.Vlaanderen.Basisregisters.GrAr.Extracts (21.6) Be.Vlaanderen.Basisregisters.Api (>= 21.0) Be.Vlaanderen.Basisregisters.Shaperon (>= 10.0.2) - Be.Vlaanderen.Basisregisters.GrAr.Import (21.1) + Be.Vlaanderen.Basisregisters.GrAr.Import (21.6) Autofac (>= 8.0) Be.Vlaanderen.Basisregisters.AggregateSource.SqlStreamStore (>= 9.0.1) Be.Vlaanderen.Basisregisters.CommandHandling (>= 9.0.1) @@ -281,31 +279,31 @@ NUGET Serilog (>= 3.1.1) Serilog.Extensions.Logging (>= 8.0) System.Threading.Tasks.Dataflow (>= 8.0) - Be.Vlaanderen.Basisregisters.GrAr.Legacy (21.1) - Be.Vlaanderen.Basisregisters.GrAr.Common (21.1) + Be.Vlaanderen.Basisregisters.GrAr.Legacy (21.6) + Be.Vlaanderen.Basisregisters.GrAr.Common (21.6) Be.Vlaanderen.Basisregisters.Utilities.Rfc3339DateTimeOffset (>= 4.0) Newtonsoft.Json (>= 13.0.3) - Be.Vlaanderen.Basisregisters.GrAr.Notifications (21.1) + Be.Vlaanderen.Basisregisters.GrAr.Notifications (21.6) AWSSDK.SimpleNotificationService (>= 3.7.301.3) System.Text.Json (>= 8.0.3) - Be.Vlaanderen.Basisregisters.GrAr.Oslo (21.1) + Be.Vlaanderen.Basisregisters.GrAr.Oslo (21.6) Be.Vlaanderen.Basisregisters.AspNetCore.Mvc.Formatters.Json (>= 5.0) - Be.Vlaanderen.Basisregisters.GrAr.Common (21.1) + Be.Vlaanderen.Basisregisters.GrAr.Common (21.6) 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.1) + Be.Vlaanderen.Basisregisters.GrAr.Provenance (21.6) Be.Vlaanderen.Basisregisters.CommandHandling (>= 9.0.1) Be.Vlaanderen.Basisregisters.Crab (>= 4.0) - Be.Vlaanderen.Basisregisters.GrAr.Common (21.1) + Be.Vlaanderen.Basisregisters.GrAr.Common (21.6) Microsoft.CSharp (>= 4.7) - Be.Vlaanderen.Basisregisters.GrAr.Provenance.AcmIdm (21.1) + Be.Vlaanderen.Basisregisters.GrAr.Provenance.AcmIdm (21.6) Be.Vlaanderen.Basisregisters.Auth.AcmIdm (>= 2.0) Be.Vlaanderen.Basisregisters.CommandHandling (>= 9.0.1) Be.Vlaanderen.Basisregisters.Crab (>= 4.0) - Be.Vlaanderen.Basisregisters.GrAr.Common (21.1) - Be.Vlaanderen.Basisregisters.GrAr.Provenance (21.1) + Be.Vlaanderen.Basisregisters.GrAr.Common (21.6) + Be.Vlaanderen.Basisregisters.GrAr.Provenance (21.6) Microsoft.CSharp (>= 4.7) Be.Vlaanderen.Basisregisters.MessageHandling.AwsSqs.Simple (5.0.1) AWSSDK.Core (>= 3.7.302.15) diff --git a/src/ParcelRegistry.Consumer.Address.Console/Program.cs b/src/ParcelRegistry.Consumer.Address.Console/Program.cs index 864cb345..85f79982 100644 --- a/src/ParcelRegistry.Consumer.Address.Console/Program.cs +++ b/src/ParcelRegistry.Consumer.Address.Console/Program.cs @@ -9,11 +9,11 @@ namespace ParcelRegistry.Consumer.Address.Console using Autofac; using Autofac.Extensions.DependencyInjection; using Be.Vlaanderen.Basisregisters.Aws.DistributedMutex; + using Be.Vlaanderen.Basisregisters.CommandHandling.Idempotency; using Be.Vlaanderen.Basisregisters.EventHandling; using Be.Vlaanderen.Basisregisters.MessageHandling.Kafka; using Be.Vlaanderen.Basisregisters.MessageHandling.Kafka.Consumer; using Destructurama; - using Infrastructure; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; @@ -25,6 +25,7 @@ namespace ParcelRegistry.Consumer.Address.Console using Serilog; using Serilog.Debugging; using Serilog.Extensions.Logging; + using MigrationsHelper = Infrastructure.MigrationsHelper; public sealed class Program { @@ -145,6 +146,18 @@ public static async Task Main(string[] args) .As>() .SingleInstance(); + services.ConfigureIdempotency( + hostContext.Configuration.GetSection(IdempotencyConfiguration.Section) + .Get()!.ConnectionString!, + new IdempotencyMigrationsTableInfo(Schema.Import), + new IdempotencyTableInfo(Schema.Import), + loggerFactory); + + builder.RegisterType() + .As() + .AsSelf() + .InstancePerLifetimeScope(); + builder .RegisterModule(new EditModule(hostContext.Configuration)) .RegisterModule(new BackOfficeModule(hostContext.Configuration, services, loggerFactory, ServiceLifetime.Transient)); diff --git a/src/ParcelRegistry.Consumer.Address.Console/appsettings.json b/src/ParcelRegistry.Consumer.Address.Console/appsettings.json index ae773a67..1ce8dbbe 100644 --- a/src/ParcelRegistry.Consumer.Address.Console/appsettings.json +++ b/src/ParcelRegistry.Consumer.Address.Console/appsettings.json @@ -7,6 +7,10 @@ "BackOffice": "Server=(localdb)\\mssqllocaldb;Database=EFProviders.InMemory.ParcelRegistry;Trusted_Connection=True;TrustServerCertificate=True;" }, + "Idempotency": { + "ConnectionString": "Server=(localdb)\\mssqllocaldb;Database=EFProviders.InMemory.ParcelRegistry;Trusted_Connection=True;TrustServerCertificate=True;" + }, + "Kafka": { "BootstrapServers": "localhost:29092/" }, diff --git a/src/ParcelRegistry.Consumer.Address/BackOfficeConsumer.cs b/src/ParcelRegistry.Consumer.Address/BackOfficeConsumer.cs index dd484088..79641938 100644 --- a/src/ParcelRegistry.Consumer.Address/BackOfficeConsumer.cs +++ b/src/ParcelRegistry.Consumer.Address/BackOfficeConsumer.cs @@ -10,6 +10,7 @@ namespace ParcelRegistry.Consumer.Address using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; + using Parcel; using Projections; public sealed class BackOfficeConsumer : BackgroundService @@ -17,6 +18,7 @@ public sealed class BackOfficeConsumer : BackgroundService private readonly ILifetimeScope _lifetimeScope; private readonly IHostApplicationLifetime _hostApplicationLifetime; private readonly IDbContextFactory _backOfficeContextFactory; + private readonly IParcels _parcels; private readonly ILoggerFactory _loggerFactory; private readonly IIdempotentConsumer _kafkaIdemIdompotencyConsumer; private readonly ILogger _logger; @@ -25,12 +27,14 @@ public BackOfficeConsumer( ILifetimeScope lifetimeScope, IHostApplicationLifetime hostApplicationLifetime, IDbContextFactory backOfficeContextFactory, + IParcels parcels, ILoggerFactory loggerFactory, IIdempotentConsumer kafkaIdemIdompotencyConsumer) { _lifetimeScope = lifetimeScope; _hostApplicationLifetime = hostApplicationLifetime; _backOfficeContextFactory = backOfficeContextFactory; + _parcels = parcels; _loggerFactory = loggerFactory; _kafkaIdemIdompotencyConsumer = kafkaIdemIdompotencyConsumer; @@ -45,7 +49,7 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken) var commandHandlingProjector = new ConnectedProjector( Resolve.WhenEqualToHandlerMessageType( - new CommandHandlingKafkaProjection(_backOfficeContextFactory).Handlers)); + new CommandHandlingKafkaProjection(_backOfficeContextFactory, _parcels).Handlers)); var commandHandler = new CommandHandler(_lifetimeScope, _loggerFactory); diff --git a/src/ParcelRegistry.Consumer.Address/Projections/CommandHandler.cs b/src/ParcelRegistry.Consumer.Address/Projections/CommandHandler.cs index 2f3a07cb..ba54f438 100644 --- a/src/ParcelRegistry.Consumer.Address/Projections/CommandHandler.cs +++ b/src/ParcelRegistry.Consumer.Address/Projections/CommandHandler.cs @@ -1,9 +1,11 @@ namespace ParcelRegistry.Consumer.Address.Projections { + using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; using Autofac; using Be.Vlaanderen.Basisregisters.CommandHandling; + using Be.Vlaanderen.Basisregisters.CommandHandling.Idempotency; using Be.Vlaanderen.Basisregisters.GrAr.Provenance; using Microsoft.Extensions.Logging; @@ -30,5 +32,22 @@ public virtual async Task Handle(T command, CancellationToken cancellationTok _logger.LogDebug($"Handled {command.GetType().FullName}"); } + + public virtual async Task HandleIdempotent(T command, CancellationToken cancellationToken) + where T : class, IHasCommandProvenance + { + _logger.LogDebug($"Idempotently handling {command.GetType().FullName}"); + + await using var scope = _container.BeginLifetimeScope(); + + var resolver = scope.Resolve(); + _ = await resolver.Dispatch( + command.CreateCommandId(), + command, + new Dictionary(), + cancellationToken: cancellationToken); + + _logger.LogDebug($"Idempotently handled {command.GetType().FullName}"); + } } } diff --git a/src/ParcelRegistry.Consumer.Address/Projections/CommandHandlingKafkaProjection.cs b/src/ParcelRegistry.Consumer.Address/Projections/CommandHandlingKafkaProjection.cs index 3126ad0a..3ad02e32 100644 --- a/src/ParcelRegistry.Consumer.Address/Projections/CommandHandlingKafkaProjection.cs +++ b/src/ParcelRegistry.Consumer.Address/Projections/CommandHandlingKafkaProjection.cs @@ -5,6 +5,7 @@ namespace ParcelRegistry.Consumer.Address.Projections using System.Threading; using System.Threading.Tasks; using Api.BackOffice.Abstractions; + using Be.Vlaanderen.Basisregisters.CommandHandling.Idempotency; using Be.Vlaanderen.Basisregisters.GrAr.Contracts.AddressRegistry; using Be.Vlaanderen.Basisregisters.GrAr.Provenance; using Be.Vlaanderen.Basisregisters.ProjectionHandling.Connector; @@ -19,7 +20,9 @@ public sealed class CommandHandlingKafkaProjection : ConnectedProjection _backOfficeContextFactory; - public CommandHandlingKafkaProjection(IDbContextFactory backOfficeContextFactory) + public CommandHandlingKafkaProjection( + IDbContextFactory backOfficeContextFactory, + IParcels parcels) { _backOfficeContextFactory = backOfficeContextFactory; @@ -159,26 +162,82 @@ await DetachBecauseRemoved( ct); }); - When(async (commandHandler, message, ct) => + When(async (commandHandler, message, ct) => { await using var backOfficeContext = await _backOfficeContextFactory.CreateDbContextAsync(ct); - await ReplaceBecauseOfReaddress( - commandHandler, - backOfficeContext, - message.ReaddressedHouseNumber, - message.Provenance, - ct); + var readdresses = message.ReaddressedHouseNumbers + .Select(x => new ReaddressData( + new AddressPersistentLocalId(x.ReaddressedHouseNumber.SourceAddressPersistentLocalId), + new AddressPersistentLocalId(x.ReaddressedHouseNumber.DestinationAddressPersistentLocalId))) + .Concat( + message.ReaddressedHouseNumbers + .SelectMany(x => x.ReaddressedBoxNumbers) + .Select(boxNumberAddress => new ReaddressData( + new AddressPersistentLocalId(boxNumberAddress.SourceAddressPersistentLocalId), + new AddressPersistentLocalId(boxNumberAddress.DestinationAddressPersistentLocalId)))) + .ToList(); + + var sourceAddressPersistentLocalIds = readdresses + .Select(x => (int)x.SourceAddressPersistentLocalId) + .ToList(); - foreach (var readdressedBoxNumber in message.ReaddressedBoxNumbers) + var sourceAddressParcelRelations = await backOfficeContext.ParcelAddressRelations + .AsNoTracking() + .Where(x => sourceAddressPersistentLocalIds.Contains(x.AddressPersistentLocalId)) + .ToListAsync(cancellationToken: ct); + + var commandByParcels = sourceAddressParcelRelations + .GroupBy( + relation => relation.ParcelId, + relation => readdresses.Where(x => x.SourceAddressPersistentLocalId == relation.AddressPersistentLocalId)) + .Select(x => new ReaddressAddresses( + new ParcelId(x.Key), + x.SelectMany(a => a), + FromProvenance(message.Provenance))) + .ToList(); + + foreach (var command in commandByParcels) { - await ReplaceBecauseOfReaddress( - commandHandler, - backOfficeContext, - readdressedBoxNumber, - message.Provenance, - ct); + try + { + await commandHandler.HandleIdempotent(command, ct); + } + catch (IdempotencyException) + { + // do nothing + } + } + + await backOfficeContext.Database.BeginTransactionAsync(); + + foreach (var parcelId in commandByParcels.Select(x => x.ParcelId)) + { + var parcel = await parcels.GetAsync(new ParcelStreamId(parcelId), ct); + + var backOfficeAddresses = (await backOfficeContext.ParcelAddressRelations + .AsNoTracking() + .Where(x => x.ParcelId == parcelId) + .Select(x => x.AddressPersistentLocalId) + .ToListAsync(cancellationToken: ct)) + .Select(x => new AddressPersistentLocalId(x)) + .ToList(); + + var addressesToRemove = backOfficeAddresses.Except(parcel.AddressPersistentLocalIds).ToList(); + var addressesToAdd = parcel.AddressPersistentLocalIds.Except(backOfficeAddresses).ToList(); + + foreach (var addressPersistentLocalId in addressesToRemove) + { + await backOfficeContext.RemoveIdempotentParcelAddressRelation(parcelId, addressPersistentLocalId, ct); + } + + foreach (var addressPersistentLocalId in addressesToAdd) + { + await backOfficeContext.AddIdempotentParcelAddressRelation(parcelId, addressPersistentLocalId, ct); + } } + + await backOfficeContext.Database.CommitTransactionAsync(); }); When(async (commandHandler, message, ct) => @@ -190,7 +249,6 @@ await DetachBecauseRejected( ct); }); - When(async (commandHandler, message, ct) => { await DetachBecauseRetired( @@ -201,37 +259,6 @@ await DetachBecauseRetired( }); } - private async Task ReplaceBecauseOfReaddress( - CommandHandler commandHandler, - BackOfficeContext backOfficeContext, - ReaddressedAddressData readdressedAddress, - Contracts.Provenance provenance, - CancellationToken ct) - { - var relations = backOfficeContext.ParcelAddressRelations - .AsNoTracking() - .Where(x => - x.AddressPersistentLocalId == readdressedAddress.SourceAddressPersistentLocalId) - .ToList(); - - foreach (var relation in relations) - { - var command = new ReplaceAttachedAddressBecauseAddressWasReaddressed( - new ParcelId(relation.ParcelId), - newAddressPersistentLocalId: new AddressPersistentLocalId(readdressedAddress.DestinationAddressPersistentLocalId), - previousAddressPersistentLocalId: new AddressPersistentLocalId(readdressedAddress.SourceAddressPersistentLocalId), - FromProvenance(provenance)); - - await commandHandler.Handle(command, ct); - - // This should only be handled by the back office projections to prevent conflicts. Else a relation is added or removed twice. - // await backOfficeContext.RemoveIdempotentParcelAddressRelation( - // command.ParcelId, new AddressPersistentLocalId(readdressedAddress.SourceAddressPersistentLocalId), ct); - // await backOfficeContext.AddIdempotentParcelAddressRelation( - // command.ParcelId, new AddressPersistentLocalId(readdressedAddress.DestinationAddressPersistentLocalId), ct); - } - } - private async Task DetachBecauseRemoved( CommandHandler commandHandler, AddressPersistentLocalId addressPersistentLocalId, @@ -264,9 +291,9 @@ private async Task DetachBecauseRetired( { await using var backOfficeContext = await _backOfficeContextFactory.CreateDbContextAsync(ct); var relations = backOfficeContext.ParcelAddressRelations - .AsNoTracking() - .Where(x => x.AddressPersistentLocalId == addressPersistentLocalId) - .ToList(); + .AsNoTracking() + .Where(x => x.AddressPersistentLocalId == addressPersistentLocalId) + .ToList(); foreach (var relation in relations) { diff --git a/src/ParcelRegistry.Producer.Snapshot.Oslo/ProducerProjections.cs b/src/ParcelRegistry.Producer.Snapshot.Oslo/ProducerProjections.cs index 3057fc3c..60414eb9 100644 --- a/src/ParcelRegistry.Producer.Snapshot.Oslo/ProducerProjections.cs +++ b/src/ParcelRegistry.Producer.Snapshot.Oslo/ProducerProjections.cs @@ -120,6 +120,20 @@ await snapshotManager.FindMatchingSnapshot( ct); }); + When>(async (_, message, ct) => + { + await FindAndProduce(async () => + await snapshotManager.FindMatchingSnapshot( + message.Message.CaPaKey, + message.Message.Provenance.Timestamp, + message.Message.GetHash(), + message.Position, + throwStaleWhenGone: false, + ct), + message.Position, + ct); + }); + When>(async (_, message, ct) => { await FindAndProduce(async () => diff --git a/src/ParcelRegistry.Producer/Extensions/MessageExtensions.cs b/src/ParcelRegistry.Producer/Extensions/MessageExtensions.cs index c32fca9e..4c88662a 100644 --- a/src/ParcelRegistry.Producer/Extensions/MessageExtensions.cs +++ b/src/ParcelRegistry.Producer/Extensions/MessageExtensions.cs @@ -1,5 +1,6 @@ namespace ParcelRegistry.Producer.Extensions { + using System.Linq; using Be.Vlaanderen.Basisregisters.GrAr.Provenance; using Contracts = Be.Vlaanderen.Basisregisters.GrAr.Contracts.ParcelRegistry; using Legacy = Legacy.Events; @@ -64,6 +65,16 @@ public static Contracts.ParcelAddressWasReplacedBecauseAddressWasReaddressed ToC message.PreviousAddressPersistentLocalId, message.Provenance.ToContract()); + public static Contracts.ParcelAddressesWereReaddressed ToContract(this ParcelAggregate.ParcelAddressesWereReaddressed message) => + new Contracts.ParcelAddressesWereReaddressed( + message.ParcelId.ToString("D"), + message.CaPaKey, + message.AttachedAddressPersistentLocalIds, + message.DetachedAddressPersistentLocalIds, + message.AddressRegistryReaddresses.Select(x => + new Contracts.AddressRegistryReaddress(x.SourceAddressPersistentLocalId, x.SourceAddressPersistentLocalId)), + message.Provenance.ToContract()); + public static Contracts.ParcelWasMigrated ToContract(this ParcelAggregate.ParcelWasMigrated message) => new Contracts.ParcelWasMigrated( message.OldParcelId.ToString("D"), diff --git a/src/ParcelRegistry.Producer/ProducerMigrateProjections.cs b/src/ParcelRegistry.Producer/ProducerMigrateProjections.cs index 844a5f9c..ca6de405 100644 --- a/src/ParcelRegistry.Producer/ProducerMigrateProjections.cs +++ b/src/ParcelRegistry.Producer/ProducerMigrateProjections.cs @@ -52,6 +52,11 @@ public ProducerMigrateProjections(IProducer producer) await Produce(message.Message.ParcelId, message.Message.ToContract(), message.Position, ct); }); + When>(async (_, message, ct) => + { + await Produce(message.Message.ParcelId, message.Message.ToContract(), message.Position, ct); + }); + When>(async (_, message, ct) => { await Produce(message.Message.ParcelId, message.Message.ToContract(), message.Position, ct); diff --git a/src/ParcelRegistry.Producer/ProducerProjections.cs b/src/ParcelRegistry.Producer/ProducerProjections.cs index 3969213b..31f54aa7 100644 --- a/src/ParcelRegistry.Producer/ProducerProjections.cs +++ b/src/ParcelRegistry.Producer/ProducerProjections.cs @@ -106,6 +106,11 @@ public ProducerProjections(IProducer producer) await Produce(message.Message.ParcelId, message.Message.ToContract(), message.Position, ct); }); + When>(async (_, message, ct) => + { + await Produce(message.Message.ParcelId, message.Message.ToContract(), message.Position, ct); + }); + When>(async (_, message, ct) => { await Produce(message.Message.ParcelId, message.Message.ToContract(), message.Position, ct); diff --git a/src/ParcelRegistry.Projections.BackOffice/BackOfficeProjections.cs b/src/ParcelRegistry.Projections.BackOffice/BackOfficeProjections.cs index 73890a48..0aaf132b 100644 --- a/src/ParcelRegistry.Projections.BackOffice/BackOfficeProjections.cs +++ b/src/ParcelRegistry.Projections.BackOffice/BackOfficeProjections.cs @@ -1,5 +1,6 @@ -namespace ParcelRegistry.Projections.BackOffice +namespace ParcelRegistry.Projections.BackOffice { + using System.Threading.Tasks; using Api.BackOffice.Abstractions; using Be.Vlaanderen.Basisregisters.ProjectionHandling.Connector; using Be.Vlaanderen.Basisregisters.ProjectionHandling.SqlStreamStore; @@ -106,6 +107,12 @@ await backOfficeContext.AddIdempotentParcelAddressRelation( newAddress.Count += 1; } }); + + When>((_, message, cancellationToken) => + { + // Do nothing + return Task.CompletedTask; + }); } } } diff --git a/src/ParcelRegistry.Projections.Extract/ParcelLinkExtractWithCount/ParcelLinkExtractProjections.cs b/src/ParcelRegistry.Projections.Extract/ParcelLinkExtractWithCount/ParcelLinkExtractProjections.cs index 0b2c57c7..9f369c25 100644 --- a/src/ParcelRegistry.Projections.Extract/ParcelLinkExtractWithCount/ParcelLinkExtractProjections.cs +++ b/src/ParcelRegistry.Projections.Extract/ParcelLinkExtractWithCount/ParcelLinkExtractProjections.cs @@ -7,7 +7,6 @@ namespace ParcelRegistry.Projections.Extract.ParcelLinkExtractWithCount using Be.Vlaanderen.Basisregisters.GrAr.Extracts; using Be.Vlaanderen.Basisregisters.ProjectionHandling.Connector; using Be.Vlaanderen.Basisregisters.ProjectionHandling.SqlStreamStore; - using Microsoft.Extensions.Options; using Parcel.Events; [ConnectedProjectionName("Extract perceelkoppelingen met adres")] @@ -18,7 +17,7 @@ public sealed class ParcelLinkExtractProjections : ConnectedProjection extractConfig, Encoding encoding) + public ParcelLinkExtractProjections(Encoding encoding) { _encoding = encoding ?? throw new ArgumentNullException(nameof(encoding)); @@ -130,6 +129,33 @@ await context newAddress.Count += 1; } }); + + When>(async (context, message, ct) => + { + foreach (var addressPersistentLocalId in message.Message.DetachedAddressPersistentLocalIds) + { + var relation = await context + .ParcelLinkExtractWithCount + .FindAsync([message.Message.ParcelId, addressPersistentLocalId], ct); + + if (relation is not null) + { + context.ParcelLinkExtractWithCount.Remove(relation); + } + } + + foreach (var addressPersistentLocalId in message.Message.AttachedAddressPersistentLocalIds) + { + var relation = await context + .ParcelLinkExtractWithCount + .FindAsync([message.Message.ParcelId, addressPersistentLocalId], ct); + + if (relation is not null) + { + await context.ParcelLinkExtractWithCount.AddAsync(relation, ct); + } + } + }); } private static async Task RemoveParcelLink( diff --git a/src/ParcelRegistry.Projections.Integration/ParcelLatestItem/ParcelLatestItemProjections.cs b/src/ParcelRegistry.Projections.Integration/ParcelLatestItem/ParcelLatestItemProjections.cs index ab53af92..bc5faccf 100644 --- a/src/ParcelRegistry.Projections.Integration/ParcelLatestItem/ParcelLatestItemProjections.cs +++ b/src/ParcelRegistry.Projections.Integration/ParcelLatestItem/ParcelLatestItemProjections.cs @@ -137,6 +137,38 @@ await context } }); + When>(async (context, message, ct) => + { + foreach (var addressPersistentLocalId in message.Message.DetachedAddressPersistentLocalIds) + { + var relation = await context + .ParcelLatestItemAddresses + .FindAsync([message.Message.ParcelId, addressPersistentLocalId], ct); + + if (relation is not null) + { + context.ParcelLatestItemAddresses.Remove(relation); + } + } + + foreach (var addressPersistentLocalId in message.Message.AttachedAddressPersistentLocalIds) + { + var relation = await context + .ParcelLatestItemAddresses + .FindAsync([message.Message.ParcelId, addressPersistentLocalId], ct); + + if (relation is null) + { + await context.ParcelLatestItemAddresses.AddAsync( + new ParcelLatestItemAddress( + message.Message.ParcelId, + addressPersistentLocalId, + message.Message.CaPaKey), + ct); + } + } + }); + When>(async (context, message, ct) => { var latestItemAddress = await context diff --git a/src/ParcelRegistry.Projections.Integration/ParcelVersion/ParcelVersionProjections.cs b/src/ParcelRegistry.Projections.Integration/ParcelVersion/ParcelVersionProjections.cs index 85ad4e5c..f75d995b 100644 --- a/src/ParcelRegistry.Projections.Integration/ParcelVersion/ParcelVersionProjections.cs +++ b/src/ParcelRegistry.Projections.Integration/ParcelVersion/ParcelVersionProjections.cs @@ -160,6 +160,44 @@ await context } }); + When>(async (context, message, ct) => + { + await context.CreateNewParcelVersion( + message.Message.ParcelId, + message, + _ => { }, ct); + + foreach (var addressPersistentLocalId in message.Message.DetachedAddressPersistentLocalIds) + { + var relation = await context + .ParcelVersionAddresses + .FindAsync([message.Position, message.Message.ParcelId, addressPersistentLocalId], ct); + + if (relation is not null) + { + context.ParcelVersionAddresses.Remove(relation); + } + } + + foreach (var addressPersistentLocalId in message.Message.AttachedAddressPersistentLocalIds) + { + var relation = await context + .ParcelVersionAddresses + .FindAsync([message.Position, message.Message.ParcelId, addressPersistentLocalId], ct); + + if (relation is null) + { + await context.ParcelVersionAddresses.AddAsync( + new ParcelVersionAddress( + message.Position, + message.Message.ParcelId, + addressPersistentLocalId, + message.Message.CaPaKey), + ct); + } + } + }); + When>(async (context, message, ct) => { await context.CreateNewParcelVersion( diff --git a/src/ParcelRegistry.Projections.LastChangedList/LastChangedListProjections.cs b/src/ParcelRegistry.Projections.LastChangedList/LastChangedListProjections.cs index f5cf0ccd..959af620 100644 --- a/src/ParcelRegistry.Projections.LastChangedList/LastChangedListProjections.cs +++ b/src/ParcelRegistry.Projections.LastChangedList/LastChangedListProjections.cs @@ -114,6 +114,11 @@ public LastChangedListProjections(ICacheValidator cacheValidator) await GetLastChangedRecordsAndUpdatePosition(message.Message.ParcelId.ToString(), message.Position, context, ct); }); + When>(async (context, message, ct) => + { + await GetLastChangedRecordsAndUpdatePosition(message.Message.ParcelId.ToString(), message.Position, context, ct); + }); + When>(async (context, message, ct) => { var records = await GetLastChangedRecordsAndUpdatePosition(message.Message.ParcelId.ToString(), message.Position, context, ct); diff --git a/src/ParcelRegistry.Projections.Legacy/ParcelDetailWithCountV2/ParcelDetailV2Projections.cs b/src/ParcelRegistry.Projections.Legacy/ParcelDetailWithCountV2/ParcelDetailV2Projections.cs index bc1b1ca6..d16c4a2d 100644 --- a/src/ParcelRegistry.Projections.Legacy/ParcelDetailWithCountV2/ParcelDetailV2Projections.cs +++ b/src/ParcelRegistry.Projections.Legacy/ParcelDetailWithCountV2/ParcelDetailV2Projections.cs @@ -211,6 +211,45 @@ await context.FindAndUpdateParcelDetail( ct); }); + When>(async (context, message, ct) => + { + await context.FindAndUpdateParcelDetail( + message.Message.ParcelId, + entity => + { + context.Entry(entity).Collection(x => x.Addresses).Load(); + + + foreach (var addressPersistentLocalId in message.Message.DetachedAddressPersistentLocalIds) + { + var relation = entity.Addresses.SingleOrDefault(parcelAddress => + parcelAddress.AddressPersistentLocalId == addressPersistentLocalId + && parcelAddress.ParcelId == message.Message.ParcelId); + + if (relation is not null) + { + entity.Addresses.Remove(relation); + } + } + + foreach (var addressPersistentLocalId in message.Message.AttachedAddressPersistentLocalIds) + { + var relation = entity.Addresses.SingleOrDefault(parcelAddress => + parcelAddress.AddressPersistentLocalId == addressPersistentLocalId + && parcelAddress.ParcelId == message.Message.ParcelId); + + if (relation is null) + { + entity.Addresses.Add(new ParcelDetailAddressV2(message.Message.ParcelId, addressPersistentLocalId)); + } + } + + UpdateHash(entity, message); + UpdateVersionTimestamp(entity, message.Message.Provenance.Timestamp); + }, + ct); + }); + When>(async (context, message, ct) => { var (geometryType, gml) = ToGml(message.Message.ExtendedWkbGeometry); diff --git a/src/ParcelRegistry.Projections.Legacy/ParcelSyndication/ParcelSyndicationProjections.cs b/src/ParcelRegistry.Projections.Legacy/ParcelSyndication/ParcelSyndicationProjections.cs index c1c5ac8a..ae2dc595 100755 --- a/src/ParcelRegistry.Projections.Legacy/ParcelSyndication/ParcelSyndicationProjections.cs +++ b/src/ParcelRegistry.Projections.Legacy/ParcelSyndication/ParcelSyndicationProjections.cs @@ -5,7 +5,6 @@ namespace ParcelRegistry.Projections.Legacy.ParcelSyndication using Be.Vlaanderen.Basisregisters.ProjectionHandling.Connector; using Be.Vlaanderen.Basisregisters.ProjectionHandling.SqlStreamStore; using Be.Vlaanderen.Basisregisters.Utilities.HexByteConvertor; - using NetTopologySuite.IO; using Parcel; using Parcel.Events; using ParcelRegistry.Legacy.Events; @@ -273,6 +272,26 @@ await context.CreateNewParcelSyndicationItem( }, ct); }); + + When>(async (context, message, ct) => + { + await context.CreateNewParcelSyndicationItem( + message.Message.ParcelId, + message, + x => + { + foreach (var addressPersistentLocalId in message.Message.DetachedAddressPersistentLocalIds) + { + x.RemoveAddressPersistentLocalId(addressPersistentLocalId); + } + + foreach (var addressPersistentLocalId in message.Message.AttachedAddressPersistentLocalIds) + { + x.AddAddressPersistentLocalId(addressPersistentLocalId); + } + }, + ct); + }); } private static async Task DoNothing() diff --git a/src/ParcelRegistry.Projector/Infrastructure/Modules/ApiModule.cs b/src/ParcelRegistry.Projector/Infrastructure/Modules/ApiModule.cs index 84aa01c7..f87ea993 100644 --- a/src/ParcelRegistry.Projector/Infrastructure/Modules/ApiModule.cs +++ b/src/ParcelRegistry.Projector/Infrastructure/Modules/ApiModule.cs @@ -102,9 +102,7 @@ private void RegisterExtractV2Projections(ContainerBuilder builder) DbaseCodePage.Western_European_ANSI.ToEncoding()), ConnectedProjectionSettings.Default) .RegisterProjections( - context => new ParcelLinkExtractWithCountProjections( - context.Resolve>(), - DbaseCodePage.Western_European_ANSI.ToEncoding()), + context => new ParcelLinkExtractWithCountProjections(DbaseCodePage.Western_European_ANSI.ToEncoding()), ConnectedProjectionSettings.Default); } diff --git a/src/ParcelRegistry/Parcel/AddressCommandHandlerModule.cs b/src/ParcelRegistry/Parcel/AddressCommandHandlerModule.cs index 02890c65..c0c9f85d 100644 --- a/src/ParcelRegistry/Parcel/AddressCommandHandlerModule.cs +++ b/src/ParcelRegistry/Parcel/AddressCommandHandlerModule.cs @@ -82,18 +82,16 @@ public AddressCommandHandlerModule( parcel.DetachAddressBecauseAddressWasRetired(message.Command.AddressPersistentLocalId); }); - For() + For() .AddSqlStreamStore(getStreamStore, getUnitOfWork, eventMapping, eventSerializer, getSnapshotStore) - .AddEventHash(getUnitOfWork) + .AddEventHash(getUnitOfWork) .AddProvenance(getUnitOfWork, provenanceFactory) .Handle(async (message, ct) => { var streamId = new ParcelStreamId(message.Command.ParcelId); var parcel = await parcelRepository().GetAsync(streamId, ct); - parcel.ReplaceAttachedAddressBecauseAddressWasReaddressed( - message.Command.NewAddressPersistentLocalId, - message.Command.PreviousAddressPersistentLocalId); + parcel.ReaddressAddresses(message.Command.Readdresses); }); } } diff --git a/src/ParcelRegistry/Parcel/Commands/ReaddressAddresses.cs b/src/ParcelRegistry/Parcel/Commands/ReaddressAddresses.cs new file mode 100644 index 00000000..edfa3633 --- /dev/null +++ b/src/ParcelRegistry/Parcel/Commands/ReaddressAddresses.cs @@ -0,0 +1,63 @@ +namespace ParcelRegistry.Parcel.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 class ReaddressAddresses : IHasCommandProvenance + { + private static readonly Guid Namespace = new Guid("646d3ef7-6cbc-4b33-b75f-e5d72e48c356"); + public ParcelId ParcelId { get; } + public IReadOnlyList Readdresses { get; } + public Provenance Provenance { get; } + + public ReaddressAddresses( + ParcelId parcelId, + IEnumerable readdresses, + Provenance provenance) + { + ParcelId = parcelId; + Readdresses = readdresses.ToList(); + Provenance = provenance; + } + + public Guid CreateCommandId() + => Deterministic.Create(Namespace, $"ReaddressAddresses-{ToString()}"); + + public override string? ToString() + => ToStringBuilder.ToString(IdentityFields()); + + private IEnumerable IdentityFields() + { + yield return ParcelId; + + foreach (var address in Readdresses) + { + yield return address.SourceAddressPersistentLocalId; + yield return address.DestinationAddressPersistentLocalId; + } + + foreach (var field in Provenance.GetIdentityFields()) + { + yield return field; + } + } + } + + public class ReaddressData + { + public AddressPersistentLocalId SourceAddressPersistentLocalId { get; } + public AddressPersistentLocalId DestinationAddressPersistentLocalId { get; } + + public ReaddressData( + AddressPersistentLocalId sourceAddressPersistentLocalId, + AddressPersistentLocalId destinationAddressPersistentLocalId) + { + SourceAddressPersistentLocalId = sourceAddressPersistentLocalId; + DestinationAddressPersistentLocalId = destinationAddressPersistentLocalId; + } + } +} diff --git a/src/ParcelRegistry/Parcel/Commands/ReplaceAttachedAddressBecauseAddressWasReaddressed.cs b/src/ParcelRegistry/Parcel/Commands/ReplaceAttachedAddressBecauseAddressWasReaddressed.cs deleted file mode 100644 index 9686f0b1..00000000 --- a/src/ParcelRegistry/Parcel/Commands/ReplaceAttachedAddressBecauseAddressWasReaddressed.cs +++ /dev/null @@ -1,50 +0,0 @@ -namespace ParcelRegistry.Parcel.Commands -{ - using System; - using System.Collections.Generic; - using Be.Vlaanderen.Basisregisters.Generators.Guid; - using Be.Vlaanderen.Basisregisters.GrAr.Provenance; - using Be.Vlaanderen.Basisregisters.Utilities; - using ParcelRegistry.Parcel; - - public sealed class ReplaceAttachedAddressBecauseAddressWasReaddressed : IHasCommandProvenance - { - private static readonly Guid Namespace = new Guid("35b8a5cf-df02-4350-b7f7-f9ebc506dfcb"); - - public ParcelId ParcelId { get; } - public AddressPersistentLocalId NewAddressPersistentLocalId { get; } - public AddressPersistentLocalId PreviousAddressPersistentLocalId { get; } - - public Provenance Provenance { get; } - - public ReplaceAttachedAddressBecauseAddressWasReaddressed( - ParcelId parcelId, - AddressPersistentLocalId newAddressPersistentLocalId, - AddressPersistentLocalId previousAddressPersistentLocalId, - Provenance provenance) - { - ParcelId = parcelId; - NewAddressPersistentLocalId = newAddressPersistentLocalId; - PreviousAddressPersistentLocalId = previousAddressPersistentLocalId; - Provenance = provenance; - } - - public Guid CreateCommandId() - => Deterministic.Create(Namespace, $"ReplaceAttachedAddressBecauseAddressWasReaddressed-{ToString()}"); - - public override string? ToString() - => ToStringBuilder.ToString(IdentityFields()); - - private IEnumerable IdentityFields() - { - yield return ParcelId; - yield return NewAddressPersistentLocalId; - yield return PreviousAddressPersistentLocalId; - - foreach (var field in Provenance.GetIdentityFields()) - { - yield return field; - } - } - } -} diff --git a/src/ParcelRegistry/Parcel/Events/ParcelAddressesWereReaddressed.cs b/src/ParcelRegistry/Parcel/Events/ParcelAddressesWereReaddressed.cs new file mode 100644 index 00000000..6e91c7a8 --- /dev/null +++ b/src/ParcelRegistry/Parcel/Events/ParcelAddressesWereReaddressed.cs @@ -0,0 +1,107 @@ +namespace ParcelRegistry.Parcel.Events +{ + using System; + using System.Collections.Generic; + using System.Linq; + using Be.Vlaanderen.Basisregisters.EventHandling; + using Be.Vlaanderen.Basisregisters.GrAr.Common; + using Be.Vlaanderen.Basisregisters.GrAr.Provenance; + using Commands; + using Newtonsoft.Json; + + [EventTags(EventTag.For.Sync, EventTag.For.Edit, Tag.Address)] + [EventName(EventName)] + [EventDescription("De adresherkoppelingen op het perceel door heradressering.")] + public sealed class ParcelAddressesWereReaddressed : IParcelEvent + { + public const string EventName = "ParcelAddressesWereReaddressed"; // BE CAREFUL CHANGING THIS!! + + [EventPropertyDescription("Interne GUID van het perceel.")] + public Guid ParcelId { get; } + + [EventPropertyDescription("CaPaKey (= objectidentificator) van het perceel, waarbij forward slashes vervangen zijn door koppeltekens i.f.v. gebruik in URI's.")] + public string CaPaKey { get; } + + [EventPropertyDescription("Objectidentificatoren van nieuw gekoppelde adressen.")] + public IEnumerable AttachedAddressPersistentLocalIds { get; } + + [EventPropertyDescription("Objectidentificatoren van ontkoppelde adressen.")] + public IEnumerable DetachedAddressPersistentLocalIds { get; } + + [EventPropertyDescription("De geheradresseerde adressen uit het Adressenregister.")] + public IEnumerable AddressRegistryReaddresses { get; } + + [EventPropertyDescription("Metadata bij het event.")] + public ProvenanceData Provenance { get; private set; } + + public ParcelAddressesWereReaddressed( + ParcelId parcelId, + VbrCaPaKey vbrCaPaKey, + IEnumerable attachedAddressPersistentLocalIds, + IEnumerable detachedAddressPersistentLocalIds, + IEnumerable addressRegistryReaddresses) + { + ParcelId = parcelId; + CaPaKey = vbrCaPaKey; + AttachedAddressPersistentLocalIds = attachedAddressPersistentLocalIds.Select(x => (int)x).ToList(); + DetachedAddressPersistentLocalIds = detachedAddressPersistentLocalIds.Select(x => (int)x).ToList(); + AddressRegistryReaddresses = addressRegistryReaddresses; + } + + [JsonConstructor] + private ParcelAddressesWereReaddressed( + Guid parcelId, + string caPaKey, + IEnumerable attachedAddressPersistentLocalIds, + IEnumerable detachedAddressPersistentLocalIds, + IEnumerable addressRegistryReaddresses, + ProvenanceData provenance) + : this( + new ParcelId(parcelId), + new VbrCaPaKey(caPaKey), + attachedAddressPersistentLocalIds.Select(x => new AddressPersistentLocalId(x)), + detachedAddressPersistentLocalIds.Select(x => new AddressPersistentLocalId(x)), + addressRegistryReaddresses) + => ((ISetProvenance)this).SetProvenance(provenance.ToProvenance()); + + void ISetProvenance.SetProvenance(Provenance provenance) => Provenance = new ProvenanceData(provenance); + + public IEnumerable GetHashFields() + { + var fields = Provenance.GetHashFields().ToList(); + fields.Add(ParcelId.ToString("D")); + fields.Add(CaPaKey); + fields.AddRange(AttachedAddressPersistentLocalIds.Select(addressPersistentLocalId => addressPersistentLocalId.ToString())); + fields.AddRange(DetachedAddressPersistentLocalIds.Select(addressPersistentLocalId => addressPersistentLocalId.ToString())); + + return fields; + } + + public string GetHash() => this.ToEventHash(EventName); + } + + public sealed class AddressRegistryReaddress + { + [EventPropertyDescription("Objectidentificator van het bronadres.")] + public int SourceAddressPersistentLocalId { get; } + + [EventPropertyDescription("Objectidentificator van het doeladres.")] + public int DestinationAddressPersistentLocalId { get; } + + public AddressRegistryReaddress( + ReaddressData readdressData) + { + SourceAddressPersistentLocalId = readdressData.SourceAddressPersistentLocalId; + DestinationAddressPersistentLocalId = readdressData.DestinationAddressPersistentLocalId; + } + + [JsonConstructor] + private AddressRegistryReaddress( + int sourceAddressPersistentLocalId, + int destinationAddressPersistentLocalId) + { + SourceAddressPersistentLocalId = sourceAddressPersistentLocalId; + DestinationAddressPersistentLocalId = destinationAddressPersistentLocalId; + } + } +} diff --git a/src/ParcelRegistry/Parcel/ParcelCommandHandlerModule.cs b/src/ParcelRegistry/Parcel/ParcelCommandHandlerModule.cs index fdf192f0..cd42267e 100644 --- a/src/ParcelRegistry/Parcel/ParcelCommandHandlerModule.cs +++ b/src/ParcelRegistry/Parcel/ParcelCommandHandlerModule.cs @@ -10,7 +10,6 @@ namespace ParcelRegistry.Parcel using Be.Vlaanderen.Basisregisters.GrAr.Provenance; using Commands; using Exceptions; - using Legacy.Commands; using SqlStreamStore; public sealed class ParcelCommandHandlerModule : CommandHandlerModule diff --git a/src/ParcelRegistry/Parcel/Parcel_Address.cs b/src/ParcelRegistry/Parcel/Parcel_Address.cs index 0f2f0d3a..398aee8e 100644 --- a/src/ParcelRegistry/Parcel/Parcel_Address.cs +++ b/src/ParcelRegistry/Parcel/Parcel_Address.cs @@ -1,6 +1,8 @@ -namespace ParcelRegistry.Parcel +namespace ParcelRegistry.Parcel { + using System.Collections.Generic; using System.Linq; + using Commands; using DataStructures; using Events; using Exceptions; @@ -85,21 +87,32 @@ public void DetachAddressBecauseAddressWasRetired(AddressPersistentLocalId addre ApplyChange(new ParcelAddressWasDetachedBecauseAddressWasRetired(ParcelId, CaPaKey, addressPersistentLocalId)); } - public void ReplaceAttachedAddressBecauseAddressWasReaddressed( - AddressPersistentLocalId addressPersistentLocalId, - AddressPersistentLocalId previousAddressPersistentLocalId) + public void ReaddressAddresses( + IReadOnlyList readdresses) { - if (AddressPersistentLocalIds.Contains(addressPersistentLocalId) - && !AddressPersistentLocalIds.Contains(previousAddressPersistentLocalId)) + var addressPersistentLocalIdsToAttach = readdresses + .Select(x => x.DestinationAddressPersistentLocalId) + .Except(readdresses.Select(x => x.SourceAddressPersistentLocalId)) + .Except(AddressPersistentLocalIds) + .ToList(); + + var addressPersistentLocalIdsToDetach = readdresses + .Select(x => x.SourceAddressPersistentLocalId) + .Except(readdresses.Select(x => x.DestinationAddressPersistentLocalId)) + .Where(AddressPersistentLocalIds.Contains) + .ToList(); + + if (!addressPersistentLocalIdsToAttach.Any() && !addressPersistentLocalIdsToDetach.Any()) { return; } - ApplyChange(new ParcelAddressWasReplacedBecauseAddressWasReaddressed( + ApplyChange(new ParcelAddressesWereReaddressed( ParcelId, CaPaKey, - addressPersistentLocalId, - previousAddressPersistentLocalId)); + addressPersistentLocalIdsToAttach, + addressPersistentLocalIdsToDetach, + readdresses.Select(x => new AddressRegistryReaddress(x)))); } } } diff --git a/src/ParcelRegistry/Parcel/Parcel_State.cs b/src/ParcelRegistry/Parcel/Parcel_State.cs index 93f26254..86e3093d 100644 --- a/src/ParcelRegistry/Parcel/Parcel_State.cs +++ b/src/ParcelRegistry/Parcel/Parcel_State.cs @@ -48,6 +48,7 @@ private Parcel() Register(When); Register(When); Register(When); + Register(When); Register(When); } @@ -146,6 +147,21 @@ private void When(ParcelAddressWasReplacedBecauseAddressWasReaddressed @event) _lastEvent = @event; } + private void When(ParcelAddressesWereReaddressed @event) + { + foreach (var addressPersistentLocalId in @event.DetachedAddressPersistentLocalIds) + { + _addressPersistentLocalIds.Remove(new AddressPersistentLocalId(addressPersistentLocalId)); + } + + foreach (var addressPersistentLocalId in @event.AttachedAddressPersistentLocalIds) + { + _addressPersistentLocalIds.Add(new AddressPersistentLocalId(addressPersistentLocalId)); + } + + _lastEvent = @event; + } + private void When(ParcelSnapshotV2 @event) { ParcelId = new ParcelId(@event.ParcelId); diff --git a/test/ParcelRegistry.Tests/AggregateTests/WhenReaddressingAddresses/GivenParcelExists.cs b/test/ParcelRegistry.Tests/AggregateTests/WhenReaddressingAddresses/GivenParcelExists.cs new file mode 100644 index 00000000..d1b6e5f4 --- /dev/null +++ b/test/ParcelRegistry.Tests/AggregateTests/WhenReaddressingAddresses/GivenParcelExists.cs @@ -0,0 +1,221 @@ +namespace ParcelRegistry.Tests.AggregateTests.WhenReaddressingAddresses +{ + using System; + using System.Collections.Generic; + using System.Linq; + using Autofac; + using AutoFixture; + using Be.Vlaanderen.Basisregisters.AggregateSource.Snapshotting; + using Be.Vlaanderen.Basisregisters.AggregateSource.Testing; + using Builders; + using EventExtensions; + using Fixtures; + using FluentAssertions; + using Parcel; + using Parcel.Commands; + using Parcel.Events; + using Xunit; + using Xunit.Abstractions; + + public class GivenParcelExists : ParcelRegistryTest + { + public GivenParcelExists(ITestOutputHelper testOutputHelper) : base(testOutputHelper) + { + Fixture.Customize(new WithFixedParcelId()); + Fixture.Customize(new WithParcelStatus()); + Fixture.Customize(new Legacy.AutoFixture.WithFixedParcelId()); + } + + [Fact] + public void WithSourceAddressAttachedAndDestinationAddressNotAttached_ThenAttachAndDetach() + { + var sourceAddressPersistentLocalId = new AddressPersistentLocalId(1); + var destinationAddressPersistentLocalId = new AddressPersistentLocalId(3); + + var command = new ReaddressAddressesBuilder(Fixture) + .WithReaddress(sourceAddressPersistentLocalId, destinationAddressPersistentLocalId) + .Build(); + + var parcelWasMigrated = new ParcelWasMigratedBuilder(Fixture) + .WithStatus(ParcelStatus.Realized) + .WithAddress(sourceAddressPersistentLocalId) + .WithAddress(2) + .Build(); + + Assert(new Scenario() + .Given( + new ParcelStreamId(command.ParcelId), + parcelWasMigrated) + .When(command) + .Then( + new ParcelStreamId(command.ParcelId), + new ParcelAddressesWereReaddressed( + command.ParcelId, + new VbrCaPaKey(parcelWasMigrated.CaPaKey), + new[] { destinationAddressPersistentLocalId }, + new[] { sourceAddressPersistentLocalId }, + command.Readdresses.Select(x => new AddressRegistryReaddress(x)).ToList() + ) + )); + } + + [Fact] + public void WithSourceAddressAttachedAndDestinationAddressAlreadyAttached_ThenOnlyDetach() + { + var sourceAddressPersistentLocalId = new AddressPersistentLocalId(1); + var destinationAddressPersistentLocalId = new AddressPersistentLocalId(3); + + var command = new ReaddressAddressesBuilder(Fixture) + .WithReaddress(sourceAddressPersistentLocalId, destinationAddressPersistentLocalId) + .Build(); + + var parcelWasMigrated = new ParcelWasMigratedBuilder(Fixture) + .WithStatus(ParcelStatus.Realized) + .WithAddress(2) + .WithAddress(sourceAddressPersistentLocalId) + .WithAddress(destinationAddressPersistentLocalId) + .Build(); + + Assert(new Scenario() + .Given( + new ParcelStreamId(command.ParcelId), + parcelWasMigrated) + .When(command) + .Then( + new ParcelStreamId(command.ParcelId), + new ParcelAddressesWereReaddressed( + command.ParcelId, + new VbrCaPaKey(parcelWasMigrated.CaPaKey), + Array.Empty(), + new[] { sourceAddressPersistentLocalId }, + command.Readdresses.Select(x => new AddressRegistryReaddress(x)).ToList() + ) + )); + } + + [Fact] + public void WithSourceAddressNotAttachedAndDestinationNotAttached_ThenOnlyAttach() + { + var sourceAddressPersistentLocalId = new AddressPersistentLocalId(1); + var destinationAddressPersistentLocalId = new AddressPersistentLocalId(3); + + var command = new ReaddressAddressesBuilder(Fixture) + .WithReaddress(sourceAddressPersistentLocalId, destinationAddressPersistentLocalId) + .Build(); + + var parcelWasMigrated = new ParcelWasMigratedBuilder(Fixture) + .WithStatus(ParcelStatus.Realized) + .WithAddress(2) + .Build(); + + Assert(new Scenario() + .Given( + new ParcelStreamId(command.ParcelId), + parcelWasMigrated) + .When(command) + .Then( + new ParcelStreamId(command.ParcelId), + new ParcelAddressesWereReaddressed( + command.ParcelId, + new VbrCaPaKey(parcelWasMigrated.CaPaKey), + new[] { destinationAddressPersistentLocalId }, + Array.Empty(), + command.Readdresses.Select(x => new AddressRegistryReaddress(x)).ToList() + ) + )); + } + + [Fact] + public void WithSourceAddressNotAttachedAndDestinationAddressAlreadyAttached_ThenNothing() + { + var sourceAddressPersistentLocalId = new AddressPersistentLocalId(1); + var destinationAddressPersistentLocalId = new AddressPersistentLocalId(3); + + var command = new ReaddressAddressesBuilder(Fixture) + .WithReaddress(sourceAddressPersistentLocalId, destinationAddressPersistentLocalId) + .Build(); + + var parcelWasMigrated = new ParcelWasMigratedBuilder(Fixture) + .WithStatus(ParcelStatus.Realized) + .WithAddress(2) + .WithAddress(destinationAddressPersistentLocalId) + .Build(); + + Assert(new Scenario() + .Given( + new ParcelStreamId(command.ParcelId), + parcelWasMigrated) + .When(command) + .ThenNone()); + } + + [Fact] + public void WithTwoReaddressesAndAddressIsBothSourceAndDestination_ThenOneAttachAndOneDetach() + { + var sourceAddressPersistentLocalId = new AddressPersistentLocalId(1); + var firstAddressPersistentLocalId = new AddressPersistentLocalId(3); + var secondAddressPersistentLocalId = new AddressPersistentLocalId(5); + + var command = new ReaddressAddressesBuilder(Fixture) + .WithReaddress(sourceAddressPersistentLocalId, firstAddressPersistentLocalId) + .WithReaddress(secondAddressPersistentLocalId, sourceAddressPersistentLocalId) + .Build(); + + var parcelWasMigrated = new ParcelWasMigratedBuilder(Fixture) + .WithStatus(ParcelStatus.Realized) + .WithAddress(secondAddressPersistentLocalId) + .WithAddress(sourceAddressPersistentLocalId) + .Build(); + + Assert(new Scenario() + .Given( + new ParcelStreamId(command.ParcelId), + parcelWasMigrated) + .When(command) + .Then( + new ParcelStreamId(command.ParcelId), + new ParcelAddressesWereReaddressed( + command.ParcelId, + new VbrCaPaKey(parcelWasMigrated.CaPaKey), + new[] { firstAddressPersistentLocalId }, + new[] { secondAddressPersistentLocalId }, + command.Readdresses.Select(x => new AddressRegistryReaddress(x)).ToList() + ) + )); + } + + [Fact] + public void StateCheck() + { + var sourceAddressPersistentLocalId = new AddressPersistentLocalId(1); + var destinationAddressPersistentLocalId = new AddressPersistentLocalId(2); + var otherAddressPersistentLocalId = new AddressPersistentLocalId(3); + + var parcelWasMigrated = new ParcelWasMigratedBuilder(Fixture) + .WithStatus(ParcelStatus.Realized) + .WithAddress(sourceAddressPersistentLocalId) + .WithAddress(otherAddressPersistentLocalId) + .Build(); + + var @event = new ParcelAddressesWereReaddressed( + Fixture.Create(), + Fixture.Create(), + new[] { destinationAddressPersistentLocalId }, + new[] { sourceAddressPersistentLocalId }, + new []{ new AddressRegistryReaddress(new ReaddressData(sourceAddressPersistentLocalId, destinationAddressPersistentLocalId)) } + ); + @event.SetFixtureProvenance(Fixture); + + // Act + var sut = new ParcelFactory(NoSnapshotStrategy.Instance, Container.Resolve()).Create(); + sut.Initialize(new List { parcelWasMigrated, @event }); + + // Assert + sut.AddressPersistentLocalIds.Should().HaveCount(2); + sut.AddressPersistentLocalIds.Should().Contain(destinationAddressPersistentLocalId); + sut.AddressPersistentLocalIds.Should().Contain(otherAddressPersistentLocalId); + sut.AddressPersistentLocalIds.Should().NotContain(sourceAddressPersistentLocalId); + sut.LastEventHash.Should().Be(@event.GetHash()); + } + } +} diff --git a/test/ParcelRegistry.Tests/AggregateTests/WhenReplacingAttachedAddressBecauseAddressWasReaddressed/GivenAddressAttached.cs b/test/ParcelRegistry.Tests/AggregateTests/WhenReplacingAttachedAddressBecauseAddressWasReaddressed/GivenAddressAttached.cs deleted file mode 100644 index 71c3c269..00000000 --- a/test/ParcelRegistry.Tests/AggregateTests/WhenReplacingAttachedAddressBecauseAddressWasReaddressed/GivenAddressAttached.cs +++ /dev/null @@ -1,77 +0,0 @@ -namespace ParcelRegistry.Tests.AggregateTests.WhenReplacingAttachedAddressBecauseAddressWasReaddressed -{ - using Be.Vlaanderen.Basisregisters.AggregateSource; - using Be.Vlaanderen.Basisregisters.AggregateSource.Testing; - using Builders; - using Fixtures; - using Parcel; - using Parcel.Events; - using Xunit; - using Xunit.Abstractions; - - public class GivenAddressAttached : ParcelRegistryTest - { - public GivenAddressAttached(ITestOutputHelper testOutputHelper) : base(testOutputHelper) - { - Fixture.Customize(new WithFixedParcelId()); - Fixture.Customize(new WithParcelStatus()); - Fixture.Customize(new Legacy.AutoFixture.WithFixedParcelId()); - } - - [Fact] - public void WithPreviousAddressAttached_ThenParcelAddressWasReplacedBecauseAddressWasReaddressed() - { - var previousAddressPersistentLocalId = new AddressPersistentLocalId(1); - var addressPersistentLocalId = new AddressPersistentLocalId(3); - - var command = new ReplaceAttachedAddressBecauseAddressWasReaddressedBuilder(Fixture) - .WithNewAddress(addressPersistentLocalId) - .WithPreviousAddress(previousAddressPersistentLocalId) - .Build(); - - var parcelWasMigrated = new ParcelWasMigratedBuilder(Fixture) - .WithStatus(ParcelStatus.Realized) - .WithAddress(previousAddressPersistentLocalId) - .WithAddress(2) - .WithAddress(addressPersistentLocalId) - .Build(); - - Assert(new Scenario() - .Given( - new ParcelStreamId(command.ParcelId), - parcelWasMigrated) - .When(command) - .Then(new Fact(new ParcelStreamId(command.ParcelId), - new ParcelAddressWasReplacedBecauseAddressWasReaddressed( - command.ParcelId, - new VbrCaPaKey(parcelWasMigrated.CaPaKey), - addressPersistentLocalId, - previousAddressPersistentLocalId)))); - } - - [Fact] - public void WithPreviousAddressNotAttached_ThenNothing() - { - var previousAddressPersistentLocalId = new AddressPersistentLocalId(1); - var addressPersistentLocalId = new AddressPersistentLocalId(3); - - var command = new ReplaceAttachedAddressBecauseAddressWasReaddressedBuilder(Fixture) - .WithNewAddress(addressPersistentLocalId) - .WithPreviousAddress(previousAddressPersistentLocalId) - .Build(); - - var parcelWasMigrated = new ParcelWasMigratedBuilder(Fixture) - .WithStatus(ParcelStatus.Realized) - .WithAddress(2) - .WithAddress(addressPersistentLocalId) - .Build(); - - Assert(new Scenario() - .Given( - new ParcelStreamId(command.ParcelId), - parcelWasMigrated) - .When(command) - .ThenNone()); - } - } -} diff --git a/test/ParcelRegistry.Tests/AggregateTests/WhenReplacingAttachedAddressBecauseAddressWasReaddressed/GivenAddressNotAttached.cs b/test/ParcelRegistry.Tests/AggregateTests/WhenReplacingAttachedAddressBecauseAddressWasReaddressed/GivenParcelExists.cs similarity index 68% rename from test/ParcelRegistry.Tests/AggregateTests/WhenReplacingAttachedAddressBecauseAddressWasReaddressed/GivenAddressNotAttached.cs rename to test/ParcelRegistry.Tests/AggregateTests/WhenReplacingAttachedAddressBecauseAddressWasReaddressed/GivenParcelExists.cs index 7a160f25..a189c38d 100644 --- a/test/ParcelRegistry.Tests/AggregateTests/WhenReplacingAttachedAddressBecauseAddressWasReaddressed/GivenAddressNotAttached.cs +++ b/test/ParcelRegistry.Tests/AggregateTests/WhenReplacingAttachedAddressBecauseAddressWasReaddressed/GivenParcelExists.cs @@ -3,14 +3,11 @@ namespace ParcelRegistry.Tests.AggregateTests.WhenReplacingAttachedAddressBecaus using System.Collections.Generic; using System.Linq; using Autofac; - using Be.Vlaanderen.Basisregisters.AggregateSource; using Be.Vlaanderen.Basisregisters.AggregateSource.Snapshotting; - using Be.Vlaanderen.Basisregisters.AggregateSource.Testing; using Builders; using Fixtures; using FluentAssertions; using Parcel; - using Parcel.Events; using Xunit; using Xunit.Abstractions; @@ -23,66 +20,6 @@ public GivenAddressNotAttached(ITestOutputHelper testOutputHelper) : base(testOu Fixture.Customize(new Legacy.AutoFixture.WithFixedParcelId()); } - [Fact] - public void WithPreviousAddressAttached_ThenParcelAddressWasReplacedBecauseAddressWasReaddressed() - { - var previousAddressPersistentLocalId = new AddressPersistentLocalId(1); - var addressPersistentLocalId = new AddressPersistentLocalId(3); - - var command = new ReplaceAttachedAddressBecauseAddressWasReaddressedBuilder(Fixture) - .WithNewAddress(addressPersistentLocalId) - .WithPreviousAddress(previousAddressPersistentLocalId) - .Build(); - - var parcelWasMigrated = new ParcelWasMigratedBuilder(Fixture) - .WithStatus(ParcelStatus.Realized) - .WithAddress(previousAddressPersistentLocalId) - .WithAddress(2) - .Build(); - - Assert(new Scenario() - .Given( - new ParcelStreamId(command.ParcelId), - parcelWasMigrated) - .When(command) - .Then(new Fact(new ParcelStreamId(command.ParcelId), - new ParcelAddressWasReplacedBecauseAddressWasReaddressed( - command.ParcelId, - new VbrCaPaKey(parcelWasMigrated.CaPaKey), - addressPersistentLocalId, - previousAddressPersistentLocalId)))); - } - - [Fact] - public void WithPreviousAddressNotAttached_ThenParcelAddressWasReplacedBecauseAddressWasReaddressed() - { - var previousAddressPersistentLocalId = new AddressPersistentLocalId(1); - var addressPersistentLocalId = new AddressPersistentLocalId(3); - - var command = new ReplaceAttachedAddressBecauseAddressWasReaddressedBuilder(Fixture) - .WithNewAddress(addressPersistentLocalId) - .WithPreviousAddress(previousAddressPersistentLocalId) - .Build(); - - var parcelWasMigrated = new ParcelWasMigratedBuilder(Fixture) - .WithParcelId(command.ParcelId) - .WithStatus(ParcelStatus.Realized) - .WithAddress(2) - .Build(); - - Assert(new Scenario() - .Given( - new ParcelStreamId(command.ParcelId), - parcelWasMigrated) - .When(command) - .Then(new Fact(new ParcelStreamId(command.ParcelId), - new ParcelAddressWasReplacedBecauseAddressWasReaddressed( - command.ParcelId, - new VbrCaPaKey(parcelWasMigrated.CaPaKey), - addressPersistentLocalId, - previousAddressPersistentLocalId)))); - } - [Fact] public void StateCheck_OnlyPreviousWasAttached() { diff --git a/test/ParcelRegistry.Tests/Builders/ParcelAddressesWereReaddressedBuilder.cs b/test/ParcelRegistry.Tests/Builders/ParcelAddressesWereReaddressedBuilder.cs new file mode 100644 index 00000000..29c9b944 --- /dev/null +++ b/test/ParcelRegistry.Tests/Builders/ParcelAddressesWereReaddressedBuilder.cs @@ -0,0 +1,58 @@ +namespace ParcelRegistry.Tests.Builders +{ + using System.Collections.Generic; + using AutoFixture; + using EventExtensions; + using Parcel; + using Parcel.Commands; + using Parcel.Events; + + public class ParcelAddressesWereReaddressedBuilder(Fixture fixture) + { + private readonly List _attachedAddressPersistentLocalIds = []; + private readonly List _detachedAddressPersistentLocalIds = []; + private readonly List _addressRegistryReaddresses = []; + + public ParcelAddressesWereReaddressedBuilder WithAttachedAddress(int addressPersistentLocalid) + { + _attachedAddressPersistentLocalIds.Add(new AddressPersistentLocalId(addressPersistentLocalid)); + return this; + } + + public ParcelAddressesWereReaddressedBuilder WithDetachedAddress(int addressPersistentLocalid) + { + _detachedAddressPersistentLocalIds.Add(new AddressPersistentLocalId(addressPersistentLocalid)); + return this; + } + + public ParcelAddressesWereReaddressedBuilder WithReaddress( + int sourceAddressPersistentLocalId, + int destinationAddressPersistentLocalId) + { + return WithReaddress(new AddressRegistryReaddress( + new ReaddressData( + new AddressPersistentLocalId(sourceAddressPersistentLocalId), + new AddressPersistentLocalId(destinationAddressPersistentLocalId)))); + } + + public ParcelAddressesWereReaddressedBuilder WithReaddress(AddressRegistryReaddress addressRegistryReaddress) + { + _addressRegistryReaddresses.Add(addressRegistryReaddress); + return this; + } + + public ParcelAddressesWereReaddressed Build() + { + var @event = new ParcelAddressesWereReaddressed( + fixture.Create(), + fixture.Create(), + _attachedAddressPersistentLocalIds, + _detachedAddressPersistentLocalIds, + _addressRegistryReaddresses); + + @event.SetFixtureProvenance(fixture); + + return @event; + } + } +} diff --git a/test/ParcelRegistry.Tests/Builders/ReaddressAddressesBuilder.cs b/test/ParcelRegistry.Tests/Builders/ReaddressAddressesBuilder.cs new file mode 100644 index 00000000..30c9e2b0 --- /dev/null +++ b/test/ParcelRegistry.Tests/Builders/ReaddressAddressesBuilder.cs @@ -0,0 +1,44 @@ +namespace ParcelRegistry.Tests.Builders +{ + using System.Collections.Generic; + using AutoFixture; + using Be.Vlaanderen.Basisregisters.GrAr.Provenance; + using Parcel; + using Parcel.Commands; + + public class ReaddressAddressesBuilder + { + private readonly Fixture _fixture; + private ParcelId? _parcelId; + private readonly List _readdresses = []; + + public ReaddressAddressesBuilder(Fixture fixture) + { + _fixture = fixture; + } + + public ReaddressAddressesBuilder WithParcelId(ParcelId parcelId) + { + _parcelId = parcelId; + + return this; + } + + public ReaddressAddressesBuilder WithReaddress(int sourceAddressPersistentLocalId, int destinationAddressPersistentLocalId) + { + _readdresses.Add(new ReaddressData( + new AddressPersistentLocalId(sourceAddressPersistentLocalId), + new AddressPersistentLocalId(destinationAddressPersistentLocalId))); + + return this; + } + + public ReaddressAddresses Build() + { + return new ReaddressAddresses( + _parcelId ?? _fixture.Create(), + _readdresses, + _fixture.Create()); + } + } +} diff --git a/test/ParcelRegistry.Tests/Builders/ReplaceAttachedAddressBecauseAddressWasReaddressedBuilder.cs b/test/ParcelRegistry.Tests/Builders/ReplaceAttachedAddressBecauseAddressWasReaddressedBuilder.cs deleted file mode 100644 index 73f03f80..00000000 --- a/test/ParcelRegistry.Tests/Builders/ReplaceAttachedAddressBecauseAddressWasReaddressedBuilder.cs +++ /dev/null @@ -1,50 +0,0 @@ -namespace ParcelRegistry.Tests.Builders -{ - using AutoFixture; - using Be.Vlaanderen.Basisregisters.GrAr.Provenance; - using Parcel; - using Parcel.Commands; - - public class ReplaceAttachedAddressBecauseAddressWasReaddressedBuilder - { - private readonly Fixture _fixture; - private ParcelId? _parcelId; - private AddressPersistentLocalId? _newAddressPersistentLocalId; - private AddressPersistentLocalId? _previousAddressPersistentLocalId; - - public ReplaceAttachedAddressBecauseAddressWasReaddressedBuilder(Fixture fixture) - { - _fixture = fixture; - } - - public ReplaceAttachedAddressBecauseAddressWasReaddressedBuilder WithParcelId(ParcelId parcelId) - { - _parcelId = parcelId; - - return this; - } - - public ReplaceAttachedAddressBecauseAddressWasReaddressedBuilder WithNewAddress(int address) - { - _newAddressPersistentLocalId = new AddressPersistentLocalId(address); - - return this; - } - - public ReplaceAttachedAddressBecauseAddressWasReaddressedBuilder WithPreviousAddress(int address) - { - _previousAddressPersistentLocalId = new AddressPersistentLocalId(address); - - return this; - } - - public ReplaceAttachedAddressBecauseAddressWasReaddressed Build() - { - return new ReplaceAttachedAddressBecauseAddressWasReaddressed( - _parcelId ?? _fixture.Create(), - _newAddressPersistentLocalId ?? _fixture.Create(), - _previousAddressPersistentLocalId ?? _fixture.Create(), - _fixture.Create()); - } - } -} diff --git a/test/ParcelRegistry.Tests/Fixtures/WithUniqueInteger.cs b/test/ParcelRegistry.Tests/Fixtures/WithUniqueInteger.cs new file mode 100644 index 00000000..f77c9f7c --- /dev/null +++ b/test/ParcelRegistry.Tests/Fixtures/WithUniqueInteger.cs @@ -0,0 +1,23 @@ +namespace ParcelRegistry.Tests.Fixtures +{ + using System; + using AutoFixture.Kernel; + + public class WithUniqueInteger : ISpecimenBuilder + { + private int _lastInt; + + public object Create(object request, ISpecimenContext context) + { + if (request is not Type type || type != typeof(int)) + { + return new NoSpecimen(); + } + + var nextInt = _lastInt + 1; + _lastInt = nextInt; + + return nextInt; + } + } +} diff --git a/test/ParcelRegistry.Tests/InfrastructureEventsTests.cs b/test/ParcelRegistry.Tests/InfrastructureEventsTests.cs index 5b6b98ae..5999e144 100644 --- a/test/ParcelRegistry.Tests/InfrastructureEventsTests.cs +++ b/test/ParcelRegistry.Tests/InfrastructureEventsTests.cs @@ -5,10 +5,11 @@ namespace Be.Vlaanderen.Basisregisters.Testing.Infrastructure.Events using System.Linq; using System.Reflection; using System.Runtime.CompilerServices; - using Be.Vlaanderen.Basisregisters.AggregateSource; - using Be.Vlaanderen.Basisregisters.EventHandling; + using AggregateSource; + using EventHandling; using FluentAssertions; using Newtonsoft.Json; + using ParcelRegistry.Parcel.Events; using Xunit; /// @@ -36,7 +37,9 @@ public InfrastructureEventsTests() _eventTypes = domainAssembly .GetTypes() - .Where(t => t.IsClass && t.Namespace != null && IsEventNamespace(t) && IsNotCompilerGenerated(t)); + .Where(t => + t.IsClass && t.Namespace != null && IsEventNamespace(t) && IsNotCompilerGenerated(t) + && t != typeof(AddressRegistryReaddress)); } [Fact] diff --git a/test/ParcelRegistry.Tests/ProjectionTests/BackOffice/ParcelBackOfficeProjectionsTests-Readdress.cs b/test/ParcelRegistry.Tests/ProjectionTests/BackOffice/ParcelBackOfficeProjectionsTests-Readdress.cs index 2f5c00a1..e6a4fd1b 100644 --- a/test/ParcelRegistry.Tests/ProjectionTests/BackOffice/ParcelBackOfficeProjectionsTests-Readdress.cs +++ b/test/ParcelRegistry.Tests/ProjectionTests/BackOffice/ParcelBackOfficeProjectionsTests-Readdress.cs @@ -1,10 +1,13 @@ -namespace ParcelRegistry.Tests.ProjectionTests.BackOffice +namespace ParcelRegistry.Tests.ProjectionTests.BackOffice { using System.Threading; using System.Threading.Tasks; using Api.BackOffice.Abstractions; using AutoFixture; + using Builders; + using Fixtures; using FluentAssertions; + using Parcel; using Parcel.Events; using Xunit; diff --git a/test/ParcelRegistry.Tests/ProjectionTests/Consumer.Address/CommandHandlingKafkaProjectionTests-Readdress.cs b/test/ParcelRegistry.Tests/ProjectionTests/Consumer.Address/CommandHandlingKafkaProjectionTests-Readdress.cs new file mode 100644 index 00000000..9925aeb6 --- /dev/null +++ b/test/ParcelRegistry.Tests/ProjectionTests/Consumer.Address/CommandHandlingKafkaProjectionTests-Readdress.cs @@ -0,0 +1,177 @@ +namespace ParcelRegistry.Tests.ProjectionTests.Consumer.Address +{ + using System; + using System.Collections.Generic; + using System.Linq; + using System.Threading; + using System.Threading.Tasks; + using Autofac; + using AutoFixture; + using Be.Vlaanderen.Basisregisters.AggregateSource.Snapshotting; + using Be.Vlaanderen.Basisregisters.GrAr.Contracts.AddressRegistry; + using Be.Vlaanderen.Basisregisters.GrAr.Provenance; + using EventExtensions; + using FluentAssertions; + using Moq; + using NodaTime; + using Parcel; + using Parcel.Commands; + using Parcel.Events; + using Xunit; + using Provenance = Be.Vlaanderen.Basisregisters.GrAr.Contracts.Common.Provenance; + + public partial class CommandHandlingKafkaProjectionTests + { + [Fact] + public async Task AttachAndDetachAddressesWhenStreetNameWasReaddressed() + { + var parcelOneId = Fixture.Create(); + var parcelTwoId = Fixture.Create(); + + var sourceAddressPersistentLocalIdOne = 1; + var sourceAddressPersistentLocalIdTwo = 2; + var sourceAddressPersistentLocalIdThree = 5; + var unattachedSourceAddressPersistentLocalIdOne = 20; + var destinationAddressPersistentLocalIdOne = 10; + var destinationAddressPersistentLocalIdTwo = 11; + var destinationAddressPersistentLocalIdThree = 12; + + var parcelOneAddressPersistentLocalIds = new[] { sourceAddressPersistentLocalIdOne, sourceAddressPersistentLocalIdTwo, 3 }; + var parcelTwoAddressPersistentLocalIds = new[] { 4, sourceAddressPersistentLocalIdThree }; + + var parcelOneExpectedAddressPersistentLocalIds = + new[] { destinationAddressPersistentLocalIdOne, destinationAddressPersistentLocalIdTwo, 3 }; + var parcelTwoExpectedAddressPersistentLocalIds = new[] { 4, destinationAddressPersistentLocalIdThree }; + + // Setup BackofficeContext + AddParcelAddressRelations(parcelOneId, parcelOneAddressPersistentLocalIds); + AddParcelAddressRelations(parcelTwoId, parcelTwoAddressPersistentLocalIds); + AddParcelAddressRelations(Fixture.Create(), [6, 7, 8]); + + // Setup domain + SetupParcelWithAddresses(parcelOneId, parcelOneExpectedAddressPersistentLocalIds); + SetupParcelWithAddresses(parcelTwoId, parcelTwoExpectedAddressPersistentLocalIds); + + // Act + var @event = new StreetNameWasReaddressed( + Fixture.Create(), + new[] + { + new AddressHouseNumberReaddressedData( + destinationAddressPersistentLocalIdOne, + CreateReaddressedAddressData(sourceAddressPersistentLocalIdOne, destinationAddressPersistentLocalIdOne), + new[] + { + CreateReaddressedAddressData(sourceAddressPersistentLocalIdTwo, destinationAddressPersistentLocalIdTwo), + CreateReaddressedAddressData(unattachedSourceAddressPersistentLocalIdOne, 21) + }), + new AddressHouseNumberReaddressedData( + destinationAddressPersistentLocalIdThree, + CreateReaddressedAddressData(sourceAddressPersistentLocalIdThree, destinationAddressPersistentLocalIdThree), + []) + }, + new Provenance( + Instant.FromDateTimeOffset(DateTimeOffset.Now).ToString(), + Application.ParcelRegistry.ToString(), + Modification.Update.ToString(), + Organisation.Aiv.ToString(), + "test") + ); + + Given(@event); + + // Assert + await Then(async _ => + { + _mockCommandHandler.Verify(x => + x.HandleIdempotent( + It.Is(y => + y.ParcelId == parcelOneId + && y.Readdresses.Count == 2 + && y.Readdresses.Any(z => + z.SourceAddressPersistentLocalId == sourceAddressPersistentLocalIdOne + && z.DestinationAddressPersistentLocalId == destinationAddressPersistentLocalIdOne) + && y.Readdresses.Any(z => + z.SourceAddressPersistentLocalId == sourceAddressPersistentLocalIdTwo + && z.DestinationAddressPersistentLocalId == destinationAddressPersistentLocalIdTwo)), + CancellationToken.None), + Times.Once); + _mockCommandHandler.Verify(x => + x.HandleIdempotent( + It.Is(y => + y.ParcelId == parcelTwoId + && y.Readdresses.Count == 1 + && y.Readdresses.Any(z => + z.SourceAddressPersistentLocalId == sourceAddressPersistentLocalIdThree + && z.DestinationAddressPersistentLocalId == destinationAddressPersistentLocalIdThree)), + CancellationToken.None), + Times.Once); + + _mockCommandHandler.Invocations.Count.Should().Be(2); + + var parcelOneRelations = _fakeBackOfficeContext.ParcelAddressRelations + .Where(x => x.ParcelId == parcelOneId) + .ToList(); + parcelOneRelations.Count.Should().Be(parcelOneExpectedAddressPersistentLocalIds.Length); + foreach (var addressPersistentLocalId in parcelOneExpectedAddressPersistentLocalIds) + { + var expectedRelation = + parcelOneRelations.SingleOrDefault(x => x.AddressPersistentLocalId == addressPersistentLocalId); + expectedRelation.Should().NotBeNull(); + expectedRelation!.Count.Should().Be(1); + } + + var parcelTwoRelations = _fakeBackOfficeContext.ParcelAddressRelations + .Where(x => x.ParcelId == parcelTwoId) + .ToList(); + parcelTwoRelations.Count.Should().Be(parcelTwoExpectedAddressPersistentLocalIds.Length); + foreach (var addressPersistentLocalId in parcelTwoExpectedAddressPersistentLocalIds) + { + var expectedRelation = + parcelTwoRelations.SingleOrDefault(x => x.AddressPersistentLocalId == addressPersistentLocalId); + expectedRelation.Should().NotBeNull(); + expectedRelation!.Count.Should().Be(1); + } + + await Task.CompletedTask; + }); + } + + private void SetupParcelWithAddresses(ParcelId parcelId, IEnumerable addressPersistentLocalIds) + { + var parcel = new ParcelFactory(NoSnapshotStrategy.Instance, Container.Resolve()).Create(); + var events = addressPersistentLocalIds + .Select(addressPersistentLocalId => + { + var parcelAddressWasAttached = new ParcelAddressWasAttachedV2( + parcelId, Fixture.Create(), new AddressPersistentLocalId(addressPersistentLocalId)); + parcelAddressWasAttached.SetFixtureProvenance(Fixture); + return parcelAddressWasAttached; + }) + .ToList(); + parcel.Initialize(events); + + _parcels + .Setup(x => x.GetAsync(new ParcelStreamId(parcelId), It.IsAny())) + .ReturnsAsync(parcel); + } + + private ReaddressedAddressData CreateReaddressedAddressData( + int sourceAddressPersistentLocalIdOne, + int destinationAddressPersistentLocalIdOne) + { + return new ReaddressedAddressData( + sourceAddressPersistentLocalIdOne, + destinationAddressPersistentLocalIdOne, + Fixture.Create(), + Fixture.Create(), + Fixture.Create(), + Fixture.Create(), + Fixture.Create(), + Fixture.Create(), + Fixture.Create(), + Fixture.Create(), + Fixture.Create()); + } + } +} diff --git a/test/ParcelRegistry.Tests/ProjectionTests/Consumer.Address/CommandHandlingKafkaProjectionTests.cs b/test/ParcelRegistry.Tests/ProjectionTests/Consumer.Address/CommandHandlingKafkaProjectionTests.cs index f183d4d7..8c1e2120 100644 --- a/test/ParcelRegistry.Tests/ProjectionTests/Consumer.Address/CommandHandlingKafkaProjectionTests.cs +++ b/test/ParcelRegistry.Tests/ProjectionTests/Consumer.Address/CommandHandlingKafkaProjectionTests.cs @@ -2,19 +2,25 @@ namespace ParcelRegistry.Tests.ProjectionTests.Consumer.Address { using System; using System.Collections.Generic; + using System.Linq; using System.Threading; using System.Threading.Tasks; using Api.BackOffice.Abstractions; + using Autofac; using AutoFixture; + using Be.Vlaanderen.Basisregisters.AggregateSource.Snapshotting; using Be.Vlaanderen.Basisregisters.GrAr.Contracts.AddressRegistry; using Be.Vlaanderen.Basisregisters.GrAr.Provenance; + using EventExtensions; using Fixtures; + using FluentAssertions; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging.Abstractions; using Moq; using NodaTime; using Parcel; using Parcel.Commands; + using Parcel.Events; using ParcelRegistry.Consumer.Address; using ParcelRegistry.Consumer.Address.Projections; using Tests.BackOffice; @@ -22,17 +28,19 @@ namespace ParcelRegistry.Tests.ProjectionTests.Consumer.Address using Xunit.Abstractions; using Provenance = Be.Vlaanderen.Basisregisters.GrAr.Contracts.Common.Provenance; - public sealed class CommandHandlingKafkaProjectionTests : KafkaProjectionTest + public partial class CommandHandlingKafkaProjectionTests : KafkaProjectionTest { private readonly FakeBackOfficeContext _fakeBackOfficeContext; private readonly Mock _mockCommandHandler; + private readonly Mock _parcels; public CommandHandlingKafkaProjectionTests(ITestOutputHelper testOutputHelper) : base(testOutputHelper) { Fixture.Customize(new InfrastructureCustomization()); _mockCommandHandler = new Mock(); - _fakeBackOfficeContext = new FakeBackOfficeContextFactory().CreateDbContext(Array.Empty()); + _fakeBackOfficeContext = new FakeBackOfficeContextFactory(dispose: false).CreateDbContext([]); + _parcels = new Mock(); } [Fact] @@ -63,12 +71,14 @@ public async Task DetachAddressBecauseRemovedAddressWasMigrated() Organisation.Aiv.ToString(), "test")); - AddRelations(addressPersistentLocalId,addressPersistentLocalId); + AddParcelAddressRelations(Fixture.Create(), [addressPersistentLocalId]); + AddParcelAddressRelations(Fixture.Create(), [addressPersistentLocalId]); Given(@event); await Then(async _ => { - _mockCommandHandler.Verify(x => x.Handle(It.IsAny(), CancellationToken.None), Times.Exactly(2)); + _mockCommandHandler.Verify(x => + x.Handle(It.IsAny(), CancellationToken.None), Times.Exactly(2)); await Task.CompletedTask; }); } @@ -101,12 +111,14 @@ public async Task DetachAddressBecauseRejectedAddressWasMigrated() Organisation.Aiv.ToString(), "test")); - AddRelations(addressPersistentLocalId, addressPersistentLocalId); + AddParcelAddressRelations(Fixture.Create(), [addressPersistentLocalId]); + AddParcelAddressRelations(Fixture.Create(), [addressPersistentLocalId]); Given(@event); await Then(async _ => { - _mockCommandHandler.Verify(x => x.Handle(It.IsAny(), CancellationToken.None), Times.Exactly(2)); + _mockCommandHandler.Verify(x => + x.Handle(It.IsAny(), CancellationToken.None), Times.Exactly(2)); await Task.CompletedTask; }); } @@ -139,12 +151,14 @@ public async Task DetachAddressBecauseRetiredAddressWasMigrated() Organisation.Aiv.ToString(), "test")); - AddRelations(addressPersistentLocalId, addressPersistentLocalId); + AddParcelAddressRelations(Fixture.Create(), [addressPersistentLocalId]); + AddParcelAddressRelations(Fixture.Create(), [addressPersistentLocalId]); Given(@event); await Then(async _ => { - _mockCommandHandler.Verify(x => x.Handle(It.IsAny(), CancellationToken.None), Times.Exactly(2)); + _mockCommandHandler.Verify(x => + x.Handle(It.IsAny(), CancellationToken.None), Times.Exactly(2)); await Task.CompletedTask; }); } @@ -179,17 +193,18 @@ public async Task DoNothingWhenAddressStatus(string status) Organisation.Aiv.ToString(), "test")); - AddRelations(addressPersistentLocalId, addressPersistentLocalId); + AddParcelAddressRelations(Fixture.Create(), [addressPersistentLocalId]); + AddParcelAddressRelations(Fixture.Create(), [addressPersistentLocalId]); Given(@event); await Then(async _ => { - _mockCommandHandler.Verify(x => x.Handle(It.IsAny(), CancellationToken.None), - Times.Never); - _mockCommandHandler.Verify(x => x.Handle(It.IsAny(), CancellationToken.None), - Times.Never); - _mockCommandHandler.Verify(x => x.Handle(It.IsAny(), CancellationToken.None), - Times.Never); + _mockCommandHandler.Verify(x => + x.Handle(It.IsAny(), CancellationToken.None), Times.Never); + _mockCommandHandler.Verify(x => + x.Handle(It.IsAny(), CancellationToken.None), Times.Never); + _mockCommandHandler.Verify(x => + x.Handle(It.IsAny(), CancellationToken.None), Times.Never); await Task.CompletedTask; }); } @@ -197,11 +212,11 @@ await Then(async _ => [Fact] public async Task DetachAddressBecauseAddressWasRemoved() { - var addressIntId = 456; + var addressPersistentLocalId = 456; var @event = new AddressWasRemovedV2( 123, - addressIntId, + addressPersistentLocalId, new Provenance( Instant.FromDateTimeOffset(DateTimeOffset.Now).ToString(), Application.ParcelRegistry.ToString(), @@ -209,24 +224,26 @@ public async Task DetachAddressBecauseAddressWasRemoved() Organisation.Aiv.ToString(), "test")); - AddRelations(456, 456); + AddParcelAddressRelations(Fixture.Create(), [addressPersistentLocalId]); + AddParcelAddressRelations(Fixture.Create(), [addressPersistentLocalId]); Given(@event); - await Then(async _ => - { - _mockCommandHandler.Verify(x => x.Handle(It.IsAny(), CancellationToken.None), Times.Exactly(2)); - await Task.CompletedTask; - }); + await Then(async _ => + { + _mockCommandHandler.Verify(x => + x.Handle(It.IsAny(), CancellationToken.None), Times.Exactly(2)); + await Task.CompletedTask; + }); } [Fact] public async Task DetachAddressBecauseAddressWasRemovedBecauseHouseNumberWasRemoved() { - var addressIntId = 456; + var addressPersistentLocalId = 456; var @event = new AddressWasRemovedBecauseHouseNumberWasRemoved( 123, - addressIntId, + addressPersistentLocalId, new Provenance( Instant.FromDateTimeOffset(DateTimeOffset.Now).ToString(), Application.ParcelRegistry.ToString(), @@ -234,24 +251,26 @@ public async Task DetachAddressBecauseAddressWasRemovedBecauseHouseNumberWasRemo Organisation.Aiv.ToString(), "test")); - AddRelations(456, 456); + AddParcelAddressRelations(Fixture.Create(), [addressPersistentLocalId]); + AddParcelAddressRelations(Fixture.Create(), [addressPersistentLocalId]); Given(@event); - await Then(async _ => - { - _mockCommandHandler.Verify(x => x.Handle(It.IsAny(), CancellationToken.None), Times.Exactly(2)); - await Task.CompletedTask; - }); + await Then(async _ => + { + _mockCommandHandler.Verify(x => + x.Handle(It.IsAny(), CancellationToken.None), Times.Exactly(2)); + await Task.CompletedTask; + }); } [Fact] public async Task DetachAddressBecauseAddressWasRejected() { - var addressIntId = 456; + var addressPersistentLocalId = 456; var @event = new AddressWasRejected( 123, - addressIntId, + addressPersistentLocalId, new Provenance( Instant.FromDateTimeOffset(DateTimeOffset.Now).ToString(), Application.ParcelRegistry.ToString(), @@ -259,12 +278,14 @@ public async Task DetachAddressBecauseAddressWasRejected() Organisation.Aiv.ToString(), "test")); - AddRelations(456, 456); + AddParcelAddressRelations(Fixture.Create(), [addressPersistentLocalId]); + AddParcelAddressRelations(Fixture.Create(), [addressPersistentLocalId]); Given(@event); await Then(async _ => { - _mockCommandHandler.Verify(x => x.Handle(It.IsAny(), CancellationToken.None), Times.Exactly(2)); + _mockCommandHandler.Verify(x => + x.Handle(It.IsAny(), CancellationToken.None), Times.Exactly(2)); await Task.CompletedTask; }); } @@ -272,11 +293,11 @@ await Then(async _ => [Fact] public async Task DetachAddressBecauseHouseNumberWasRejected() { - var addressIntId = 456; + var addressPersistentLocalId = 456; var @event = new AddressWasRejectedBecauseHouseNumberWasRejected( 123, - addressIntId, + addressPersistentLocalId, new Provenance( Instant.FromDateTimeOffset(DateTimeOffset.Now).ToString(), Application.ParcelRegistry.ToString(), @@ -284,12 +305,14 @@ public async Task DetachAddressBecauseHouseNumberWasRejected() Organisation.Aiv.ToString(), "test")); - AddRelations(456, 456); + AddParcelAddressRelations(Fixture.Create(), [addressPersistentLocalId]); + AddParcelAddressRelations(Fixture.Create(), [addressPersistentLocalId]); Given(@event); await Then(async _ => { - _mockCommandHandler.Verify(x => x.Handle(It.IsAny(), CancellationToken.None), Times.Exactly(2)); + _mockCommandHandler.Verify(x => + x.Handle(It.IsAny(), CancellationToken.None), Times.Exactly(2)); await Task.CompletedTask; }); } @@ -297,11 +320,11 @@ await Then(async _ => [Fact] public async Task DetachAddressBecause_AddressWasRejectedBecauseHouseNumberWasRejected() { - var addressIntId = 456; + var addressPersistentLocalId = 456; var @event = new AddressWasRejectedBecauseHouseNumberWasRejected( 123, - addressIntId, + addressPersistentLocalId, new Provenance( Instant.FromDateTimeOffset(DateTimeOffset.Now).ToString(), Application.ParcelRegistry.ToString(), @@ -309,12 +332,14 @@ public async Task DetachAddressBecause_AddressWasRejectedBecauseHouseNumberWasRe Organisation.Aiv.ToString(), "test")); - AddRelations(456, 456); + AddParcelAddressRelations(Fixture.Create(), [addressPersistentLocalId]); + AddParcelAddressRelations(Fixture.Create(), [addressPersistentLocalId]); Given(@event); await Then(async _ => { - _mockCommandHandler.Verify(x => x.Handle(It.IsAny(), CancellationToken.None), Times.Exactly(2)); + _mockCommandHandler.Verify(x => + x.Handle(It.IsAny(), CancellationToken.None), Times.Exactly(2)); await Task.CompletedTask; }); } @@ -322,11 +347,11 @@ await Then(async _ => [Fact] public async Task DetachAddressBecause_AddressWasRejectedBecauseHouseNumberWasRetired() { - var addressIntId = 456; + var addressPersistentLocalId = 456; var @event = new AddressWasRejectedBecauseHouseNumberWasRetired( 123, - addressIntId, + addressPersistentLocalId, new Provenance( Instant.FromDateTimeOffset(DateTimeOffset.Now).ToString(), Application.ParcelRegistry.ToString(), @@ -334,12 +359,14 @@ public async Task DetachAddressBecause_AddressWasRejectedBecauseHouseNumberWasRe Organisation.Aiv.ToString(), "test")); - AddRelations(456, 456); + AddParcelAddressRelations(Fixture.Create(), [addressPersistentLocalId]); + AddParcelAddressRelations(Fixture.Create(), [addressPersistentLocalId]); Given(@event); await Then(async _ => { - _mockCommandHandler.Verify(x => x.Handle(It.IsAny(), CancellationToken.None), Times.Exactly(2)); + _mockCommandHandler.Verify(x => + x.Handle(It.IsAny(), CancellationToken.None), Times.Exactly(2)); await Task.CompletedTask; }); } @@ -347,11 +374,11 @@ await Then(async _ => [Fact] public async Task DetachAddressBecause_AddressWasRejectedBecauseStreetNameWasRetired() { - var addressIntId = 456; + var addressPersistentLocalId = 456; var @event = new AddressWasRejectedBecauseStreetNameWasRetired( 123, - addressIntId, + addressPersistentLocalId, new Provenance( Instant.FromDateTimeOffset(DateTimeOffset.Now).ToString(), Application.ParcelRegistry.ToString(), @@ -359,12 +386,14 @@ public async Task DetachAddressBecause_AddressWasRejectedBecauseStreetNameWasRet Organisation.Aiv.ToString(), "test")); - AddRelations(456, 456); + AddParcelAddressRelations(Fixture.Create(), [addressPersistentLocalId]); + AddParcelAddressRelations(Fixture.Create(), [addressPersistentLocalId]); Given(@event); await Then(async _ => { - _mockCommandHandler.Verify(x => x.Handle(It.IsAny(), CancellationToken.None), Times.Exactly(2)); + _mockCommandHandler.Verify(x => + x.Handle(It.IsAny(), CancellationToken.None), Times.Exactly(2)); await Task.CompletedTask; }); } @@ -372,11 +401,11 @@ await Then(async _ => [Fact] public async Task DetachAddressBecauseAddressWasRetiredV2() { - var addressIntId = 456; + var addressPersistentLocalId = 456; var @event = new AddressWasRetiredV2( 123, - addressIntId, + addressPersistentLocalId, new Provenance( Instant.FromDateTimeOffset(DateTimeOffset.Now).ToString(), Application.ParcelRegistry.ToString(), @@ -384,12 +413,14 @@ public async Task DetachAddressBecauseAddressWasRetiredV2() Organisation.Aiv.ToString(), "test")); - AddRelations(456, 456); + AddParcelAddressRelations(Fixture.Create(), [addressPersistentLocalId]); + AddParcelAddressRelations(Fixture.Create(), [addressPersistentLocalId]); Given(@event); await Then(async _ => { - _mockCommandHandler.Verify(x => x.Handle(It.IsAny(), CancellationToken.None), Times.Exactly(2)); + _mockCommandHandler.Verify(x => + x.Handle(It.IsAny(), CancellationToken.None), Times.Exactly(2)); await Task.CompletedTask; }); } @@ -397,11 +428,11 @@ await Then(async _ => [Fact] public async Task DetachAddressBecauseHouseNumberWasRetired() { - var addressIntId = 456; + var addressPersistentLocalId = 456; var @event = new AddressWasRetiredBecauseHouseNumberWasRetired( 123, - addressIntId, + addressPersistentLocalId, new Provenance( Instant.FromDateTimeOffset(DateTimeOffset.Now).ToString(), Application.ParcelRegistry.ToString(), @@ -409,12 +440,14 @@ public async Task DetachAddressBecauseHouseNumberWasRetired() Organisation.Aiv.ToString(), "test")); - AddRelations(456, 456); + AddParcelAddressRelations(Fixture.Create(), [addressPersistentLocalId]); + AddParcelAddressRelations(Fixture.Create(), [addressPersistentLocalId]); Given(@event); await Then(async _ => { - _mockCommandHandler.Verify(x => x.Handle(It.IsAny(), CancellationToken.None), Times.Exactly(2)); + _mockCommandHandler.Verify(x => + x.Handle(It.IsAny(), CancellationToken.None), Times.Exactly(2)); await Task.CompletedTask; }); } @@ -422,11 +455,11 @@ await Then(async _ => [Fact] public async Task DetachAddressBecauseStreetNameWasRejected() { - var addressIntId = 456; + var addressPersistentLocalId = 456; var @event = new AddressWasRetiredBecauseStreetNameWasRejected( 123, - addressIntId, + addressPersistentLocalId, new Provenance( Instant.FromDateTimeOffset(DateTimeOffset.Now).ToString(), Application.ParcelRegistry.ToString(), @@ -434,12 +467,14 @@ public async Task DetachAddressBecauseStreetNameWasRejected() Organisation.Aiv.ToString(), "test")); - AddRelations(456, 456); + AddParcelAddressRelations(Fixture.Create(), [addressPersistentLocalId]); + AddParcelAddressRelations(Fixture.Create(), [addressPersistentLocalId]); Given(@event); await Then(async _ => { - _mockCommandHandler.Verify(x => x.Handle(It.IsAny(), CancellationToken.None), Times.Exactly(2)); + _mockCommandHandler.Verify(x => + x.Handle(It.IsAny(), CancellationToken.None), Times.Exactly(2)); await Task.CompletedTask; }); } @@ -447,11 +482,11 @@ await Then(async _ => [Fact] public async Task DetachAddressBecauseStreetNameWasRetired() { - var addressIntId = 456; + var addressPersistentLocalId = 456; var @event = new AddressWasRetiredBecauseStreetNameWasRetired( 123, - addressIntId, + addressPersistentLocalId, new Provenance( Instant.FromDateTimeOffset(DateTimeOffset.Now).ToString(), Application.ParcelRegistry.ToString(), @@ -459,12 +494,14 @@ public async Task DetachAddressBecauseStreetNameWasRetired() Organisation.Aiv.ToString(), "test")); - AddRelations(456, 456); + AddParcelAddressRelations(Fixture.Create(), [addressPersistentLocalId]); + AddParcelAddressRelations(Fixture.Create(), [addressPersistentLocalId]); Given(@event); await Then(async _ => { - _mockCommandHandler.Verify(x => x.Handle(It.IsAny(), CancellationToken.None), Times.Exactly(2)); + _mockCommandHandler.Verify(x => + x.Handle(It.IsAny(), CancellationToken.None), Times.Exactly(2)); await Task.CompletedTask; }); } @@ -472,11 +509,11 @@ await Then(async _ => [Fact] public async Task DetachAddressFromBuildingUnitBecauseStreetNameWasRemoved() { - var addressIntId = 456; + var addressPersistentLocalId = 456; var @event = new AddressWasRemovedBecauseStreetNameWasRemoved( 123, - addressIntId, + addressPersistentLocalId, new Provenance( Instant.FromDateTimeOffset(DateTimeOffset.Now).ToString(), Application.ParcelRegistry.ToString(), @@ -484,86 +521,14 @@ public async Task DetachAddressFromBuildingUnitBecauseStreetNameWasRemoved() Organisation.Aiv.ToString(), "test")); - AddRelations(addressIntId, addressIntId); - - Given(@event); - await Then(async _ => - { - _mockCommandHandler.Verify(x => x.Handle(It.IsAny(), CancellationToken.None), Times.Exactly(2)); - await Task.CompletedTask; - }); - } - - [Fact] - public async Task StreetNameWasReaddressed() - { - var sourceAddressPersistentLocalId = 1; - var sourceBoxNumberAddressPersistentLocalId = 2; - var destinationAddressPersistentLocalId = 3; - var destinationBoxNumberAddressPersistentLocalId = 4; - - var @event = new AddressHouseNumberWasReaddressed( - 1000000, - sourceAddressPersistentLocalId, - new ReaddressedAddressData( - sourceAddressPersistentLocalId, - destinationAddressPersistentLocalId, - true, - "Current", - "120", - null, - "9000", - "AppointedByAdministrator", - "Entry", - "ExtendedWkbGeometry", - true), - new [] - { - new ReaddressedAddressData( - sourceBoxNumberAddressPersistentLocalId, - destinationBoxNumberAddressPersistentLocalId, - true, - "Current", - "120", - "A", - "9000", - "AppointedByAdministrator", - "Entry", - "ExtendedWkbGeometry", - true), - }, - new Provenance( - Instant.FromDateTimeOffset(DateTimeOffset.Now).ToString(), - Application.ParcelRegistry.ToString(), - Modification.Update.ToString(), - Organisation.Aiv.ToString(), - "test")); - - AddRelations(sourceAddressPersistentLocalId); - AddRelations(sourceBoxNumberAddressPersistentLocalId); + AddParcelAddressRelations(Fixture.Create(), [addressPersistentLocalId]); + AddParcelAddressRelations(Fixture.Create(), [addressPersistentLocalId]); Given(@event); await Then(async _ => { _mockCommandHandler.Verify(x => - x.Handle(It.IsAny(), CancellationToken.None), - Times.Exactly(2)); - - _mockCommandHandler.Verify(x => - x.Handle( - It.Is(y => - y.NewAddressPersistentLocalId == destinationAddressPersistentLocalId - && y.PreviousAddressPersistentLocalId == sourceAddressPersistentLocalId), - CancellationToken.None), - Times.Exactly(1)); - _mockCommandHandler.Verify(x => - x.Handle( - It.Is(y => - y.NewAddressPersistentLocalId == destinationBoxNumberAddressPersistentLocalId - && y.PreviousAddressPersistentLocalId == sourceBoxNumberAddressPersistentLocalId), - CancellationToken.None), - Times.Exactly(1)); - + x.Handle(It.IsAny(), CancellationToken.None), Times.Exactly(2)); await Task.CompletedTask; }); } @@ -571,11 +536,11 @@ await Then(async _ => [Fact] public async Task DetachAddressBecauseAddressWasRejectedBecauseOfReaddress() { - var addressIntId = 456; + var addressPersistentLocalId = 456; var @event = new AddressWasRejectedBecauseOfReaddress( 123, - addressIntId, + addressPersistentLocalId, new Provenance( Instant.FromDateTimeOffset(DateTimeOffset.Now).ToString(), Application.ParcelRegistry.ToString(), @@ -583,12 +548,14 @@ public async Task DetachAddressBecauseAddressWasRejectedBecauseOfReaddress() Organisation.Aiv.ToString(), "test")); - AddRelations(456, 456); + AddParcelAddressRelations(Fixture.Create(), [addressPersistentLocalId]); + AddParcelAddressRelations(Fixture.Create(), [addressPersistentLocalId]); Given(@event); await Then(async _ => { - _mockCommandHandler.Verify(x => x.Handle(It.IsAny(), CancellationToken.None), Times.Exactly(2)); + _mockCommandHandler.Verify(x => + x.Handle(It.IsAny(), CancellationToken.None), Times.Exactly(2)); await Task.CompletedTask; }); } @@ -596,11 +563,11 @@ await Then(async _ => [Fact] public async Task DetachAddressBecauseAddressWasRetiredBecauseOfReaddress() { - var addressIntId = 456; + var addressPersistentLocalId = 456; var @event = new AddressWasRetiredBecauseOfReaddress( 123, - addressIntId, + addressPersistentLocalId, new Provenance( Instant.FromDateTimeOffset(DateTimeOffset.Now).ToString(), Application.ParcelRegistry.ToString(), @@ -608,23 +575,24 @@ public async Task DetachAddressBecauseAddressWasRetiredBecauseOfReaddress() Organisation.Aiv.ToString(), "test")); - AddRelations(456, 456); + AddParcelAddressRelations(Fixture.Create(), [addressPersistentLocalId]); + AddParcelAddressRelations(Fixture.Create(), [addressPersistentLocalId]); Given(@event); await Then(async _ => { - _mockCommandHandler.Verify(x => x.Handle(It.IsAny(), CancellationToken.None), Times.Exactly(2)); + _mockCommandHandler.Verify(x => + x.Handle(It.IsAny(), CancellationToken.None), Times.Exactly(2)); await Task.CompletedTask; }); } - private void AddRelations(params int[] addressInts) + private void AddParcelAddressRelations(ParcelId parcelId, int[] addressPersistentLocalIds) { - foreach (var addressInt in addressInts) + foreach (var addressPersistentLocalId in addressPersistentLocalIds) { _fakeBackOfficeContext.ParcelAddressRelations.Add( - new ParcelAddressRelation(Fixture.Create(), - new AddressPersistentLocalId(addressInt))); + new ParcelAddressRelation(parcelId, new AddressPersistentLocalId(addressPersistentLocalId))); } _fakeBackOfficeContext.SaveChanges(); @@ -641,7 +609,7 @@ protected override CommandHandlingKafkaProjection CreateProjection() factoryMock .Setup(x => x.CreateDbContextAsync(CancellationToken.None)) .Returns(Task.FromResult(_fakeBackOfficeContext)); - return new CommandHandlingKafkaProjection(factoryMock.Object); + return new CommandHandlingKafkaProjection(factoryMock.Object, _parcels.Object); } } diff --git a/test/ParcelRegistry.Tests/ProjectionTests/Integration/ParcelLatestItemProjectionTests-Readdress.cs b/test/ParcelRegistry.Tests/ProjectionTests/Integration/ParcelLatestItemProjectionTests-Readdress.cs index 9f62a270..9225081d 100644 --- a/test/ParcelRegistry.Tests/ProjectionTests/Integration/ParcelLatestItemProjectionTests-Readdress.cs +++ b/test/ParcelRegistry.Tests/ProjectionTests/Integration/ParcelLatestItemProjectionTests-Readdress.cs @@ -2,6 +2,8 @@ { using System.Threading.Tasks; using AutoFixture; + using Builders; + using Fixtures; using FluentAssertions; using Parcel; using Parcel.Events; @@ -112,5 +114,67 @@ await Sut newRelation.Count.Should().Be(2); }); } + + [Fact] + public async Task GivenParcelAddressesWereReaddressed_ThenAddressesAreAttachedAndDetached() + { + _fixture.Customizations.Add(new WithUniqueInteger()); + + var firstParcelAddressWasAttachedV2 = _fixture.Create(); + var secondParcelAddressWasAttachedV2 = _fixture.Create(); + + var attachedAddressPersistentLocalIds = new[] + { + _fixture.Create(), + _fixture.Create() + }; + var detachedAddressPersistentLocalIds = new[] + { + firstParcelAddressWasAttachedV2.AddressPersistentLocalId, + secondParcelAddressWasAttachedV2.AddressPersistentLocalId + }; + + var eventBuilder = new ParcelAddressesWereReaddressedBuilder(_fixture); + + foreach (var addressPersistentLocalId in attachedAddressPersistentLocalIds) + { + eventBuilder.WithAttachedAddress(addressPersistentLocalId); + } + + foreach (var addressPersistentLocalId in detachedAddressPersistentLocalIds) + { + eventBuilder.WithDetachedAddress(addressPersistentLocalId); + } + + var @event = eventBuilder.Build(); + + await Sut + .Given( + _fixture.Create(), + firstParcelAddressWasAttachedV2, + secondParcelAddressWasAttachedV2, + @event) + .Then(async context => + { + foreach (var addressPersistentLocalId in attachedAddressPersistentLocalIds) + { + var parcelAddressRelation = await context.ParcelLatestItemAddresses.FindAsync( + @event.ParcelId, + addressPersistentLocalId); + + parcelAddressRelation.Should().NotBeNull(); + parcelAddressRelation!.Count.Should().Be(1); + } + + foreach (var addressPersistentLocalId in detachedAddressPersistentLocalIds) + { + var parcelAddressRelation = await context.ParcelLatestItemAddresses.FindAsync( + @event.ParcelId, + addressPersistentLocalId); + + parcelAddressRelation.Should().BeNull(); + } + }); + } } } diff --git a/test/ParcelRegistry.Tests/ProjectionTests/Integration/ParcelVersionProjectionTests-Readdress.cs b/test/ParcelRegistry.Tests/ProjectionTests/Integration/ParcelVersionProjectionTests-Readdress.cs index efe2b886..54fff358 100644 --- a/test/ParcelRegistry.Tests/ProjectionTests/Integration/ParcelVersionProjectionTests-Readdress.cs +++ b/test/ParcelRegistry.Tests/ProjectionTests/Integration/ParcelVersionProjectionTests-Readdress.cs @@ -6,6 +6,7 @@ using Be.Vlaanderen.Basisregisters.GrAr.Common.Pipes; using Be.Vlaanderen.Basisregisters.ProjectionHandling.SqlStreamStore; using Builders; + using Fixtures; using FluentAssertions; using Parcel.Events; using Xunit; @@ -205,5 +206,98 @@ await Sut parcelVersion!.Type.Should().Be("EventName"); }); } + + [Fact] + public async Task GivenParcelAddressesWereReaddressed_ThenAddressesAreAttachedAndDetached() + { + _fixture.Customizations.Add(new WithUniqueInteger()); + + var parcelWasImported = _fixture.Create(); + var firstParcelAddressWasAttachedV2 = _fixture.Create(); + var secondParcelAddressWasAttachedV2 = _fixture.Create(); + + var attachedAddressPersistentLocalIds = new[] + { + _fixture.Create(), + _fixture.Create() + }; + var detachedAddressPersistentLocalIds = new[] + { + firstParcelAddressWasAttachedV2.AddressPersistentLocalId, + secondParcelAddressWasAttachedV2.AddressPersistentLocalId + }; + + var eventBuilder = new ParcelAddressesWereReaddressedBuilder(_fixture); + + foreach (var addressPersistentLocalId in attachedAddressPersistentLocalIds) + { + eventBuilder.WithAttachedAddress(addressPersistentLocalId); + } + + foreach (var addressPersistentLocalId in detachedAddressPersistentLocalIds) + { + eventBuilder.WithDetachedAddress(addressPersistentLocalId); + } + + var @event = eventBuilder.Build(); + + var position = _fixture.Create(); + var parcelWasImportedMetadata = new Dictionary + { + { AddEventHashPipe.HashMetadataKey, _fixture.Create() }, + { Envelope.PositionMetadataKey, position }, + { Envelope.EventNameMetadataKey, _fixture.Create()} + }; + var firstParcelAddressWasAttachedV2Metadata = new Dictionary + { + { AddEventHashPipe.HashMetadataKey, _fixture.Create() }, + { Envelope.PositionMetadataKey, ++position }, + { Envelope.EventNameMetadataKey, _fixture.Create()} + }; + var secondParcelAddressWasAttachedV2Metadata = new Dictionary + { + { AddEventHashPipe.HashMetadataKey, _fixture.Create() }, + { Envelope.PositionMetadataKey, ++position }, + { Envelope.EventNameMetadataKey, _fixture.Create()} + }; + var eventMetadata = new Dictionary + { + { AddEventHashPipe.HashMetadataKey, _fixture.Create() }, + { Envelope.PositionMetadataKey, ++position }, + { Envelope.EventNameMetadataKey, "EventName"} + }; + + await Sut + .Given( + new Envelope(new Envelope(parcelWasImported, parcelWasImportedMetadata)), + new Envelope( + new Envelope(firstParcelAddressWasAttachedV2, firstParcelAddressWasAttachedV2Metadata)), + new Envelope( + new Envelope(secondParcelAddressWasAttachedV2, secondParcelAddressWasAttachedV2Metadata)), + new Envelope(new Envelope(@event, eventMetadata))) + .Then(async context => + { + foreach (var addressPersistentLocalId in attachedAddressPersistentLocalIds) + { + var parcelAddressRelation = await context.ParcelVersionAddresses.FindAsync( + position, + @event.ParcelId, + addressPersistentLocalId); + + parcelAddressRelation.Should().NotBeNull(); + parcelAddressRelation!.Count.Should().Be(1); + } + + foreach (var addressPersistentLocalId in detachedAddressPersistentLocalIds) + { + var parcelAddressRelation = await context.ParcelVersionAddresses.FindAsync( + position, + @event.ParcelId, + addressPersistentLocalId); + + parcelAddressRelation.Should().BeNull(); + } + }); + } } } diff --git a/test/ParcelRegistry.Tests/ProjectionTests/Legacy/ParcelDetailItemV2Tests-Readdress.cs b/test/ParcelRegistry.Tests/ProjectionTests/Legacy/ParcelDetailItemV2Tests-Readdress.cs index a4b6fbc5..b302c703 100644 --- a/test/ParcelRegistry.Tests/ProjectionTests/Legacy/ParcelDetailItemV2Tests-Readdress.cs +++ b/test/ParcelRegistry.Tests/ProjectionTests/Legacy/ParcelDetailItemV2Tests-Readdress.cs @@ -7,6 +7,7 @@ namespace ParcelRegistry.Tests.ProjectionTests.Legacy using Be.Vlaanderen.Basisregisters.GrAr.Common.Pipes; using Be.Vlaanderen.Basisregisters.ProjectionHandling.SqlStreamStore; using Builders; + using Fixtures; using FluentAssertions; using Parcel.Events; using Xunit; @@ -172,5 +173,88 @@ await Sut parcelDetailV2.LastEventHash.Should().Be(@event.GetHash()); }); } + + [Fact] + public async Task GivenParcelAddressesWereReaddressed_ThenAddressesAreAttachedAndDetached() + { + _fixture.Customizations.Add(new WithUniqueInteger()); + + var parcelWasImported = _fixture.Create(); + var firstParcelAddressWasAttachedV2 = _fixture.Create(); + var secondParcelAddressWasAttachedV2 = _fixture.Create(); + + var attachedAddressPersistentLocalIds = new[] + { + _fixture.Create(), + _fixture.Create() + }; + var detachedAddressPersistentLocalIds = new[] + { + firstParcelAddressWasAttachedV2.AddressPersistentLocalId, + secondParcelAddressWasAttachedV2.AddressPersistentLocalId + }; + + var eventBuilder = new ParcelAddressesWereReaddressedBuilder(_fixture); + + foreach (var addressPersistentLocalId in attachedAddressPersistentLocalIds) + { + eventBuilder.WithAttachedAddress(addressPersistentLocalId); + } + + foreach (var addressPersistentLocalId in detachedAddressPersistentLocalIds) + { + eventBuilder.WithDetachedAddress(addressPersistentLocalId); + } + + var @event = eventBuilder.Build(); + + var parcelWasImportedMetadata = new Dictionary + { + { AddEventHashPipe.HashMetadataKey, parcelWasImported.GetHash() } + }; + var firstParcelAddressWasAttachedMetadata = new Dictionary + { + { AddEventHashPipe.HashMetadataKey, firstParcelAddressWasAttachedV2.GetHash() } + }; + var secondParcelAddressWasAttachedMetadata = new Dictionary + { + { AddEventHashPipe.HashMetadataKey, secondParcelAddressWasAttachedV2.GetHash() } + }; + var eventMetadata = new Dictionary + { + { AddEventHashPipe.HashMetadataKey, @event.GetHash() } + }; + + await Sut + .Given( + new Envelope(new Envelope(parcelWasImported, parcelWasImportedMetadata)), + new Envelope( + new Envelope(firstParcelAddressWasAttachedV2, firstParcelAddressWasAttachedMetadata)), + new Envelope( + new Envelope(secondParcelAddressWasAttachedV2, secondParcelAddressWasAttachedMetadata)), + new Envelope(new Envelope(@event, eventMetadata))) + .Then(async context => + { + var parcelDetailV2 = await context.ParcelDetailWithCountV2.FindAsync(parcelWasImported.ParcelId); + parcelDetailV2.Should().NotBeNull(); + + foreach (var addressPersistentLocalId in attachedAddressPersistentLocalIds) + { + var parcelAddressRelation = parcelDetailV2!.Addresses + .SingleOrDefault(x => x.AddressPersistentLocalId == addressPersistentLocalId); + + parcelAddressRelation.Should().NotBeNull(); + parcelAddressRelation!.Count.Should().Be(1); + } + + foreach (var addressPersistentLocalId in detachedAddressPersistentLocalIds) + { + var parcelAddressRelation = parcelDetailV2!.Addresses + .SingleOrDefault(x => x.AddressPersistentLocalId == addressPersistentLocalId); + + parcelAddressRelation.Should().BeNull(); + } + }); + } } } diff --git a/test/ParcelRegistry.Tests/ProjectionTests/Legacy/ParcelSyndicationV2Tests.cs b/test/ParcelRegistry.Tests/ProjectionTests/Legacy/ParcelSyndicationV2Tests.cs index c8994b9a..3abf48ea 100644 --- a/test/ParcelRegistry.Tests/ProjectionTests/Legacy/ParcelSyndicationV2Tests.cs +++ b/test/ParcelRegistry.Tests/ProjectionTests/Legacy/ParcelSyndicationV2Tests.cs @@ -11,6 +11,7 @@ namespace ParcelRegistry.Tests.ProjectionTests.Legacy using Be.Vlaanderen.Basisregisters.GrAr.Provenance; using Be.Vlaanderen.Basisregisters.ProjectionHandling.SqlStreamStore; using Be.Vlaanderen.Basisregisters.Utilities.HexByteConvertor; + using Builders; using EventExtensions; using Fixtures; using FluentAssertions; @@ -23,7 +24,7 @@ namespace ParcelRegistry.Tests.ProjectionTests.Legacy public class ParcelSyndicationV2Tests : ParcelLegacyProjectionTest { - private readonly Fixture? _fixture; + private readonly Fixture _fixture; public ParcelSyndicationV2Tests() { @@ -297,6 +298,91 @@ await Sut }); } + [Fact] + public async Task GivenParcelAddressesWereReaddressed_ThenAddressesAreAttachedAndDetached() + { + _fixture.Customizations.Add(new WithUniqueInteger()); + + var parcelWasImported = _fixture.Create(); + var firstParcelAddressWasAttachedV2 = _fixture.Create(); + var secondParcelAddressWasAttachedV2 = _fixture.Create(); + + var attachedAddressPersistentLocalIds = new[] + { + _fixture.Create(), + _fixture.Create() + }; + var detachedAddressPersistentLocalIds = new[] + { + firstParcelAddressWasAttachedV2.AddressPersistentLocalId, + secondParcelAddressWasAttachedV2.AddressPersistentLocalId + }; + + var eventBuilder = new ParcelAddressesWereReaddressedBuilder(_fixture); + + foreach (var addressPersistentLocalId in attachedAddressPersistentLocalIds) + { + eventBuilder.WithAttachedAddress(addressPersistentLocalId); + } + + foreach (var addressPersistentLocalId in detachedAddressPersistentLocalIds) + { + eventBuilder.WithDetachedAddress(addressPersistentLocalId); + } + + var @event = eventBuilder.Build(); + + var position = _fixture.Create(); + var parcelWasImportedMetadata = new Dictionary + { + { AddEventHashPipe.HashMetadataKey, parcelWasImported.GetHash() }, + { Envelope.PositionMetadataKey, position }, + { Envelope.EventNameMetadataKey, nameof(ParcelWasImported) }, + }; + var firstParcelAddressWasAttachedMetadata = new Dictionary + { + { AddEventHashPipe.HashMetadataKey, parcelWasImported.GetHash() }, + { Envelope.PositionMetadataKey, ++position }, + { Envelope.EventNameMetadataKey, nameof(ParcelAddressWasAttachedV2) }, + }; + var secondParcelAddressWasAttachedMetadata = new Dictionary + { + { AddEventHashPipe.HashMetadataKey, parcelWasImported.GetHash() }, + { Envelope.PositionMetadataKey, ++position }, + { Envelope.EventNameMetadataKey, nameof(ParcelAddressWasAttachedV2) }, + }; + var eventMetadata = new Dictionary + { + { AddEventHashPipe.HashMetadataKey, parcelWasImported.GetHash() }, + { Envelope.PositionMetadataKey, ++position }, + { Envelope.EventNameMetadataKey, nameof(ParcelAddressesWereReaddressed) }, + }; + + await Sut + .Given( + new Envelope(new Envelope(parcelWasImported, parcelWasImportedMetadata)), + new Envelope( + new Envelope(firstParcelAddressWasAttachedV2, firstParcelAddressWasAttachedMetadata)), + new Envelope( + new Envelope(secondParcelAddressWasAttachedV2, secondParcelAddressWasAttachedMetadata)), + new Envelope(new Envelope(@event, eventMetadata))) + .Then(async context => + { + var parcelSyndicationItem = await context.ParcelSyndication.FindAsync(position); + parcelSyndicationItem.Should().NotBeNull(); + + foreach (var addressPersistentLocalId in attachedAddressPersistentLocalIds) + { + parcelSyndicationItem!.AddressPersistentLocalIds.Should().Contain(addressPersistentLocalId); + } + + foreach (var addressPersistentLocalId in detachedAddressPersistentLocalIds) + { + parcelSyndicationItem!.AddressPersistentLocalIds.Should().NotContain(addressPersistentLocalId); + } + }); + } + protected override ParcelSyndicationProjections CreateProjection() => new ParcelSyndicationProjections(); }