From 1d0fd48cb5c0b5d2aa2832c8356ca98efeb56cbc Mon Sep 17 00:00:00 2001 From: jvandaal Date: Fri, 24 May 2024 16:04:45 +0200 Subject: [PATCH] feat: create oslo snapshots --- .../Requests/CreateOsloSnapshotsRequest.cs | 11 ++ .../CreateOsloSnapshotsSqsRequest.cs | 10 ++ .../Handlers/CreateOsloSnapshotsHandler.cs | 52 ++++++++ .../MessageHandler.cs | 6 + .../CreateOsloSnapshotsLambdaRequest.cs | 37 ++++++ .../Handlers/AttachAddressHandler.cs | 2 +- .../Handlers/CreateOsloSnapshotsHandler.cs | 35 ++++++ .../Handlers/DetachAddressHandler.cs | 2 +- .../ParcelController-CreateOsloSnapshots.cs | 41 +++++++ .../Modules/CommandHandlingModule.cs | 8 +- .../Modules/RepositoriesModule.cs | 5 + .../Repositories/AllStreamRepository.cs | 19 +++ .../Infrastructure/Modules/ApiModule.cs | 15 ++- .../ProducerProjections.cs | 21 +++- src/ParcelRegistry/AllStream/AllStream.cs | 17 +++ .../AllStreamCommandHandlerModule.cs | 41 +++++++ src/ParcelRegistry/AllStream/AllStreamId.cs | 22 ++++ .../AllStream/CommandHandlerModules.cs | 22 ++++ .../AllStream/Commands/CreateOsloSnapshots.cs | 42 +++++++ .../ParcelOsloSnapshotsWereRequested.cs | 54 +++++++++ .../AllStream/IAllStreamRepository.cs | 9 ++ .../{Parcel => }/ProvenanceFactory.cs | 2 +- .../GivenAllStreamDoesNotExist.cs | 35 ++++++ .../GivenAllStreamExists.cs | 36 ++++++ .../GivenRequest.cs | 70 +++++++++++ ...venCreateOsloSnapshotsBackOfficeRequest.cs | 76 ++++++++++++ .../BackOffice/Lambda/MessageHandlerTests.cs | 32 +++++ .../WhenCreatingOsloSnapshotsRequest.cs | 113 ++++++++++++++++++ 28 files changed, 821 insertions(+), 14 deletions(-) create mode 100644 src/ParcelRegistry.Api.BackOffice.Abstractions/Requests/CreateOsloSnapshotsRequest.cs create mode 100644 src/ParcelRegistry.Api.BackOffice.Abstractions/SqsRequests/CreateOsloSnapshotsSqsRequest.cs create mode 100644 src/ParcelRegistry.Api.BackOffice.Handlers.Lambda/Handlers/CreateOsloSnapshotsHandler.cs create mode 100644 src/ParcelRegistry.Api.BackOffice.Handlers.Lambda/Requests/CreateOsloSnapshotsLambdaRequest.cs create mode 100644 src/ParcelRegistry.Api.BackOffice/Handlers/CreateOsloSnapshotsHandler.cs create mode 100644 src/ParcelRegistry.Api.BackOffice/ParcelController-CreateOsloSnapshots.cs create mode 100644 src/ParcelRegistry.Infrastructure/Repositories/AllStreamRepository.cs create mode 100644 src/ParcelRegistry/AllStream/AllStream.cs create mode 100644 src/ParcelRegistry/AllStream/AllStreamCommandHandlerModule.cs create mode 100644 src/ParcelRegistry/AllStream/AllStreamId.cs create mode 100644 src/ParcelRegistry/AllStream/CommandHandlerModules.cs create mode 100644 src/ParcelRegistry/AllStream/Commands/CreateOsloSnapshots.cs create mode 100644 src/ParcelRegistry/AllStream/Events/ParcelOsloSnapshotsWereRequested.cs create mode 100644 src/ParcelRegistry/AllStream/IAllStreamRepository.cs rename src/ParcelRegistry/{Parcel => }/ProvenanceFactory.cs (97%) create mode 100644 test/ParcelRegistry.Tests/AggregateTests/WhenRequestingCreateOsloSnapshots/GivenAllStreamDoesNotExist.cs create mode 100644 test/ParcelRegistry.Tests/AggregateTests/WhenRequestingCreateOsloSnapshots/GivenAllStreamExists.cs create mode 100644 test/ParcelRegistry.Tests/BackOffice/Api/WhenRequestingCreateOsloSnapshots/GivenRequest.cs create mode 100644 test/ParcelRegistry.Tests/BackOffice/Handler/GivenCreateOsloSnapshotsBackOfficeRequest.cs create mode 100644 test/ParcelRegistry.Tests/BackOffice/Lambda/WhenCreatingOsloSnapshotsRequest.cs diff --git a/src/ParcelRegistry.Api.BackOffice.Abstractions/Requests/CreateOsloSnapshotsRequest.cs b/src/ParcelRegistry.Api.BackOffice.Abstractions/Requests/CreateOsloSnapshotsRequest.cs new file mode 100644 index 00000000..a8a5ed27 --- /dev/null +++ b/src/ParcelRegistry.Api.BackOffice.Abstractions/Requests/CreateOsloSnapshotsRequest.cs @@ -0,0 +1,11 @@ +namespace ParcelRegistry.Api.BackOffice.Abstractions.Requests +{ + using System.Collections.Generic; + + public class CreateOsloSnapshotsRequest + { + public List CaPaKeys { get; set; } + + public string Reden { get; set; } + } +} diff --git a/src/ParcelRegistry.Api.BackOffice.Abstractions/SqsRequests/CreateOsloSnapshotsSqsRequest.cs b/src/ParcelRegistry.Api.BackOffice.Abstractions/SqsRequests/CreateOsloSnapshotsSqsRequest.cs new file mode 100644 index 00000000..95b16acb --- /dev/null +++ b/src/ParcelRegistry.Api.BackOffice.Abstractions/SqsRequests/CreateOsloSnapshotsSqsRequest.cs @@ -0,0 +1,10 @@ +namespace ParcelRegistry.Api.BackOffice.Abstractions.SqsRequests +{ + using Be.Vlaanderen.Basisregisters.Sqs.Requests; + using Requests; + + public class CreateOsloSnapshotsSqsRequest : SqsRequest + { + public CreateOsloSnapshotsRequest Request { get; set; } + } +} diff --git a/src/ParcelRegistry.Api.BackOffice.Handlers.Lambda/Handlers/CreateOsloSnapshotsHandler.cs b/src/ParcelRegistry.Api.BackOffice.Handlers.Lambda/Handlers/CreateOsloSnapshotsHandler.cs new file mode 100644 index 00000000..df1fb55a --- /dev/null +++ b/src/ParcelRegistry.Api.BackOffice.Handlers.Lambda/Handlers/CreateOsloSnapshotsHandler.cs @@ -0,0 +1,52 @@ +namespace ParcelRegistry.Api.BackOffice.Handlers.Lambda.Handlers +{ + using Be.Vlaanderen.Basisregisters.AggregateSource; + using Be.Vlaanderen.Basisregisters.CommandHandling.Idempotency; + using Be.Vlaanderen.Basisregisters.Sqs.Lambda.Handlers; + using Be.Vlaanderen.Basisregisters.Sqs.Lambda.Infrastructure; + using Requests; + using TicketingService.Abstractions; + + public sealed class CreateOsloSnapshotsLambdaHandler : SqsLambdaHandlerBase + { + public CreateOsloSnapshotsLambdaHandler( + ICustomRetryPolicy retryPolicy, + ITicketing ticketing, + IIdempotentCommandHandler idempotentCommandHandler) + : base(retryPolicy, ticketing, idempotentCommandHandler) + { + } + + protected override async Task InnerHandle(CreateOsloSnapshotsLambdaRequest request, CancellationToken cancellationToken) + { + var cmd = request.ToCommand(); + + try + { + await IdempotentCommandHandler.Dispatch( + cmd.CreateCommandId(), + cmd, + request.Metadata!, + cancellationToken); + } + catch (IdempotencyException) + { + // Idempotent: Do Nothing return last etag + } + + return "done"; + } + + protected override TicketError? MapDomainException(DomainException exception, CreateOsloSnapshotsLambdaRequest request) => null; + + protected override Task HandleAggregateIdIsNotFoundException(CreateOsloSnapshotsLambdaRequest request, CancellationToken cancellationToken) + { + throw new NotImplementedException(); + } + + protected override Task ValidateIfMatchHeaderValue(CreateOsloSnapshotsLambdaRequest request, CancellationToken cancellationToken) + { + return Task.CompletedTask; + } + } +} diff --git a/src/ParcelRegistry.Api.BackOffice.Handlers.Lambda/MessageHandler.cs b/src/ParcelRegistry.Api.BackOffice.Handlers.Lambda/MessageHandler.cs index f14f355f..93bb93ac 100644 --- a/src/ParcelRegistry.Api.BackOffice.Handlers.Lambda/MessageHandler.cs +++ b/src/ParcelRegistry.Api.BackOffice.Handlers.Lambda/MessageHandler.cs @@ -49,6 +49,12 @@ await mediator.Send( cancellationToken); break; + case CreateOsloSnapshotsSqsRequest request: + await mediator.Send( + new CreateOsloSnapshotsLambdaRequest(messageMetadata.MessageGroupId!, request), + cancellationToken); + break; + default: throw new NotImplementedException( $"{sqsRequest.GetType().Name} has no corresponding SqsLambdaRequest defined."); diff --git a/src/ParcelRegistry.Api.BackOffice.Handlers.Lambda/Requests/CreateOsloSnapshotsLambdaRequest.cs b/src/ParcelRegistry.Api.BackOffice.Handlers.Lambda/Requests/CreateOsloSnapshotsLambdaRequest.cs new file mode 100644 index 00000000..1b5e4042 --- /dev/null +++ b/src/ParcelRegistry.Api.BackOffice.Handlers.Lambda/Requests/CreateOsloSnapshotsLambdaRequest.cs @@ -0,0 +1,37 @@ +namespace ParcelRegistry.Api.BackOffice.Handlers.Lambda.Requests +{ + using Abstractions.Requests; + using Abstractions.SqsRequests; + using AllStream.Commands; + using Be.Vlaanderen.Basisregisters.Sqs.Lambda.Requests; + + public sealed record CreateOsloSnapshotsLambdaRequest : SqsLambdaRequest + { + public CreateOsloSnapshotsRequest Request { get; } + + + public CreateOsloSnapshotsLambdaRequest( + string messageGroupId, + CreateOsloSnapshotsSqsRequest sqsRequest) + : base( + messageGroupId, + sqsRequest.TicketId, + null, + sqsRequest.ProvenanceData.ToProvenance(), + sqsRequest.Metadata) + { + Request = sqsRequest.Request; + } + + /// + /// Map to CreateOsloSnapshots command + /// + /// CreateOsloSnapshots + public CreateOsloSnapshots ToCommand() + { + return new CreateOsloSnapshots( + Request.CaPaKeys.Select(x => new VbrCaPaKey(x)), + Provenance); + } + } +} diff --git a/src/ParcelRegistry.Api.BackOffice/Handlers/AttachAddressHandler.cs b/src/ParcelRegistry.Api.BackOffice/Handlers/AttachAddressHandler.cs index 2ecc62e6..6f523913 100644 --- a/src/ParcelRegistry.Api.BackOffice/Handlers/AttachAddressHandler.cs +++ b/src/ParcelRegistry.Api.BackOffice/Handlers/AttachAddressHandler.cs @@ -7,7 +7,7 @@ namespace ParcelRegistry.Api.BackOffice.Handlers using Parcel; using TicketingService.Abstractions; - public class AttachAddressHandler : SqsHandler + public sealed class AttachAddressHandler : SqsHandler { public const string Action = "AttachAddressParcel"; diff --git a/src/ParcelRegistry.Api.BackOffice/Handlers/CreateOsloSnapshotsHandler.cs b/src/ParcelRegistry.Api.BackOffice/Handlers/CreateOsloSnapshotsHandler.cs new file mode 100644 index 00000000..55df0419 --- /dev/null +++ b/src/ParcelRegistry.Api.BackOffice/Handlers/CreateOsloSnapshotsHandler.cs @@ -0,0 +1,35 @@ +namespace ParcelRegistry.Api.BackOffice.Handlers +{ + using System.Collections.Generic; + using Abstractions.SqsRequests; + using AllStream; + using Be.Vlaanderen.Basisregisters.Sqs; + using Be.Vlaanderen.Basisregisters.Sqs.Handlers; + using TicketingService.Abstractions; + + public sealed class CreateOsloSnapshotsHandler : SqsHandler + { + public const string Action = "CreateOsloSnapshots"; + + public CreateOsloSnapshotsHandler( + ISqsQueue sqsQueue, + ITicketing ticketing, + ITicketingUrl ticketingUrl) : base(sqsQueue, ticketing, ticketingUrl) + { } + + protected override string? WithAggregateId(CreateOsloSnapshotsSqsRequest request) + { + return AllStreamId.Instance; + } + + protected override IDictionary WithTicketMetadata(string aggregateId, CreateOsloSnapshotsSqsRequest sqsRequest) + { + return new Dictionary + { + { RegistryKey, nameof(ParcelRegistry) }, + { ActionKey, Action }, + { AggregateIdKey, aggregateId } + }; + } + } +} diff --git a/src/ParcelRegistry.Api.BackOffice/Handlers/DetachAddressHandler.cs b/src/ParcelRegistry.Api.BackOffice/Handlers/DetachAddressHandler.cs index e5a6014f..1d491219 100644 --- a/src/ParcelRegistry.Api.BackOffice/Handlers/DetachAddressHandler.cs +++ b/src/ParcelRegistry.Api.BackOffice/Handlers/DetachAddressHandler.cs @@ -7,7 +7,7 @@ namespace ParcelRegistry.Api.BackOffice.Handlers using Parcel; using TicketingService.Abstractions; - public class DetachAddressHandler : SqsHandler + public sealed class DetachAddressHandler : SqsHandler { public const string Action = "DetachAddressParcel"; diff --git a/src/ParcelRegistry.Api.BackOffice/ParcelController-CreateOsloSnapshots.cs b/src/ParcelRegistry.Api.BackOffice/ParcelController-CreateOsloSnapshots.cs new file mode 100644 index 00000000..3fa3b13c --- /dev/null +++ b/src/ParcelRegistry.Api.BackOffice/ParcelController-CreateOsloSnapshots.cs @@ -0,0 +1,41 @@ +namespace ParcelRegistry.Api.BackOffice +{ + using System.Threading; + using System.Threading.Tasks; + using Abstractions.Requests; + using Abstractions.SqsRequests; + using Be.Vlaanderen.Basisregisters.Auth.AcmIdm; + using Be.Vlaanderen.Basisregisters.GrAr.Provenance; + using Microsoft.AspNetCore.Authentication.JwtBearer; + using Microsoft.AspNetCore.Authorization; + using Microsoft.AspNetCore.Mvc; + + public partial class ParcelController + { + /// + /// Creëer nieuwe OSLO snapshots. + /// + /// + /// + /// + [HttpPost("acties/oslosnapshots")] + [Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme, Policy = PolicyNames.Adres.InterneBijwerker)] + public async Task CreateOsloSnapshots( + [FromBody] CreateOsloSnapshotsRequest request, + CancellationToken cancellationToken = default) + { + var provenance = _provenanceFactory.Create(new Reason(request.Reden), Modification.Unknown); + + var sqsRequest = new CreateOsloSnapshotsSqsRequest + { + Request = request, + Metadata = GetMetadata(), + ProvenanceData = new ProvenanceData(provenance) + }; + + var sqsResult = await _mediator.Send(sqsRequest, cancellationToken); + + return Accepted(sqsResult); + } + } +} diff --git a/src/ParcelRegistry.Infrastructure/Modules/CommandHandlingModule.cs b/src/ParcelRegistry.Infrastructure/Modules/CommandHandlingModule.cs index dd3321ce..2b50a634 100644 --- a/src/ParcelRegistry.Infrastructure/Modules/CommandHandlingModule.cs +++ b/src/ParcelRegistry.Infrastructure/Modules/CommandHandlingModule.cs @@ -1,10 +1,9 @@ namespace ParcelRegistry.Infrastructure.Modules { - using Be.Vlaanderen.Basisregisters.AggregateSource; - using Be.Vlaanderen.Basisregisters.CommandHandling; using Autofac; + using Be.Vlaanderen.Basisregisters.CommandHandling; + using Legacy; using Microsoft.Extensions.Configuration; - using Parcel; public class CommandHandlingModule : Module { @@ -17,8 +16,9 @@ protected override void Load(ContainerBuilder builder) { builder.RegisterModule(new AggregateSourceModule(_configuration)); - Legacy.CommandHandlerModules.Register(builder); CommandHandlerModules.Register(builder); + ParcelRegistry.Parcel.CommandHandlerModules.Register(builder); + AllStream.CommandHandlerModules.Register(builder); builder .RegisterType() diff --git a/src/ParcelRegistry.Infrastructure/Modules/RepositoriesModule.cs b/src/ParcelRegistry.Infrastructure/Modules/RepositoriesModule.cs index aa351741..9a92c76a 100644 --- a/src/ParcelRegistry.Infrastructure/Modules/RepositoriesModule.cs +++ b/src/ParcelRegistry.Infrastructure/Modules/RepositoriesModule.cs @@ -1,5 +1,6 @@ namespace ParcelRegistry.Infrastructure.Modules { + using AllStream; using Autofac; using Parcel; using Repositories; @@ -16,6 +17,10 @@ protected override void Load(ContainerBuilder builder) builder .RegisterType() .As(); + + builder + .RegisterType() + .As(); } } } diff --git a/src/ParcelRegistry.Infrastructure/Repositories/AllStreamRepository.cs b/src/ParcelRegistry.Infrastructure/Repositories/AllStreamRepository.cs new file mode 100644 index 00000000..d922373b --- /dev/null +++ b/src/ParcelRegistry.Infrastructure/Repositories/AllStreamRepository.cs @@ -0,0 +1,19 @@ +namespace ParcelRegistry.Infrastructure.Repositories +{ + using AllStream; + using Be.Vlaanderen.Basisregisters.AggregateSource; + using Be.Vlaanderen.Basisregisters.AggregateSource.SqlStreamStore; + using Be.Vlaanderen.Basisregisters.EventHandling; + using Parcel; + using SqlStreamStore; + + public class AllStreamRepository : Repository, IAllStreamRepository + { + public AllStreamRepository( + ConcurrentUnitOfWork unitOfWork, + IStreamStore eventStore, + EventMapping eventMapping, + EventDeserializer eventDeserializer) + : base(() => new AllStream(), unitOfWork, eventStore, eventMapping, eventDeserializer) { } + } +} diff --git a/src/ParcelRegistry.Producer.Snapshot.Oslo/Infrastructure/Modules/ApiModule.cs b/src/ParcelRegistry.Producer.Snapshot.Oslo/Infrastructure/Modules/ApiModule.cs index faeb730e..23540603 100644 --- a/src/ParcelRegistry.Producer.Snapshot.Oslo/Infrastructure/Modules/ApiModule.cs +++ b/src/ParcelRegistry.Producer.Snapshot.Oslo/Infrastructure/Modules/ApiModule.cs @@ -83,7 +83,7 @@ private void RegisterProjections(ContainerBuilder builder) _loggerFactory) .RegisterProjections(c => { - var bootstrapServers = _configuration["Kafka:BootstrapServers"]; + var bootstrapServers = _configuration["Kafka:BootstrapServers"]!; var topic = $"{_configuration[ProducerProjections.TopicKey]}" ?? throw new ArgumentException($"Configuration has no value for {ProducerProjections.TopicKey}"); var producerOptions = new ProducerOptions( new BootstrapServers(bootstrapServers), @@ -95,18 +95,21 @@ private void RegisterProjections(ContainerBuilder builder) && !string.IsNullOrEmpty(_configuration["Kafka:SaslPassword"])) { producerOptions.ConfigureSaslAuthentication(new SaslAuthentication( - _configuration["Kafka:SaslUserName"], - _configuration["Kafka:SaslPassword"])); + _configuration["Kafka:SaslUserName"]!, + _configuration["Kafka:SaslPassword"]!)); } + var osloProxy = c.Resolve(); + return new ProducerProjections( new Producer(producerOptions), new SnapshotManager( c.Resolve(), - c.Resolve(), + osloProxy, SnapshotManagerOptions.Create( - _configuration["RetryPolicy:MaxRetryWaitIntervalSeconds"], - _configuration["RetryPolicy:RetryBackoffFactor"]))); + _configuration["RetryPolicy:MaxRetryWaitIntervalSeconds"]!, + _configuration["RetryPolicy:RetryBackoffFactor"]!)), + osloProxy); }, connectedProjectionSettings); } diff --git a/src/ParcelRegistry.Producer.Snapshot.Oslo/ProducerProjections.cs b/src/ParcelRegistry.Producer.Snapshot.Oslo/ProducerProjections.cs index 60414eb9..ba186986 100644 --- a/src/ParcelRegistry.Producer.Snapshot.Oslo/ProducerProjections.cs +++ b/src/ParcelRegistry.Producer.Snapshot.Oslo/ProducerProjections.cs @@ -4,6 +4,7 @@ namespace ParcelRegistry.Producer.Snapshot.Oslo using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; + using AllStream.Events; using Be.Vlaanderen.Basisregisters.GrAr.Oslo.SnapshotProducer; using Be.Vlaanderen.Basisregisters.MessageHandling.Kafka; using Be.Vlaanderen.Basisregisters.MessageHandling.Kafka.Producer; @@ -18,10 +19,28 @@ public sealed class ProducerProjections : ConnectedProjection private readonly IProducer _producer; - public ProducerProjections(IProducer producer, ISnapshotManager snapshotManager) + public ProducerProjections( + IProducer producer, + ISnapshotManager snapshotManager, + IOsloProxy osloProxy) { _producer = producer; + When>(async (_, message, ct) => + { + foreach (var caPaKey in message.Message.ParcelIdsWithCapaKey.Values) + { + if (ct.IsCancellationRequested) + { + break; + } + await FindAndProduce(async () => + await osloProxy.GetSnapshot(caPaKey, ct), + message.Position, + ct); + } + }); + When>(async (_, message, ct) => { await FindAndProduce(async () => diff --git a/src/ParcelRegistry/AllStream/AllStream.cs b/src/ParcelRegistry/AllStream/AllStream.cs new file mode 100644 index 00000000..87ed7a40 --- /dev/null +++ b/src/ParcelRegistry/AllStream/AllStream.cs @@ -0,0 +1,17 @@ +namespace ParcelRegistry.AllStream +{ + using System.Collections.Generic; + using System.Linq; + using Be.Vlaanderen.Basisregisters.AggregateSource; + using Events; + using Parcel; + + public sealed class AllStream : AggregateRootEntity + { + public void CreateOsloSnapshots(IReadOnlyList caPaKeys) + { + ApplyChange(new ParcelOsloSnapshotsWereRequested( + caPaKeys.ToDictionary(ParcelId.CreateFor, x => x))); + } + } +} diff --git a/src/ParcelRegistry/AllStream/AllStreamCommandHandlerModule.cs b/src/ParcelRegistry/AllStream/AllStreamCommandHandlerModule.cs new file mode 100644 index 00000000..ee2c8ec1 --- /dev/null +++ b/src/ParcelRegistry/AllStream/AllStreamCommandHandlerModule.cs @@ -0,0 +1,41 @@ +namespace ParcelRegistry.AllStream +{ + using System; + using Be.Vlaanderen.Basisregisters.AggregateSource; + using Be.Vlaanderen.Basisregisters.CommandHandling; + using Be.Vlaanderen.Basisregisters.CommandHandling.SqlStreamStore; + using Be.Vlaanderen.Basisregisters.EventHandling; + using Be.Vlaanderen.Basisregisters.GrAr.Provenance; + using Commands; + using SqlStreamStore; + + public sealed class AllStreamCommandHandlerModule : CommandHandlerModule + { + public AllStreamCommandHandlerModule( + Func allStreamRepository, + Func getUnitOfWork, + Func getStreamStore, + EventMapping eventMapping, + EventSerializer eventSerializer, + IProvenanceFactory provenanceFactory) + { + + For() + .AddSqlStreamStore(getStreamStore, getUnitOfWork, eventMapping, eventSerializer) + .AddProvenance(getUnitOfWork, provenanceFactory) + .Handle(async (message, ct) => + { + var optionalAllStream = await allStreamRepository().GetOptionalAsync(AllStreamId.Instance, ct); + + var allStream = optionalAllStream.HasValue ? optionalAllStream.Value : new AllStream(); + + allStream.CreateOsloSnapshots(message.Command.CaPaKeys); + + if (!optionalAllStream.HasValue) + { + allStreamRepository().Add(AllStreamId.Instance, allStream); + } + }); + } + } +} diff --git a/src/ParcelRegistry/AllStream/AllStreamId.cs b/src/ParcelRegistry/AllStream/AllStreamId.cs new file mode 100644 index 00000000..970df974 --- /dev/null +++ b/src/ParcelRegistry/AllStream/AllStreamId.cs @@ -0,0 +1,22 @@ +namespace ParcelRegistry.AllStream +{ + using System; + using System.Collections.Generic; + using Be.Vlaanderen.Basisregisters.AggregateSource; + + public sealed class AllStreamId : ValueObject + { + public static readonly AllStreamId Instance = new(); + + private readonly Guid _id = new("e8ceb8e6-4cc6-4c15-bcef-7792b3dbc547"); + + private AllStreamId() { } + + protected override IEnumerable Reflect() + { + yield return _id; + } + + public override string ToString() => _id.ToString("D"); + } +} diff --git a/src/ParcelRegistry/AllStream/CommandHandlerModules.cs b/src/ParcelRegistry/AllStream/CommandHandlerModules.cs new file mode 100644 index 00000000..1587678b --- /dev/null +++ b/src/ParcelRegistry/AllStream/CommandHandlerModules.cs @@ -0,0 +1,22 @@ +namespace ParcelRegistry.AllStream +{ + using Autofac; + using Be.Vlaanderen.Basisregisters.CommandHandling; + + public static class CommandHandlerModules + { + public static void Register(ContainerBuilder containerBuilder) + { + containerBuilder + .RegisterType>() + .AsSelf() + .AsImplementedInterfaces() + .SingleInstance(); + + containerBuilder + .RegisterType() + .Named(typeof(AllStreamCommandHandlerModule).FullName) + .As(); + } + } +} diff --git a/src/ParcelRegistry/AllStream/Commands/CreateOsloSnapshots.cs b/src/ParcelRegistry/AllStream/Commands/CreateOsloSnapshots.cs new file mode 100644 index 00000000..073f755c --- /dev/null +++ b/src/ParcelRegistry/AllStream/Commands/CreateOsloSnapshots.cs @@ -0,0 +1,42 @@ +namespace ParcelRegistry.AllStream.Commands +{ + using System; + using System.Collections.Generic; + using System.Linq; + using Be.Vlaanderen.Basisregisters.Generators.Guid; + using Be.Vlaanderen.Basisregisters.GrAr.Provenance; + using Be.Vlaanderen.Basisregisters.Utilities; + + public sealed class CreateOsloSnapshots : IHasCommandProvenance + { + private static readonly Guid Namespace = new Guid("ed0ab2d7-6fd2-4cef-95fa-089f494d34cb"); + + public IReadOnlyList CaPaKeys { get; } + + public Provenance Provenance { get; } + + public CreateOsloSnapshots( + IEnumerable caPaKeys, + Provenance provenance) + { + CaPaKeys = caPaKeys.ToList(); + Provenance = provenance; + } + + public Guid CreateCommandId() + => Deterministic.Create(Namespace, $"CreateOsloSnapshots-{ToString()}"); + + public override string? ToString() + => ToStringBuilder.ToString(IdentityFields()); + + private IEnumerable IdentityFields() + { + yield return CaPaKeys; + + foreach (var field in Provenance.GetIdentityFields()) + { + yield return field; + } + } + } +} diff --git a/src/ParcelRegistry/AllStream/Events/ParcelOsloSnapshotsWereRequested.cs b/src/ParcelRegistry/AllStream/Events/ParcelOsloSnapshotsWereRequested.cs new file mode 100644 index 00000000..e7c7142d --- /dev/null +++ b/src/ParcelRegistry/AllStream/Events/ParcelOsloSnapshotsWereRequested.cs @@ -0,0 +1,54 @@ +namespace ParcelRegistry.AllStream.Events +{ + using System; + using System.Collections.Generic; + using System.Linq; + using Be.Vlaanderen.Basisregisters.EventHandling; + using Be.Vlaanderen.Basisregisters.GrAr.Provenance; + using Newtonsoft.Json; + using Parcel; + + [EventName(EventName)] + [EventDescription("Nieuwe OSLO snapshots werd aangevraagd voor de percelen.")] + public sealed class ParcelOsloSnapshotsWereRequested : IHasProvenance, ISetProvenance, IMessage + { + public const string EventName = "ParcelOsloSnapshotsWereRequested"; // BE CAREFUL CHANGING THIS!! + + [EventPropertyDescription("Interne GUID's van de percelen met de bijhorende CaPaKey.")] + public IDictionary ParcelIdsWithCapaKey { get; } + + [EventPropertyDescription("Metadata bij het event.")] + public ProvenanceData Provenance { get; private set; } + + public ParcelOsloSnapshotsWereRequested( + IDictionary parcelIdsWithCapaKey) + { + ParcelIdsWithCapaKey = parcelIdsWithCapaKey.ToDictionary( + x => (Guid)x.Key, + x => (string)x.Value); + } + + [JsonConstructor] + private ParcelOsloSnapshotsWereRequested( + Guid parcelId, + string caPaKey, + IDictionary parcelIdsWithCapaKey, + ProvenanceData provenance) + : this( + parcelIdsWithCapaKey.ToDictionary( + x => new ParcelId(x.Key), + x => new VbrCaPaKey(x.Value))) + => ((ISetProvenance)this).SetProvenance(provenance.ToProvenance()); + + void ISetProvenance.SetProvenance(Provenance provenance) => Provenance = new ProvenanceData(provenance); + + public IEnumerable GetHashFields() + { + var fields = Provenance.GetHashFields().ToList(); + fields.AddRange(ParcelIdsWithCapaKey.Keys.Select(x => x.ToString("D"))); + fields.AddRange(ParcelIdsWithCapaKey.Values.Select(x => x)); + + return fields; + } + } +} diff --git a/src/ParcelRegistry/AllStream/IAllStreamRepository.cs b/src/ParcelRegistry/AllStream/IAllStreamRepository.cs new file mode 100644 index 00000000..b0fad138 --- /dev/null +++ b/src/ParcelRegistry/AllStream/IAllStreamRepository.cs @@ -0,0 +1,9 @@ +namespace ParcelRegistry.AllStream +{ + using Be.Vlaanderen.Basisregisters.AggregateSource; + + public interface IAllStreamRepository : IAsyncRepository + { + + } +} diff --git a/src/ParcelRegistry/Parcel/ProvenanceFactory.cs b/src/ParcelRegistry/ProvenanceFactory.cs similarity index 97% rename from src/ParcelRegistry/Parcel/ProvenanceFactory.cs rename to src/ParcelRegistry/ProvenanceFactory.cs index 2c6ffe2c..47becede 100644 --- a/src/ParcelRegistry/Parcel/ProvenanceFactory.cs +++ b/src/ParcelRegistry/ProvenanceFactory.cs @@ -1,4 +1,4 @@ -namespace ParcelRegistry.Parcel +namespace ParcelRegistry { using System; using Be.Vlaanderen.Basisregisters.AggregateSource; diff --git a/test/ParcelRegistry.Tests/AggregateTests/WhenRequestingCreateOsloSnapshots/GivenAllStreamDoesNotExist.cs b/test/ParcelRegistry.Tests/AggregateTests/WhenRequestingCreateOsloSnapshots/GivenAllStreamDoesNotExist.cs new file mode 100644 index 00000000..c5267e36 --- /dev/null +++ b/test/ParcelRegistry.Tests/AggregateTests/WhenRequestingCreateOsloSnapshots/GivenAllStreamDoesNotExist.cs @@ -0,0 +1,35 @@ +namespace ParcelRegistry.Tests.AggregateTests.WhenRequestingCreateOsloSnapshots +{ + using System.Linq; + using AllStream; + using AllStream.Commands; + using AllStream.Events; + using AutoFixture; + using Be.Vlaanderen.Basisregisters.AggregateSource.Testing; + using Be.Vlaanderen.Basisregisters.GrAr.Provenance; + using Parcel; + using Xunit; + using Xunit.Abstractions; + + public class GivenAllStreamDoesNotExist : ParcelRegistryTest + { + public GivenAllStreamDoesNotExist(ITestOutputHelper testOutputHelper) : base(testOutputHelper) + { + Fixture.Customize(new WithExtendedWkbGeometryPolygon()); + } + + [Fact] + public void ThenParcelOsloSnapshotsWereRequested() + { + var command = new CreateOsloSnapshots( + [new VbrCaPaKey("11001B0001-00X000"), new VbrCaPaKey("11001B0001-00S000")], + Fixture.Create()); + + Assert(new Scenario() + .When(command) + .Then(AllStreamId.Instance, + new ParcelOsloSnapshotsWereRequested( + command.CaPaKeys.ToDictionary(ParcelId.CreateFor, x => x)))); + } + } +} diff --git a/test/ParcelRegistry.Tests/AggregateTests/WhenRequestingCreateOsloSnapshots/GivenAllStreamExists.cs b/test/ParcelRegistry.Tests/AggregateTests/WhenRequestingCreateOsloSnapshots/GivenAllStreamExists.cs new file mode 100644 index 00000000..ac588306 --- /dev/null +++ b/test/ParcelRegistry.Tests/AggregateTests/WhenRequestingCreateOsloSnapshots/GivenAllStreamExists.cs @@ -0,0 +1,36 @@ +namespace ParcelRegistry.Tests.AggregateTests.WhenRequestingCreateOsloSnapshots +{ + using System.Linq; + using AllStream; + using AllStream.Commands; + using AllStream.Events; + using AutoFixture; + using Be.Vlaanderen.Basisregisters.AggregateSource.Testing; + using Be.Vlaanderen.Basisregisters.GrAr.Provenance; + using Parcel; + using Xunit; + using Xunit.Abstractions; + + public class GivenParcelsExist : ParcelRegistryTest + { + public GivenParcelsExist(ITestOutputHelper testOutputHelper) : base(testOutputHelper) + { + Fixture.Customize(new WithExtendedWkbGeometryPolygon()); + } + + [Fact] + public void ThenParcelOsloSnapshotsWereRequested() + { + var command = new CreateOsloSnapshots( + [new VbrCaPaKey("11001B0001-00X000"), new VbrCaPaKey("11001B0001-00S000")], + Fixture.Create()); + + Assert(new Scenario() + .Given(AllStreamId.Instance) + .When(command) + .Then(AllStreamId.Instance, + new ParcelOsloSnapshotsWereRequested( + command.CaPaKeys.ToDictionary(ParcelId.CreateFor, x => x)))); + } + } +} diff --git a/test/ParcelRegistry.Tests/BackOffice/Api/WhenRequestingCreateOsloSnapshots/GivenRequest.cs b/test/ParcelRegistry.Tests/BackOffice/Api/WhenRequestingCreateOsloSnapshots/GivenRequest.cs new file mode 100644 index 00000000..c4876803 --- /dev/null +++ b/test/ParcelRegistry.Tests/BackOffice/Api/WhenRequestingCreateOsloSnapshots/GivenRequest.cs @@ -0,0 +1,70 @@ +namespace ParcelRegistry.Tests.BackOffice.Api.WhenRequestingCreateOsloSnapshots +{ + using System; + using System.Collections.Generic; + using System.Linq; + using System.Threading; + using System.Threading.Tasks; + using AutoFixture; + using Be.Vlaanderen.Basisregisters.GrAr.Provenance; + using Be.Vlaanderen.Basisregisters.Sqs.Requests; + using Fixtures; + using FluentAssertions; + using Microsoft.AspNetCore.Mvc; + using Moq; + using NodaTime; + using ParcelRegistry.Api.BackOffice; + using ParcelRegistry.Api.BackOffice.Abstractions.Requests; + using ParcelRegistry.Api.BackOffice.Abstractions.SqsRequests; + using Xunit; + using Xunit.Abstractions; + + public class GivenRequest : BackOfficeApiTest + { + private readonly ParcelController _controller; + + public GivenRequest(ITestOutputHelper testOutputHelper) : base(testOutputHelper) + { + Fixture.Customize(new WithValidVbrCaPaKey()); + + _controller = CreateParcelControllerWithUser(); + } + + [Fact] + public async Task ThenTicketLocationIsReturned() + { + var vbrCaPaKeys = Fixture.Create>(); + + var ticketId = Fixture.Create(); + var expectedLocationResult = new LocationResult(CreateTicketUri(ticketId)); + + MockMediator + .Setup(x => x.Send( + It.IsAny(), + CancellationToken.None)) + .Returns(Task.FromResult(expectedLocationResult)); + + var request = new CreateOsloSnapshotsRequest + { + CaPaKeys = vbrCaPaKeys.Select(x => (string)x).ToList(), + Reden = "UnitTest" + }; + + var result = (AcceptedResult)await _controller.CreateOsloSnapshots(request); + + result.Should().NotBeNull(); + AssertLocation(result.Location, ticketId); + + MockMediator.Verify(x => + x.Send( + It.Is(sqsRequest => + sqsRequest.Request == request + && sqsRequest.ProvenanceData.Timestamp != Instant.MinValue + && sqsRequest.ProvenanceData.Application == Application.ParcelRegistry + && sqsRequest.ProvenanceData.Modification == Modification.Unknown + && sqsRequest.ProvenanceData.Reason == request.Reden + ), + CancellationToken.None)); + } + } +} diff --git a/test/ParcelRegistry.Tests/BackOffice/Handler/GivenCreateOsloSnapshotsBackOfficeRequest.cs b/test/ParcelRegistry.Tests/BackOffice/Handler/GivenCreateOsloSnapshotsBackOfficeRequest.cs new file mode 100644 index 00000000..8020108b --- /dev/null +++ b/test/ParcelRegistry.Tests/BackOffice/Handler/GivenCreateOsloSnapshotsBackOfficeRequest.cs @@ -0,0 +1,76 @@ +namespace ParcelRegistry.Tests.BackOffice.Handler +{ + using System; + using System.Collections.Generic; + using System.Linq; + using System.Threading; + using System.Threading.Tasks; + using AllStream; + using AutoFixture; + using Be.Vlaanderen.Basisregisters.MessageHandling.AwsSqs.Simple; + using Be.Vlaanderen.Basisregisters.Sqs; + using Fixtures; + using FluentAssertions; + using Moq; + using ParcelRegistry.Api.BackOffice.Abstractions.Requests; + using ParcelRegistry.Api.BackOffice.Abstractions.SqsRequests; + using ParcelRegistry.Api.BackOffice.Handlers; + using TicketingService.Abstractions; + using Xunit; + using Xunit.Abstractions; + + public class GivenCreateOsloSnapshotsBackOfficeRequest : ParcelRegistryTest + { + public GivenCreateOsloSnapshotsBackOfficeRequest(ITestOutputHelper testOutputHelper) : base(testOutputHelper) + { + Fixture.Customize(new WithValidVbrCaPaKey()); + } + + [Fact] + public async Task ThenTicketWithLocationIsCreated() + { + // Arrange + var ticketId = Fixture.Create(); + var ticketingMock = new Mock(); + ticketingMock + .Setup(x => x.CreateTicket(It.IsAny>(), CancellationToken.None)) + .ReturnsAsync(ticketId); + + var ticketingUrl = new TicketingUrl(Fixture.Create().ToString()); + + var sqsQueue = new Mock(); + + var sut = new CreateOsloSnapshotsHandler( + sqsQueue.Object, + ticketingMock.Object, + ticketingUrl); + + var sqsRequest = new CreateOsloSnapshotsSqsRequest + { + Request = new CreateOsloSnapshotsRequest + { + CaPaKeys = Fixture.Create>().Select(x => (string)x).ToList() + } + }; + + // Act + var result = await sut.Handle(sqsRequest, CancellationToken.None); + + // Assert + sqsRequest.TicketId.Should().Be(ticketId); + + ticketingMock.Verify(x => x.CreateTicket(new Dictionary + { + { AttachAddressHandler.RegistryKey, nameof(ParcelRegistry) }, + { AttachAddressHandler.ActionKey, "CreateOsloSnapshots" }, + { AttachAddressHandler.AggregateIdKey, AllStreamId.Instance }, + }, CancellationToken.None)); + + sqsQueue.Verify(x => x.Copy( + sqsRequest, + It.Is(y => y.MessageGroupId == AllStreamId.Instance.ToString()), + CancellationToken.None)); + result.Location.Should().Be(ticketingUrl.For(ticketId)); + } + } +} diff --git a/test/ParcelRegistry.Tests/BackOffice/Lambda/MessageHandlerTests.cs b/test/ParcelRegistry.Tests/BackOffice/Lambda/MessageHandlerTests.cs index 501ac6f7..0b2ddb1c 100644 --- a/test/ParcelRegistry.Tests/BackOffice/Lambda/MessageHandlerTests.cs +++ b/test/ParcelRegistry.Tests/BackOffice/Lambda/MessageHandlerTests.cs @@ -90,6 +90,38 @@ await sut.HandleMessage( ), It.IsAny()), Times.Once); } + [Fact] + public async Task WhenProcessingCreateOsloSnapshotsSqsRequest_ThenCreateOsloSnapshotsLambdaRequestIsSent() + { + // Arrange + var mediator = new Mock(); + var containerBuilder = new ContainerBuilder(); + containerBuilder.Register(_ => mediator.Object); + var container = containerBuilder.Build(); + + var messageData = Fixture.Create(); + var messageMetadata = new MessageMetadata { MessageGroupId = Fixture.Create() }; + + var sut = new MessageHandler(container); + + // Act + await sut.HandleMessage( + messageData, + messageMetadata, + CancellationToken.None); + + // Assert + mediator + .Verify(x => x.Send(It.Is(request => + request.TicketId == messageData.TicketId && + request.MessageGroupId == messageMetadata.MessageGroupId && + request.Request == messageData.Request && + string.IsNullOrEmpty(request.IfMatchHeaderValue) && + request.Provenance == messageData.ProvenanceData.ToProvenance() && + request.Metadata == messageData.Metadata + ), It.IsAny()), Times.Once); + } + [Fact] public async Task WhenProcessingUnknownMessage_ThenNothingIsSent() { diff --git a/test/ParcelRegistry.Tests/BackOffice/Lambda/WhenCreatingOsloSnapshotsRequest.cs b/test/ParcelRegistry.Tests/BackOffice/Lambda/WhenCreatingOsloSnapshotsRequest.cs new file mode 100644 index 00000000..b8e56729 --- /dev/null +++ b/test/ParcelRegistry.Tests/BackOffice/Lambda/WhenCreatingOsloSnapshotsRequest.cs @@ -0,0 +1,113 @@ +namespace ParcelRegistry.Tests.BackOffice.Lambda +{ + using System; + using System.Linq; + using System.Threading; + using System.Threading.Tasks; + using AllStream; + using Autofac; + using AutoFixture; + using Be.Vlaanderen.Basisregisters.CommandHandling; + using Be.Vlaanderen.Basisregisters.CommandHandling.Idempotency; + using Be.Vlaanderen.Basisregisters.GrAr.Provenance; + using Fixtures; + using FluentAssertions; + using Moq; + using ParcelRegistry.Api.BackOffice.Abstractions.Requests; + using ParcelRegistry.Api.BackOffice.Abstractions.SqsRequests; + using ParcelRegistry.Api.BackOffice.Handlers.Lambda.Handlers; + using ParcelRegistry.Api.BackOffice.Handlers.Lambda.Requests; + using SqlStreamStore; + using SqlStreamStore.Streams; + using TicketingService.Abstractions; + using Xunit; + using Xunit.Abstractions; + + public class WhenCreatingOsloSnapshotsRequest : LambdaHandlerTest + { + private readonly IdempotencyContext _idempotencyContext; + + public WhenCreatingOsloSnapshotsRequest(ITestOutputHelper testOutputHelper) : base(testOutputHelper) + { + Fixture.Customize(new WithValidVbrCaPaKey()); + + _idempotencyContext = new FakeIdempotencyContextFactory().CreateDbContext([]); + } + + [Fact] + public async Task ThenTicketingCompleteIsExpected() + { + // Arrange + var ticketing = new Mock(); + + var handler = new CreateOsloSnapshotsLambdaHandler( + new FakeRetryPolicy(), + ticketing.Object, + new IdempotentCommandHandler(Container.Resolve(), _idempotencyContext)); + + // Act + var ticketId = Guid.NewGuid(); + await handler.Handle( + new CreateOsloSnapshotsLambdaRequest( + AllStreamId.Instance, + new CreateOsloSnapshotsSqsRequest + { + TicketId = ticketId, + Request = new CreateOsloSnapshotsRequest + { + CaPaKeys = ["11001B0001-00X000"] + }, + ProvenanceData = Fixture.Create() + }), + CancellationToken.None); + + //Assert + ticketing.Verify(x => + x.Complete( + ticketId, + new TicketResult("done"), + CancellationToken.None)); + + //Assert + var stream = await Container.Resolve().ReadStreamBackwards(new StreamId(AllStreamId.Instance), 0, 1); + var message = stream.Messages.First(); + message.JsonMetadata.Should().Contain(Provenance.ProvenanceMetadataKey.ToLower()); + } + + [Fact] + public async Task WhenIdempotencyException_ThenTicketingCompleteIsExpected() + { + // Arrange + var ticketing = new Mock(); + + + var handler = new CreateOsloSnapshotsLambdaHandler( + new FakeRetryPolicy(), + ticketing.Object, + MockExceptionIdempotentCommandHandler(() => new IdempotencyException(string.Empty)).Object); + + // Act + var ticketId = Guid.NewGuid(); + await handler.Handle( + new CreateOsloSnapshotsLambdaRequest( + AllStreamId.Instance, + new CreateOsloSnapshotsSqsRequest + { + TicketId = ticketId, + Request = new CreateOsloSnapshotsRequest + { + CaPaKeys = ["11001B0001-00X000"] + }, + ProvenanceData = Fixture.Create() + }), + CancellationToken.None); + + //Assert + ticketing.Verify(x => + x.Complete( + ticketId, + new TicketResult("done"), + CancellationToken.None)); + } + } +}