From 6792871f7be3e91f61aa4bf438d1b3233593ee87 Mon Sep 17 00:00:00 2001 From: Arne Dumarey Date: Wed, 6 Nov 2024 16:19:22 +0100 Subject: [PATCH] feat: add snapshot on request --- paket.dependencies | 24 +++--- paket.lock | 60 +++++++-------- .../Requests/CreateBuildingSnapshotRequest.cs | 7 ++ .../CreateBuildingSnapshotSqsRequest.cs | 10 +++ .../CreateBuildingSnapshotLambdaHandler.cs | 56 ++++++++++++++ .../MessageHandler.cs | 7 ++ .../CreateBuildingSnapshotLambdaRequest.cs | 38 ++++++++++ .../BuildingController-CreateSnapshot.cs | 61 ++++++++++++++++ ... CreateBuildingOsloSnapshotsSqsHandler.cs} | 0 .../CreateBuildingSnapshotSqsHandler.cs | 35 +++++++++ .../Modules/AggregateSourceModule.cs | 5 +- src/BuildingRegistry/Building/Building.cs | 5 ++ .../Building/BuildingCommandHandlerModule.cs | 14 +++- .../Building/Commands/CreateOsloSnapshots.cs | 41 +++++++++++ .../Events/BuildingSnapshotWasRequested.cs | 45 ++++++++++++ .../SnapshotTests/GivenBuilding.cs | 54 ++++++++++++++ .../GivenBuildingExists.cs | 35 +++++++++ .../GivenNoBuildingExists.cs | 28 +++++++ .../GivenRequest.cs | 73 +++++++++++++++++++ .../ProjectionsHandlesEventsTests.cs | 2 +- 20 files changed, 555 insertions(+), 45 deletions(-) create mode 100644 src/BuildingRegistry.Api.BackOffice.Abstractions/Building/Requests/CreateBuildingSnapshotRequest.cs create mode 100644 src/BuildingRegistry.Api.BackOffice.Abstractions/Building/SqsRequests/CreateBuildingSnapshotSqsRequest.cs create mode 100644 src/BuildingRegistry.Api.BackOffice.Handlers.Lambda/Handlers/Building/CreateBuildingSnapshotLambdaHandler.cs create mode 100644 src/BuildingRegistry.Api.BackOffice.Handlers.Lambda/Requests/Building/CreateBuildingSnapshotLambdaRequest.cs create mode 100644 src/BuildingRegistry.Api.BackOffice/Building/BuildingController-CreateSnapshot.cs rename src/BuildingRegistry.Api.BackOffice/Handlers/Building/{CreateBuildingUnitOsloSnapshotsSqsHandler.cs => CreateBuildingOsloSnapshotsSqsHandler.cs} (100%) create mode 100644 src/BuildingRegistry.Api.BackOffice/Handlers/Building/CreateBuildingSnapshotSqsHandler.cs create mode 100644 src/BuildingRegistry/Building/Commands/CreateOsloSnapshots.cs create mode 100644 src/BuildingRegistry/Building/Events/BuildingSnapshotWasRequested.cs create mode 100644 test/BuildingRegistry.Tests/AggregateTests/WhenRequestingBuildingSnapshot/GivenBuildingExists.cs create mode 100644 test/BuildingRegistry.Tests/AggregateTests/WhenRequestingBuildingSnapshot/GivenNoBuildingExists.cs create mode 100644 test/BuildingRegistry.Tests/BackOffice/Api/Building/WhenRequestingCreateBuildingSnapshot/GivenRequest.cs diff --git a/paket.dependencies b/paket.dependencies index 8ae271a55..83545f581 100644 --- a/paket.dependencies +++ b/paket.dependencies @@ -34,18 +34,18 @@ nuget Npgsql.EntityFrameworkCore.PostgreSQL.Design 2.0.0-preview1 nuget Be.Vlaanderen.Basisregisters.Build.Pipeline 7.2.5 nuget Be.Vlaanderen.Basisregisters.Testing.Infrastructure.Events 4.0.0 content: true, copy_content_to_output_dir: always, copy_local: true -nuget Be.Vlaanderen.Basisregisters.AggregateSource 9.0.1 -nuget Be.Vlaanderen.Basisregisters.AggregateSource.ExplicitRouting 9.0.1 -nuget Be.Vlaanderen.Basisregisters.AggregateSource.SqlStreamStore.Autofac 9.0.1 -nuget Be.Vlaanderen.Basisregisters.AggregateSource.Testing 9.0.1 -nuget Be.Vlaanderen.Basisregisters.AggregateSource.Testing.SqlStreamStore.Autofac 9.0.1 -nuget Be.Vlaanderen.Basisregisters.AggregateSource.Testing.Xunit 9.0.1 - -nuget Be.Vlaanderen.Basisregisters.CommandHandling 9.0.1 -nuget Be.Vlaanderen.Basisregisters.CommandHandling.Idempotency 9.0.1 -nuget Be.Vlaanderen.Basisregisters.CommandHandling.SqlStreamStore 9.0.1 - -nuget Be.Vlaanderen.Basisregisters.Snapshotverifier 9.0.1 +nuget Be.Vlaanderen.Basisregisters.AggregateSource 9.1.1 +nuget Be.Vlaanderen.Basisregisters.AggregateSource.ExplicitRouting 9.1.1 +nuget Be.Vlaanderen.Basisregisters.AggregateSource.SqlStreamStore.Autofac 9.1.1 +nuget Be.Vlaanderen.Basisregisters.AggregateSource.Testing 9.1.1 +nuget Be.Vlaanderen.Basisregisters.AggregateSource.Testing.SqlStreamStore.Autofac 9.1.1 +nuget Be.Vlaanderen.Basisregisters.AggregateSource.Testing.Xunit 9.1.1 + +nuget Be.Vlaanderen.Basisregisters.CommandHandling 9.1.1 +nuget Be.Vlaanderen.Basisregisters.CommandHandling.Idempotency 9.1.1 +nuget Be.Vlaanderen.Basisregisters.CommandHandling.SqlStreamStore 9.1.1 + +nuget Be.Vlaanderen.Basisregisters.Snapshotverifier 9.1.1 nuget Be.Vlaanderen.Basisregisters.Api 23.1.0 diff --git a/paket.lock b/paket.lock index 78a5a84ff..de784e1b9 100644 --- a/paket.lock +++ b/paket.lock @@ -63,44 +63,44 @@ NUGET System.Security.Cryptography.ProtectedData (>= 4.7) System.Text.Json (>= 4.7.2) System.Threading.Tasks.Extensions (>= 4.5.4) - Be.Vlaanderen.Basisregisters.AggregateSource (9.0.1) + Be.Vlaanderen.Basisregisters.AggregateSource (9.1.1) Be.Vlaanderen.Basisregisters.Utilities.HashCodeCalculator (>= 4.0) Be.Vlaanderen.Basisregisters.Utilities.HexByteConvertor (>= 4.0) Be.Vlaanderen.Basisregisters.Utilities.ToStringBuilder (>= 4.0) Newtonsoft.Json (>= 13.0.3) NodaTime (>= 3.1.11) - Be.Vlaanderen.Basisregisters.AggregateSource.ExplicitRouting (9.0.1) - Be.Vlaanderen.Basisregisters.AggregateSource (9.0.1) - Be.Vlaanderen.Basisregisters.AggregateSource.SqlStreamStore (9.0.1) - Be.Vlaanderen.Basisregisters.AggregateSource (9.0.1) + Be.Vlaanderen.Basisregisters.AggregateSource.ExplicitRouting (9.1.1) + Be.Vlaanderen.Basisregisters.AggregateSource (9.1.1) + Be.Vlaanderen.Basisregisters.AggregateSource.SqlStreamStore (9.1.1) + Be.Vlaanderen.Basisregisters.AggregateSource (9.1.1) Be.Vlaanderen.Basisregisters.EventHandling (>= 5.0) SqlStreamStore (>= 1.2) - Be.Vlaanderen.Basisregisters.AggregateSource.SqlStreamStore.Autofac (9.0.1) + Be.Vlaanderen.Basisregisters.AggregateSource.SqlStreamStore.Autofac (9.1.1) Autofac.Extensions.DependencyInjection (>= 9.0) - Be.Vlaanderen.Basisregisters.AggregateSource (9.0.1) - Be.Vlaanderen.Basisregisters.AggregateSource.SqlStreamStore (9.0.1) + Be.Vlaanderen.Basisregisters.AggregateSource (9.1.1) + Be.Vlaanderen.Basisregisters.AggregateSource.SqlStreamStore (9.1.1) SqlStreamStore (>= 1.2) SqlStreamStore.MsSql (>= 1.2) - Be.Vlaanderen.Basisregisters.AggregateSource.Testing (9.0.1) - Be.Vlaanderen.Basisregisters.AggregateSource (9.0.1) + Be.Vlaanderen.Basisregisters.AggregateSource.Testing (9.1.1) + Be.Vlaanderen.Basisregisters.AggregateSource (9.1.1) CompareNETObjects (>= 4.83) Microsoft.CSharp (>= 4.7) Newtonsoft.Json (>= 13.0.3) - Be.Vlaanderen.Basisregisters.AggregateSource.Testing.CommandHandling (9.0.1) - Be.Vlaanderen.Basisregisters.AggregateSource.Testing (9.0.1) - Be.Vlaanderen.Basisregisters.CommandHandling (9.0.1) - Be.Vlaanderen.Basisregisters.AggregateSource.Testing.SqlStreamStore (9.0.1) - Be.Vlaanderen.Basisregisters.AggregateSource.Testing (9.0.1) + Be.Vlaanderen.Basisregisters.AggregateSource.Testing.CommandHandling (9.1.1) + Be.Vlaanderen.Basisregisters.AggregateSource.Testing (9.1.1) + Be.Vlaanderen.Basisregisters.CommandHandling (9.1.1) + Be.Vlaanderen.Basisregisters.AggregateSource.Testing.SqlStreamStore (9.1.1) + Be.Vlaanderen.Basisregisters.AggregateSource.Testing (9.1.1) Be.Vlaanderen.Basisregisters.EventHandling (>= 5.0) SqlStreamStore (>= 1.2) - Be.Vlaanderen.Basisregisters.AggregateSource.Testing.SqlStreamStore.Autofac (9.0.1) + Be.Vlaanderen.Basisregisters.AggregateSource.Testing.SqlStreamStore.Autofac (9.1.1) Autofac.Extensions.DependencyInjection (>= 9.0) - Be.Vlaanderen.Basisregisters.AggregateSource.SqlStreamStore.Autofac (9.0.1) - Be.Vlaanderen.Basisregisters.AggregateSource.Testing.CommandHandling (9.0.1) - Be.Vlaanderen.Basisregisters.AggregateSource.Testing.SqlStreamStore (9.0.1) - Be.Vlaanderen.Basisregisters.AggregateSource.Testing.Xunit (9.0.1) - Be.Vlaanderen.Basisregisters.AggregateSource.Testing.Xunit (9.0.1) - Be.Vlaanderen.Basisregisters.AggregateSource.Testing (9.0.1) + Be.Vlaanderen.Basisregisters.AggregateSource.SqlStreamStore.Autofac (9.1.1) + Be.Vlaanderen.Basisregisters.AggregateSource.Testing.CommandHandling (9.1.1) + Be.Vlaanderen.Basisregisters.AggregateSource.Testing.SqlStreamStore (9.1.1) + Be.Vlaanderen.Basisregisters.AggregateSource.Testing.Xunit (9.1.1) + Be.Vlaanderen.Basisregisters.AggregateSource.Testing.Xunit (9.1.1) + Be.Vlaanderen.Basisregisters.AggregateSource.Testing (9.1.1) Microsoft.Extensions.Logging (>= 8.0) xunit (>= 2.7) Be.Vlaanderen.Basisregisters.Api (23.1) @@ -224,9 +224,9 @@ NUGET Microsoft.Data.SqlClient (>= 5.2) System.Collections.Immutable (>= 8.0) Be.Vlaanderen.Basisregisters.Build.Pipeline (7.2.5) - Be.Vlaanderen.Basisregisters.CommandHandling (9.0.1) - Be.Vlaanderen.Basisregisters.CommandHandling.Idempotency (9.0.1) - Be.Vlaanderen.Basisregisters.CommandHandling (9.0.1) + Be.Vlaanderen.Basisregisters.CommandHandling (9.1.1) + Be.Vlaanderen.Basisregisters.CommandHandling.Idempotency (9.1.1) + Be.Vlaanderen.Basisregisters.CommandHandling (9.1.1) Be.Vlaanderen.Basisregisters.Converters.Timestamp (>= 4.0) Be.Vlaanderen.Basisregisters.Utilities.HexByteConvertor (>= 4.0) Microsoft.Data.SqlClient (>= 5.2) @@ -234,9 +234,9 @@ NUGET Microsoft.Extensions.Configuration (>= 8.0) Microsoft.Extensions.Options.ConfigurationExtensions (>= 8.0) Newtonsoft.Json (>= 13.0.3) - Be.Vlaanderen.Basisregisters.CommandHandling.SqlStreamStore (9.0.1) - Be.Vlaanderen.Basisregisters.AggregateSource (9.0.1) - Be.Vlaanderen.Basisregisters.CommandHandling (9.0.1) + Be.Vlaanderen.Basisregisters.CommandHandling.SqlStreamStore (9.1.1) + Be.Vlaanderen.Basisregisters.AggregateSource (9.1.1) + Be.Vlaanderen.Basisregisters.CommandHandling (9.1.1) Be.Vlaanderen.Basisregisters.EventHandling (>= 5.0) Be.Vlaanderen.Basisregisters.Generators.Guid.Deterministic (>= 4.0) SqlStreamStore (>= 1.2) @@ -401,8 +401,8 @@ NUGET System.Threading.Tasks.Dataflow (>= 8.0) Be.Vlaanderen.Basisregisters.Shaperon (10.0.2) Be.Vlaanderen.Basisregisters.Shaperon.Geometries (10.0.2) - Be.Vlaanderen.Basisregisters.SnapshotVerifier (9.0.1) - Be.Vlaanderen.Basisregisters.AggregateSource.SqlStreamStore (9.0.1) + Be.Vlaanderen.Basisregisters.SnapshotVerifier (9.1.1) + Be.Vlaanderen.Basisregisters.AggregateSource.SqlStreamStore (9.1.1) CompareNETObjects (>= 4.83) Dapper (>= 2.1.35) Microsoft.Extensions.Hosting (>= 8.0) diff --git a/src/BuildingRegistry.Api.BackOffice.Abstractions/Building/Requests/CreateBuildingSnapshotRequest.cs b/src/BuildingRegistry.Api.BackOffice.Abstractions/Building/Requests/CreateBuildingSnapshotRequest.cs new file mode 100644 index 000000000..4d1f8bbbc --- /dev/null +++ b/src/BuildingRegistry.Api.BackOffice.Abstractions/Building/Requests/CreateBuildingSnapshotRequest.cs @@ -0,0 +1,7 @@ +namespace BuildingRegistry.Api.BackOffice.Abstractions.Building.Requests +{ + public sealed class CreateBuildingSnapshotRequest + { + public int PersistentLocalId { get; set; } + } +} diff --git a/src/BuildingRegistry.Api.BackOffice.Abstractions/Building/SqsRequests/CreateBuildingSnapshotSqsRequest.cs b/src/BuildingRegistry.Api.BackOffice.Abstractions/Building/SqsRequests/CreateBuildingSnapshotSqsRequest.cs new file mode 100644 index 000000000..08c9e5578 --- /dev/null +++ b/src/BuildingRegistry.Api.BackOffice.Abstractions/Building/SqsRequests/CreateBuildingSnapshotSqsRequest.cs @@ -0,0 +1,10 @@ +namespace BuildingRegistry.Api.BackOffice.Abstractions.Building.SqsRequests +{ + using Be.Vlaanderen.Basisregisters.Sqs.Requests; + using Requests; + + public sealed class CreateBuildingSnapshotSqsRequest : SqsRequest + { + public CreateBuildingSnapshotRequest Request { get; set; } + } +} diff --git a/src/BuildingRegistry.Api.BackOffice.Handlers.Lambda/Handlers/Building/CreateBuildingSnapshotLambdaHandler.cs b/src/BuildingRegistry.Api.BackOffice.Handlers.Lambda/Handlers/Building/CreateBuildingSnapshotLambdaHandler.cs new file mode 100644 index 000000000..4b30c74c9 --- /dev/null +++ b/src/BuildingRegistry.Api.BackOffice.Handlers.Lambda/Handlers/Building/CreateBuildingSnapshotLambdaHandler.cs @@ -0,0 +1,56 @@ +namespace BuildingRegistry.Api.BackOffice.Handlers.Lambda.Handlers.Building +{ + using Abstractions; + using Abstractions.Validation; + using Be.Vlaanderen.Basisregisters.AggregateSource; + using Be.Vlaanderen.Basisregisters.CommandHandling.Idempotency; + using Be.Vlaanderen.Basisregisters.Sqs.Lambda.Infrastructure; + using Be.Vlaanderen.Basisregisters.Sqs.Responses; + using BuildingRegistry.Building; + using BuildingRegistry.Building.Exceptions; + using Microsoft.Extensions.Configuration; + using Requests.Building; + using TicketingService.Abstractions; + + public sealed class CreateBuildingSnapshotLambdaHandler : BuildingLambdaHandler + { + public CreateBuildingSnapshotLambdaHandler( + IConfiguration configuration, + ICustomRetryPolicy retryPolicy, + ITicketing ticketing, + IIdempotentCommandHandler idempotentCommandHandler, + IBuildings buildings) + : base( + configuration, + retryPolicy, + ticketing, + idempotentCommandHandler, + buildings) + { } + + protected override async Task InnerHandle(CreateBuildingSnapshotLambdaRequest 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 "snapshot created"; + } + + protected override TicketError? InnerMapDomainException(DomainException exception, CreateBuildingSnapshotLambdaRequest request) + { + return null; + } + } +} diff --git a/src/BuildingRegistry.Api.BackOffice.Handlers.Lambda/MessageHandler.cs b/src/BuildingRegistry.Api.BackOffice.Handlers.Lambda/MessageHandler.cs index 22f2b7517..2f6514b54 100644 --- a/src/BuildingRegistry.Api.BackOffice.Handlers.Lambda/MessageHandler.cs +++ b/src/BuildingRegistry.Api.BackOffice.Handlers.Lambda/MessageHandler.cs @@ -10,6 +10,7 @@ namespace BuildingRegistry.Api.BackOffice.Handlers.Lambda using Requests.BuildingUnit; using System.Threading; using System.Threading.Tasks; + using Abstractions.Building.Requests; using Building; public class MessageHandler : IMessageHandler @@ -185,6 +186,12 @@ await mediator.Send( cancellationToken); break; + case CreateBuildingSnapshotSqsRequest request: + await mediator.Send( + new CreateBuildingSnapshotLambdaRequest(messageMetadata.MessageGroupId!, request), + cancellationToken); + break; + default: throw new NotImplementedException( $"{messageData.GetType().Name} has no corresponding SqsLambdaRequest defined."); diff --git a/src/BuildingRegistry.Api.BackOffice.Handlers.Lambda/Requests/Building/CreateBuildingSnapshotLambdaRequest.cs b/src/BuildingRegistry.Api.BackOffice.Handlers.Lambda/Requests/Building/CreateBuildingSnapshotLambdaRequest.cs new file mode 100644 index 000000000..55515b3e2 --- /dev/null +++ b/src/BuildingRegistry.Api.BackOffice.Handlers.Lambda/Requests/Building/CreateBuildingSnapshotLambdaRequest.cs @@ -0,0 +1,38 @@ +namespace BuildingRegistry.Api.BackOffice.Handlers.Lambda.Requests.Building +{ + using Abstractions.Building.Requests; + using Abstractions.Building.SqsRequests; + using BuildingRegistry.Building; + using BuildingRegistry.Building.Commands; + using IHasBuildingPersistentLocalId = Abstractions.IHasBuildingPersistentLocalId; + + public sealed record CreateBuildingSnapshotLambdaRequest : BuildingLambdaRequest, IHasBuildingPersistentLocalId + { + public CreateBuildingSnapshotRequest Request { get; } + public int BuildingPersistentLocalId => Request.PersistentLocalId; + + public CreateBuildingSnapshotLambdaRequest( + string messageGroupId, + CreateBuildingSnapshotSqsRequest sqsRequest) + : base( + messageGroupId, + sqsRequest.TicketId, + null, + sqsRequest.ProvenanceData.ToProvenance(), + sqsRequest.Metadata) + { + Request = sqsRequest.Request; + } + + /// + /// Map to CreateSnapshot command + /// + /// CreateSnapshot + public CreateSnapshot ToCommand() + { + return new CreateSnapshot( + new BuildingPersistentLocalId(BuildingPersistentLocalId), + Provenance); + } + } +} diff --git a/src/BuildingRegistry.Api.BackOffice/Building/BuildingController-CreateSnapshot.cs b/src/BuildingRegistry.Api.BackOffice/Building/BuildingController-CreateSnapshot.cs new file mode 100644 index 000000000..a10e8ee8c --- /dev/null +++ b/src/BuildingRegistry.Api.BackOffice/Building/BuildingController-CreateSnapshot.cs @@ -0,0 +1,61 @@ +namespace BuildingRegistry.Api.BackOffice.Building +{ + using System.Threading; + using System.Threading.Tasks; + using Abstractions.Building.Requests; + using Abstractions.Building.Validators; + using Abstractions.Building.SqsRequests; + using Abstractions.Validation; + using Be.Vlaanderen.Basisregisters.Auth.AcmIdm; + using Be.Vlaanderen.Basisregisters.Api.ETag; + using Be.Vlaanderen.Basisregisters.Api.Exceptions; + using Be.Vlaanderen.Basisregisters.GrAr.Provenance; + using BuildingRegistry.Building; + using Infrastructure; + using Microsoft.AspNetCore.Authentication.JwtBearer; + using Microsoft.AspNetCore.Authorization; + using Microsoft.AspNetCore.Http; + using Microsoft.AspNetCore.Mvc; + using Swashbuckle.AspNetCore.Filters; + + public partial class BuildingController + { + /// + /// Snapshot voor het gebouw aanvragen. + /// + /// + /// + /// + /// Als de snapshot voor het gebouw aangevraagd is. + /// + [HttpPost("{persistentLocalId}/acties/snapshot")] + [ProducesResponseType(StatusCodes.Status202Accepted)] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status500InternalServerError)] + [SwaggerResponseHeader(StatusCodes.Status202Accepted, "location", "string", "De url van het gebouw.")] + [SwaggerResponseExample(StatusCodes.Status400BadRequest, typeof(BadRequestResponseExamples))] + [SwaggerResponseExample(StatusCodes.Status500InternalServerError, typeof(InternalServerErrorResponseExamples))] + [Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme, Policy = PolicyNames.GeschetstGebouw.DecentraleBijwerker)] + public async Task CreateSnapshot( + [FromServices] BuildingExistsValidator buildingExistsValidator, + [FromRoute] CreateBuildingSnapshotRequest request, + CancellationToken cancellationToken = default) + { + if (!await buildingExistsValidator.Exists(new BuildingPersistentLocalId(request.PersistentLocalId), + cancellationToken)) + { + throw new ApiException(ValidationErrors.Common.BuildingNotFound.Message, StatusCodes.Status404NotFound); + } + + var result = await Mediator.Send( + new CreateBuildingSnapshotSqsRequest + { + Request = request, + Metadata = GetMetadata(), + ProvenanceData = new ProvenanceData(CreateProvenance(Modification.Unknown)), + }, cancellationToken); + + return Accepted(result); + } + } +} diff --git a/src/BuildingRegistry.Api.BackOffice/Handlers/Building/CreateBuildingUnitOsloSnapshotsSqsHandler.cs b/src/BuildingRegistry.Api.BackOffice/Handlers/Building/CreateBuildingOsloSnapshotsSqsHandler.cs similarity index 100% rename from src/BuildingRegistry.Api.BackOffice/Handlers/Building/CreateBuildingUnitOsloSnapshotsSqsHandler.cs rename to src/BuildingRegistry.Api.BackOffice/Handlers/Building/CreateBuildingOsloSnapshotsSqsHandler.cs diff --git a/src/BuildingRegistry.Api.BackOffice/Handlers/Building/CreateBuildingSnapshotSqsHandler.cs b/src/BuildingRegistry.Api.BackOffice/Handlers/Building/CreateBuildingSnapshotSqsHandler.cs new file mode 100644 index 000000000..ac2dc1bdb --- /dev/null +++ b/src/BuildingRegistry.Api.BackOffice/Handlers/Building/CreateBuildingSnapshotSqsHandler.cs @@ -0,0 +1,35 @@ +namespace BuildingRegistry.Api.BackOffice.Handlers.Building +{ + using System.Collections.Generic; + using Abstractions.Building.SqsRequests; + using AllStream; + using Be.Vlaanderen.Basisregisters.Sqs; + using Be.Vlaanderen.Basisregisters.Sqs.Handlers; + using TicketingService.Abstractions; + + public sealed class CreateBuildingSnapshotSqsHandler : SqsHandler + { + public const string Action = "CreateBuildingSnapshot"; + + public CreateBuildingSnapshotSqsHandler( + ISqsQueue sqsQueue, + ITicketing ticketing, + ITicketingUrl ticketingUrl) : base(sqsQueue, ticketing, ticketingUrl) + { } + + protected override string? WithAggregateId(CreateBuildingSnapshotSqsRequest request) + { + return request.Request.PersistentLocalId.ToString(); + } + + protected override IDictionary WithTicketMetadata(string aggregateId, CreateBuildingSnapshotSqsRequest sqsRequest) + { + return new Dictionary + { + { RegistryKey, nameof(BuildingRegistry) }, + { ActionKey, Action }, + { AggregateIdKey, aggregateId } + }; + } + } +} diff --git a/src/BuildingRegistry.Infrastructure/Modules/AggregateSourceModule.cs b/src/BuildingRegistry.Infrastructure/Modules/AggregateSourceModule.cs index c120da3fa..3aaddd458 100644 --- a/src/BuildingRegistry.Infrastructure/Modules/AggregateSourceModule.cs +++ b/src/BuildingRegistry.Infrastructure/Modules/AggregateSourceModule.cs @@ -6,6 +6,7 @@ namespace BuildingRegistry.Infrastructure.Modules using Be.Vlaanderen.Basisregisters.AggregateSource.Snapshotting; using Be.Vlaanderen.Basisregisters.EventHandling; using Be.Vlaanderen.Basisregisters.EventHandling.Autofac; + using Building.Events; using BuildingRegistry.AllStream; using BuildingRegistry.Building; using BuildingRegistry.Infrastructure; @@ -36,10 +37,12 @@ protected override void Load(ContainerBuilder builder) snapshotStrategy = IntervalStrategy.SnapshotEvery(snapshotInterval); } + var anySnapshot = new AnySnapshotStrategy([snapshotStrategy, new AfterEventTypeStrategy(typeof(BuildingSnapshotWasRequested))]); + builder.RegisterSnapshotModule(_configuration); builder - .Register(c => new BuildingFactory(snapshotStrategy)) + .Register(c => new BuildingFactory(anySnapshot)) .As(); builder diff --git a/src/BuildingRegistry/Building/Building.cs b/src/BuildingRegistry/Building/Building.cs index 771b286dc..aeba67365 100644 --- a/src/BuildingRegistry/Building/Building.cs +++ b/src/BuildingRegistry/Building/Building.cs @@ -317,6 +317,11 @@ protected override void BeforeApplyChange(object @event) #region Snapshot + public void RequestSnapshot() + { + ApplyChange(new BuildingSnapshotWasRequested(BuildingPersistentLocalId)); + } + public object TakeSnapshot() { return new BuildingSnapshot( diff --git a/src/BuildingRegistry/Building/BuildingCommandHandlerModule.cs b/src/BuildingRegistry/Building/BuildingCommandHandlerModule.cs index dd1d9fe82..d24913ec2 100755 --- a/src/BuildingRegistry/Building/BuildingCommandHandlerModule.cs +++ b/src/BuildingRegistry/Building/BuildingCommandHandlerModule.cs @@ -24,6 +24,18 @@ public BuildingCommandHandlerModule( IProvenanceFactory provenanceFactory, IBuildingGeometries? buildingGeometries = null) { + For() + .AddSqlStreamStore(getStreamStore, getUnitOfWork, eventMapping, eventSerializer, getSnapshotStore) + .AddEventHash(getUnitOfWork) + .AddProvenance(getUnitOfWork, provenanceFactory) + .Handle(async (message, ct) => + { + var streamId = new BuildingStreamId(message.Command.BuildingPersistentLocalId); + var building = await buildingRepository().GetAsync(streamId, ct); + + building.RequestSnapshot(); + }); + For() .AddSqlStreamStore(getStreamStore, getUnitOfWork, eventMapping, eventSerializer, getSnapshotStore) .AddEventHash(getUnitOfWork) @@ -248,7 +260,7 @@ public BuildingCommandHandlerModule( { var streamId = new BuildingStreamId(message.Command.BuildingPersistentLocalId); var building = await buildingRepository().GetAsync(streamId, ct); - + building.ReaddressAddresses(message.Command.Readdresses); }); } diff --git a/src/BuildingRegistry/Building/Commands/CreateOsloSnapshots.cs b/src/BuildingRegistry/Building/Commands/CreateOsloSnapshots.cs new file mode 100644 index 000000000..ab03983f1 --- /dev/null +++ b/src/BuildingRegistry/Building/Commands/CreateOsloSnapshots.cs @@ -0,0 +1,41 @@ +namespace BuildingRegistry.Building.Commands +{ + using System; + using System.Collections.Generic; + using Be.Vlaanderen.Basisregisters.Generators.Guid; + using Be.Vlaanderen.Basisregisters.GrAr.Provenance; + using Be.Vlaanderen.Basisregisters.Utilities; + + public sealed class CreateSnapshot : IHasCommandProvenance + { + private static readonly Guid Namespace = new Guid("1ca80df2-b1e3-463a-849e-c22eeca84c3f"); + + public BuildingPersistentLocalId BuildingPersistentLocalId { get; } + + public Provenance Provenance { get; } + + public CreateSnapshot( + BuildingPersistentLocalId buildingPersistentLocalId, + Provenance provenance) + { + BuildingPersistentLocalId = buildingPersistentLocalId; + Provenance = provenance; + } + + public Guid CreateCommandId() + => Deterministic.Create(Namespace, $"CreateSnapshot-{ToString()}"); + + public override string? ToString() + => ToStringBuilder.ToString(IdentityFields()); + + private IEnumerable IdentityFields() + { + yield return BuildingPersistentLocalId; + + foreach (var field in Provenance.GetIdentityFields()) + { + yield return field; + } + } + } +} diff --git a/src/BuildingRegistry/Building/Events/BuildingSnapshotWasRequested.cs b/src/BuildingRegistry/Building/Events/BuildingSnapshotWasRequested.cs new file mode 100644 index 000000000..5abd57988 --- /dev/null +++ b/src/BuildingRegistry/Building/Events/BuildingSnapshotWasRequested.cs @@ -0,0 +1,45 @@ +namespace BuildingRegistry.Building.Events +{ + using System.Collections.Generic; + using System.Globalization; + using System.Linq; + using Be.Vlaanderen.Basisregisters.EventHandling; + using Be.Vlaanderen.Basisregisters.GrAr.Common; + using Be.Vlaanderen.Basisregisters.GrAr.Provenance; + using Newtonsoft.Json; + + [HideEvent] + [EventName(EventName)] + [EventDescription("EventStore snapshot voor het gebouw werd aangevraagd.")] + public sealed class BuildingSnapshotWasRequested: IBuildingEvent, IHasBuildingPersistentLocalId + { + public const string EventName = "BuildingSnapshotWasRequested"; // BE CAREFUL CHANGING THIS!! + + [EventPropertyDescription("Objectidentificator van het gebouw.")] + public int BuildingPersistentLocalId { get; } + + [EventPropertyDescription("Metadata bij het event.")] + public ProvenanceData Provenance { get; private set; } + + public BuildingSnapshotWasRequested(BuildingPersistentLocalId buildingPersistentLocalId) + { + BuildingPersistentLocalId = buildingPersistentLocalId; + } + + [JsonConstructor] + private BuildingSnapshotWasRequested(int buildingPersistentLocalId, ProvenanceData provenance) + : this(new BuildingPersistentLocalId(buildingPersistentLocalId)) + => ((ISetProvenance)this).SetProvenance(provenance.ToProvenance()); + + void ISetProvenance.SetProvenance(Provenance provenance) => Provenance = new ProvenanceData(provenance); + + public IEnumerable GetHashFields() + { + var fields = Provenance.GetHashFields().ToList(); + fields.Add(BuildingPersistentLocalId.ToString(CultureInfo.InvariantCulture)); + return fields; + } + + public string GetHash() => this.ToEventHash(EventName); + } +} diff --git a/test/BuildingRegistry.Tests/AggregateTests/SnapshotTests/GivenBuilding.cs b/test/BuildingRegistry.Tests/AggregateTests/SnapshotTests/GivenBuilding.cs index ba179696a..6d1a968fa 100644 --- a/test/BuildingRegistry.Tests/AggregateTests/SnapshotTests/GivenBuilding.cs +++ b/test/BuildingRegistry.Tests/AggregateTests/SnapshotTests/GivenBuilding.cs @@ -116,6 +116,60 @@ public async Task ThenSnapshotWasCreated() }); } + [Fact] + public async Task WithAfterBuildingSnapshotWasRequested_ThenSnapshotWasCreated() + { + Fixture.Register(ISnapshotStrategy () => new AfterEventTypeStrategy(typeof(BuildingSnapshotWasRequested))); + + var provenance = Fixture.Create(); + var buildingWasPlanned = Fixture.Create(); + + var buildingUnitWasPlanned = Fixture.Create(); + + var buildingGeometry = new BuildingGeometry(new ExtendedWkbGeometry(buildingWasPlanned.ExtendedWkbGeometry), + BuildingGeometryMethod.Outlined); + + var expectedEvent = new BuildingSnapshotWasRequested(new BuildingPersistentLocalId(buildingWasPlanned.BuildingPersistentLocalId)); + ((ISetProvenance)expectedEvent).SetProvenance(provenance); + + Assert(new Scenario() + .Given(_streamId, + buildingWasPlanned, + buildingUnitWasPlanned) + .When(new CreateSnapshot(new BuildingPersistentLocalId(buildingWasPlanned.BuildingPersistentLocalId), Fixture.Create())) + .Then( + new Fact(_streamId, expectedEvent))); + + var plannedBuildingUnit = new BuildingUnit(o => { }); + plannedBuildingUnit.Route(buildingUnitWasPlanned); + + var expectedSnapshot = new BuildingSnapshot( + Fixture.Create(), + BuildingStatus.Planned, + buildingGeometry, + false, + buildingUnitWasPlanned.GetHash(), + buildingUnitWasPlanned.Provenance, + new List + { + plannedBuildingUnit, + }, + new List()); + + var snapshotStore = (ISnapshotStore)Container.Resolve(typeof(ISnapshotStore)); + var latestSnapshot = await snapshotStore.FindLatestSnapshotAsync(_streamId, CancellationToken.None); + + latestSnapshot.Should().NotBeNull(); + var snapshot = JsonConvert.DeserializeObject(latestSnapshot!.Data, EventSerializerSettings); + + snapshot.Should().BeEquivalentTo(expectedSnapshot, options => + { + options.Excluding(x => x.Path.EndsWith("LastEventHash")); + options.Excluding(x => x.Type == typeof(Instant)); + return options; + }); + } + [Fact] public async Task WithMigratedBuildingAndUnusedCommonBuildingUnits_ThenSnapshotWasCreated() { diff --git a/test/BuildingRegistry.Tests/AggregateTests/WhenRequestingBuildingSnapshot/GivenBuildingExists.cs b/test/BuildingRegistry.Tests/AggregateTests/WhenRequestingBuildingSnapshot/GivenBuildingExists.cs new file mode 100644 index 000000000..79b13c053 --- /dev/null +++ b/test/BuildingRegistry.Tests/AggregateTests/WhenRequestingBuildingSnapshot/GivenBuildingExists.cs @@ -0,0 +1,35 @@ +namespace BuildingRegistry.Tests.AggregateTests.WhenRequestingBuildingSnapshot +{ + using AutoFixture; + using Be.Vlaanderen.Basisregisters.AggregateSource; + using Be.Vlaanderen.Basisregisters.AggregateSource.Testing; + using Building; + using Building.Commands; + using Building.Events; + using Fixtures; + using Xunit; + using Xunit.Abstractions; + + public class GivenBuildingExists : BuildingRegistryTest + { + public GivenBuildingExists(ITestOutputHelper testOutputHelper) : base(testOutputHelper) + { + Fixture.Customize(new WithFixedBuildingPersistentLocalId()); + } + + [Fact] + public void ThenCreateSnapshotWasRequested() + { + var command = Fixture.Create(); + + Assert(new Scenario() + .Given( + new BuildingStreamId(Fixture.Create()), + Fixture.Create()) + .When(command) + .Then(new Fact( + new BuildingStreamId(command.BuildingPersistentLocalId), + new BuildingSnapshotWasRequested(command.BuildingPersistentLocalId)))); + } + } +} diff --git a/test/BuildingRegistry.Tests/AggregateTests/WhenRequestingBuildingSnapshot/GivenNoBuildingExists.cs b/test/BuildingRegistry.Tests/AggregateTests/WhenRequestingBuildingSnapshot/GivenNoBuildingExists.cs new file mode 100644 index 000000000..1802d7c02 --- /dev/null +++ b/test/BuildingRegistry.Tests/AggregateTests/WhenRequestingBuildingSnapshot/GivenNoBuildingExists.cs @@ -0,0 +1,28 @@ +namespace BuildingRegistry.Tests.AggregateTests.WhenRequestingBuildingSnapshot +{ + using AutoFixture; + using Be.Vlaanderen.Basisregisters.AggregateSource; + using Be.Vlaanderen.Basisregisters.AggregateSource.Testing; + using Building; + using Building.Commands; + using Xunit; + using Xunit.Abstractions; + + public class GivenNoBuildingExists : BuildingRegistryTest + { + public GivenNoBuildingExists(ITestOutputHelper testOutputHelper) : base(testOutputHelper) + { } + + [Fact] + public void ThenThrowsAggregateNotFoundException() + { + var command = Fixture.Create(); + var streamId = new BuildingStreamId(command.BuildingPersistentLocalId); + + Assert(new Scenario() + .GivenNone() + .When(command) + .Throws(new AggregateNotFoundException(streamId, typeof(Building)))); + } + } +} diff --git a/test/BuildingRegistry.Tests/BackOffice/Api/Building/WhenRequestingCreateBuildingSnapshot/GivenRequest.cs b/test/BuildingRegistry.Tests/BackOffice/Api/Building/WhenRequestingCreateBuildingSnapshot/GivenRequest.cs new file mode 100644 index 000000000..e1df6e65a --- /dev/null +++ b/test/BuildingRegistry.Tests/BackOffice/Api/Building/WhenRequestingCreateBuildingSnapshot/GivenRequest.cs @@ -0,0 +1,73 @@ +namespace BuildingRegistry.Tests.BackOffice.Api.Building.WhenRequestingCreateBuildingSnapshot +{ + using System; + using System.Threading; + using System.Threading.Tasks; + using AutoFixture; + using Be.Vlaanderen.Basisregisters.GrAr.Provenance; + using Be.Vlaanderen.Basisregisters.Sqs.Requests; + using BuildingRegistry.Api.BackOffice.Abstractions.Building.Requests; + using BuildingRegistry.Api.BackOffice.Abstractions.Building.SqsRequests; + using BuildingRegistry.Api.BackOffice.Abstractions.Building.Validators; + using BuildingRegistry.Api.BackOffice.Building; + using BuildingRegistry.Building; + using Fixtures; + using FluentAssertions; + using Microsoft.AspNetCore.Mvc; + using Moq; + using NodaTime; + using SqlStreamStore; + using Xunit; + using Xunit.Abstractions; + + public class GivenRequest : BackOfficeApiTest + { + private readonly BuildingController _controller; + private readonly Mock _streamStore; + + public GivenRequest(ITestOutputHelper testOutputHelper) : base(testOutputHelper) + { + Fixture.Customize(new WithFixedBuildingPersistentLocalId()); + + _streamStore = new Mock(); + _controller = CreateBuildingControllerWithUser(); + } + + [Fact] + public async Task ThenTicketLocationIsReturned() + { + var buildingPersistentLocalId = 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)); + + _streamStore.SetStreamFound(); + + var request = new CreateBuildingSnapshotRequest + { + PersistentLocalId = (int)buildingPersistentLocalId + }; + + var result = (AcceptedResult)await _controller.CreateSnapshot(new BuildingExistsValidator(_streamStore.Object), request, CancellationToken.None); + + 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.BuildingRegistry + && sqsRequest.ProvenanceData.Modification == Modification.Unknown + ), + CancellationToken.None)); + } + } +} diff --git a/test/BuildingRegistry.Tests/ProjectionsHandlesEventsTests.cs b/test/BuildingRegistry.Tests/ProjectionsHandlesEventsTests.cs index 78d229bc4..4f5d76bcf 100644 --- a/test/BuildingRegistry.Tests/ProjectionsHandlesEventsTests.cs +++ b/test/BuildingRegistry.Tests/ProjectionsHandlesEventsTests.cs @@ -45,7 +45,7 @@ namespace BuildingRegistry.Tests public sealed class ProjectionsHandlesEventsTests { - private readonly IEnumerable _eventsToExclude = [typeof(BuildingSnapshot)]; + private readonly IEnumerable _eventsToExclude = [typeof(BuildingSnapshot), typeof(BuildingSnapshotWasRequested)]; private readonly IList _eventTypes; public ProjectionsHandlesEventsTests()