diff --git a/BuildingRegistry.sln b/BuildingRegistry.sln index ae38447e1..0264949b5 100755 --- a/BuildingRegistry.sln +++ b/BuildingRegistry.sln @@ -78,6 +78,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BuildingRegistry.Consumer.R EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BuildingRegistry.Snapshot.Verifier", "src\BuildingRegistry.Snapshot.Verifier\BuildingRegistry.Snapshot.Verifier.csproj", "{6F18B5C9-0E57-4352-B320-7976094B0738}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BuildingRegistry.Projections.Integration", "src\BuildingRegistry.Projections.Integration\BuildingRegistry.Projections.Integration.csproj", "{2CD597C6-43AB-4AB8-AF3A-5AC186AB92A3}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -184,6 +186,10 @@ Global {6F18B5C9-0E57-4352-B320-7976094B0738}.Debug|Any CPU.Build.0 = Debug|Any CPU {6F18B5C9-0E57-4352-B320-7976094B0738}.Release|Any CPU.ActiveCfg = Release|Any CPU {6F18B5C9-0E57-4352-B320-7976094B0738}.Release|Any CPU.Build.0 = Release|Any CPU + {2CD597C6-43AB-4AB8-AF3A-5AC186AB92A3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2CD597C6-43AB-4AB8-AF3A-5AC186AB92A3}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2CD597C6-43AB-4AB8-AF3A-5AC186AB92A3}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2CD597C6-43AB-4AB8-AF3A-5AC186AB92A3}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -214,6 +220,7 @@ Global {72C98F9C-C576-4DFC-BAD6-4249E2AB64F1} = {1A034126-30E6-416B-BB2E-F5786E07B178} {7ADEA3CC-89A7-40B2-936A-3DC0BBF755B5} = {24B47154-C5F0-4356-B059-49A1E72C38A6} {6F18B5C9-0E57-4352-B320-7976094B0738} = {24B47154-C5F0-4356-B059-49A1E72C38A6} + {2CD597C6-43AB-4AB8-AF3A-5AC186AB92A3} = {24B47154-C5F0-4356-B059-49A1E72C38A6} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {65FA30D5-DDFC-40EC-9BC9-3BF93D1B430F} diff --git a/paket.dependencies b/paket.dependencies index b3397ffbf..f3f84a267 100644 --- a/paket.dependencies +++ b/paket.dependencies @@ -19,6 +19,7 @@ nuget MediatR.Contracts 2.0.1 nuget MediatR.Extensions.Microsoft.DependencyInjection 10.0.1 nuget Microsoft.EntityFrameworkCore.SqlServer.NetTopologySuite 6.0.3 +nuget Npgsql.EntityFrameworkCore.PostgreSQL.NetTopologySuite 6.0.3 nuget NetTopologySuite 2.4.0 nuget NodaTime 3.1.6 diff --git a/paket.lock b/paket.lock index c5d261621..7ba6de81e 100644 --- a/paket.lock +++ b/paket.lock @@ -1031,6 +1031,8 @@ NUGET Microsoft.NETCore.Platforms (>= 1.1) NetTopologySuite (2.4) System.Memory (>= 4.5.3) + NetTopologySuite.IO.PostGis (2.1) - restriction: || (&& (== net472) (>= net6.0)) (== net6.0) + NetTopologySuite (>= 2.0 < 3.0.0-A) NetTopologySuite.IO.SqlServerBytes (2.1) - restriction: || (&& (== net472) (>= net6.0)) (== net6.0) NetTopologySuite (>= 2.0 < 3.0.0-A) Newtonsoft.Json (13.0.1) @@ -1073,6 +1075,12 @@ NUGET NETStandard.Library (>= 1.6.1) - restriction: || (&& (== net472) (< net46)) (== net6.0) Npgsql (>= 3.2.2) Npgsql.EntityFrameworkCore.PostgreSQL (>= 2.0.0-preview1) + Npgsql.EntityFrameworkCore.PostgreSQL.NetTopologySuite (6.0.3) + Npgsql.EntityFrameworkCore.PostgreSQL (>= 6.0.3) - restriction: || (&& (== net472) (>= net6.0)) (== net6.0) + Npgsql.NetTopologySuite (>= 6.0.3) - restriction: || (&& (== net472) (>= net6.0)) (== net6.0) + Npgsql.NetTopologySuite (6.0.3) - restriction: || (&& (== net472) (>= net6.0)) (== net6.0) + NetTopologySuite.IO.PostGis (>= 2.1) + Npgsql (>= 6.0.3) NSwag.CodeGeneration (13.15.10) - restriction: || (&& (== net472) (>= net6.0)) (== net6.0) Newtonsoft.Json (>= 9.0.1) NJsonSchema (>= 10.6.10) diff --git a/src/BuildingRegistry.Infrastructure/Schema.cs b/src/BuildingRegistry.Infrastructure/Schema.cs index a10d97890..b99cbf7d8 100644 --- a/src/BuildingRegistry.Infrastructure/Schema.cs +++ b/src/BuildingRegistry.Infrastructure/Schema.cs @@ -17,6 +17,7 @@ public static class Schema public const string BackOfficeProjections = "BuildingRegistryBackOfficeProjections"; public const string Producer = "BuildingRegistryProducer"; public const string ProducerSnapshotOslo = "BuildingRegistryProducerSnapshotOslo"; + public const string Integration = "integration_building"; } public static class MigrationTables @@ -35,5 +36,6 @@ public static class MigrationTables public const string MigratorProjection = "__EFMigrationsHistoryMigrationProjection"; public const string Producer = "__EFMigrationsHistoryProducer"; public const string ProducerSnapshotOslo = "__EFMigrationsHistoryProducerSnapshotOslo"; + public const string Integration = "__EFMigrationsHistory"; } } diff --git a/src/BuildingRegistry.Projections.Integration/Building/LatestItem/BuildingLatestItem.cs b/src/BuildingRegistry.Projections.Integration/Building/LatestItem/BuildingLatestItem.cs new file mode 100644 index 000000000..bc949f00a --- /dev/null +++ b/src/BuildingRegistry.Projections.Integration/Building/LatestItem/BuildingLatestItem.cs @@ -0,0 +1,76 @@ +namespace BuildingRegistry.Projections.Integration.Building.LatestItem +{ + using System; + using Be.Vlaanderen.Basisregisters.GrAr.Common; + using Be.Vlaanderen.Basisregisters.Utilities; + using BuildingRegistry.Infrastructure; + using Microsoft.EntityFrameworkCore; + using Microsoft.EntityFrameworkCore.Metadata.Builders; + using NetTopologySuite.Geometries; + using NodaTime; + + public sealed class BuildingLatestItem + { + public const string VersionTimestampBackingPropertyName = nameof(VersionTimestampAsDateTimeOffset); + + public int BuildingPersistentLocalId { get; set; } + public string Status { get; set; } + public string OsloStatus { get; set; } + public string GeometryMethod { get; set; } + public string OsloGeometryMethod { get; set; } + public Geometry Geometry { get; set; } + public string? NisCode { get; set; } + public bool IsRemoved { get; set; } + + public string PuriId { get; set; } + public string Namespace { get; set; } + + public string VersionAsString { get; set; } + private DateTimeOffset VersionTimestampAsDateTimeOffset { get; set; } + public Instant VersionTimestamp + { + get => Instant.FromDateTimeOffset(VersionTimestampAsDateTimeOffset); + set + { + VersionTimestampAsDateTimeOffset = value.ToDateTimeOffset(); + VersionAsString = new Rfc3339SerializableDateTimeOffset(value.ToBelgianDateTimeOffset()).ToString(); + } + } + + public BuildingLatestItem() + { } + } + + public sealed class BuildingLatestItemConfiguration : IEntityTypeConfiguration + { + public void Configure(EntityTypeBuilder builder) + { + const string tableName = "building_latest_items"; + + builder + .ToTable(tableName, Schema.Integration) // to schema per type + .HasKey(x => x.BuildingPersistentLocalId); + + builder.Property(x => x.BuildingPersistentLocalId).HasColumnName("building_persistent_local_id"); + builder.Property(x => x.Status).HasColumnName("status"); + builder.Property(x => x.OsloStatus).HasColumnName("oslo_status"); + builder.Property(x => x.GeometryMethod).HasColumnName("geometry_method"); + builder.Property(x => x.OsloGeometryMethod).HasColumnName("oslo_geometry_method"); + builder.Property(x => x.Geometry).HasColumnName("geometry"); + builder.Property(x => x.NisCode).HasColumnName("nis_code"); + builder.Property(x => x.IsRemoved).HasColumnName("is_removed"); + builder.Property(x => x.PuriId).HasColumnName("puri_id"); + builder.Property(x => x.Namespace).HasColumnName("namespace"); + builder.Property(x => x.VersionAsString).HasColumnName("version_as_string"); + builder.Property(BuildingLatestItem.VersionTimestampBackingPropertyName).HasColumnName("version_timestamp"); + + builder.Ignore(x => x.VersionTimestamp); + + builder.HasIndex(x => x.Status); + builder.HasIndex(x => x.OsloStatus); + builder.HasIndex(x => x.IsRemoved); + builder.HasIndex(x => x.NisCode); + builder.HasIndex(x => x.Geometry).HasMethod("GIST"); + } + } +} diff --git a/src/BuildingRegistry.Projections.Integration/Building/LatestItem/BuildingLatestItemExtensions.cs b/src/BuildingRegistry.Projections.Integration/Building/LatestItem/BuildingLatestItemExtensions.cs new file mode 100644 index 000000000..c5c086609 --- /dev/null +++ b/src/BuildingRegistry.Projections.Integration/Building/LatestItem/BuildingLatestItemExtensions.cs @@ -0,0 +1,30 @@ +namespace BuildingRegistry.Projections.Integration.Building.LatestItem +{ + using System; + using System.Threading; + using System.Threading.Tasks; + using Be.Vlaanderen.Basisregisters.ProjectionHandling.Connector; + + public static class BuildingLatestItemExtensions + { + public static async Task FindAndUpdateBuilding(this IntegrationContext context, + int buildingPersistentLocalId, + Action updateFunc, + CancellationToken ct) + { + var building = await context + .BuildingLatestItems + .FindAsync(buildingPersistentLocalId, cancellationToken: ct); + + if (building == null) + throw DatabaseItemNotFound(buildingPersistentLocalId); + + updateFunc(building); + + return building; + } + + private static ProjectionItemNotFoundException DatabaseItemNotFound(int buildingPersistentLocalId) + => new ProjectionItemNotFoundException(buildingPersistentLocalId.ToString()); + } +} diff --git a/src/BuildingRegistry.Projections.Integration/Building/LatestItem/BuildingLatestItemProjections.cs b/src/BuildingRegistry.Projections.Integration/Building/LatestItem/BuildingLatestItemProjections.cs new file mode 100644 index 000000000..661019042 --- /dev/null +++ b/src/BuildingRegistry.Projections.Integration/Building/LatestItem/BuildingLatestItemProjections.cs @@ -0,0 +1,380 @@ +namespace BuildingRegistry.Projections.Integration.Building.LatestItem +{ + using Be.Vlaanderen.Basisregisters.GrAr.Provenance; + using Be.Vlaanderen.Basisregisters.ProjectionHandling.Connector; + using Be.Vlaanderen.Basisregisters.ProjectionHandling.SqlStreamStore; + using Be.Vlaanderen.Basisregisters.Utilities.HexByteConvertor; + using BuildingRegistry.Building; + using BuildingRegistry.Building.Events; + using Converters; + using Infrastructure; + using Microsoft.Extensions.Options; + + [ConnectedProjectionName("Integratie gebouw latest item")] + [ConnectedProjectionDescription("Projectie die de laatste gebouw data voor de integratie database bijhoudt.")] + public sealed class BuildingLatestItemProjections : ConnectedProjection + { + public BuildingLatestItemProjections(IOptions options) + { + var wkbReader = WKBReaderFactory.Create(); + + When>(async (context, message, ct) => + { + var geometryAsBinary = message.Message.ExtendedWkbGeometry.ToByteArray(); + var sysGeometry = wkbReader.Read(geometryAsBinary); + + var nisCode = await context.FindMostIntersectingNisCodeBy(sysGeometry, ct); + + var building = new BuildingLatestItem + { + BuildingPersistentLocalId = message.Message.BuildingPersistentLocalId, + Status = BuildingStatus.Parse(message.Message.BuildingStatus).Map(), + OsloStatus = message.Message.BuildingStatus, + GeometryMethod = BuildingGeometryMethod.Parse(message.Message.GeometryMethod).Map(), + OsloGeometryMethod = message.Message.GeometryMethod, + Geometry = sysGeometry, + NisCode = nisCode, + IsRemoved = message.Message.IsRemoved, + VersionTimestamp = message.Message.Provenance.Timestamp, + Namespace = options.Value.BuildingNamespace, + PuriId = $"{options.Value.BuildingNamespace}/{message.Message.BuildingPersistentLocalId}" + }; + + await context + .BuildingLatestItems + .AddAsync(building, ct); + }); + + When>(async (context, message, ct) => + { + var geometryAsBinary = message.Message.ExtendedWkbGeometry.ToByteArray(); + var sysGeometry = wkbReader.Read(geometryAsBinary); + + var nisCode = await context.FindMostIntersectingNisCodeBy(sysGeometry, ct); + + var building = new BuildingLatestItem + { + BuildingPersistentLocalId = message.Message.BuildingPersistentLocalId, + Status = BuildingStatus.Planned.Map(), + OsloStatus = BuildingStatus.Planned.Value, + GeometryMethod = BuildingGeometryMethod.Outlined.Map(), + OsloGeometryMethod = BuildingGeometryMethod.Outlined.Value, + Geometry = sysGeometry, + NisCode = nisCode, + IsRemoved = false, + VersionTimestamp = message.Message.Provenance.Timestamp, + Namespace = options.Value.BuildingNamespace, + PuriId = $"{options.Value.BuildingNamespace}/{message.Message.BuildingPersistentLocalId}" + }; + + await context + .BuildingLatestItems + .AddAsync(building, ct); + }); + + When>(async (context, message, ct) => + { + var geometryAsBinary = message.Message.ExtendedWkbGeometry.ToByteArray(); + var sysGeometry = wkbReader.Read(geometryAsBinary); + + var nisCode = await context.FindMostIntersectingNisCodeBy(sysGeometry, ct); + + var building = new BuildingLatestItem + { + BuildingPersistentLocalId = message.Message.BuildingPersistentLocalId, + Status = BuildingStatus.Realized.Map(), + OsloStatus = BuildingStatus.Realized.Value, + GeometryMethod = BuildingGeometryMethod.MeasuredByGrb.Map(), + OsloGeometryMethod = BuildingGeometryMethod.MeasuredByGrb.Value, + Geometry = sysGeometry, + NisCode = nisCode, + IsRemoved = false, + VersionTimestamp = message.Message.Provenance.Timestamp, + Namespace = options.Value.BuildingNamespace, + PuriId = $"{options.Value.BuildingNamespace}/{message.Message.BuildingPersistentLocalId}" + }; + + await context + .BuildingLatestItems + .AddAsync(building, ct); + }); + + When>(async (context, message, ct) => + { + var geometryAsBinary = message.Message.ExtendedWkbGeometryBuilding.ToByteArray(); + var sysGeometry = wkbReader.Read(geometryAsBinary); + + var nisCode = await context.FindMostIntersectingNisCodeBy(sysGeometry, ct); + + await context.FindAndUpdateBuilding( + message.Message.BuildingPersistentLocalId, + building => + { + building.Geometry = sysGeometry; + building.GeometryMethod = BuildingGeometryMethod.Outlined.Map(); + building.OsloGeometryMethod = BuildingGeometryMethod.Outlined.Value; + building.NisCode = nisCode; + UpdateVersionTimestamp(building, message.Message); + }, + ct); + }); + + When>(async (context, message, ct) => + { + var geometryAsBinary = message.Message.ExtendedWkbGeometryBuilding.ToByteArray(); + var sysGeometry = wkbReader.Read(geometryAsBinary); + + var nisCode = await context.FindMostIntersectingNisCodeBy(sysGeometry, ct); + + await context.FindAndUpdateBuilding( + message.Message.BuildingPersistentLocalId, + building => + { + building.Geometry = sysGeometry; + building.GeometryMethod = BuildingGeometryMethod.MeasuredByGrb.Map(); + building.OsloGeometryMethod = BuildingGeometryMethod.MeasuredByGrb.Value; + building.NisCode = nisCode; + UpdateVersionTimestamp(building, message.Message); + }, + ct); + }); + + When>(async (context, message, ct) => + { + await context.FindAndUpdateBuilding( + message.Message.BuildingPersistentLocalId, + building => + { + building.Status = BuildingStatus.UnderConstruction.Map(); + building.OsloStatus = BuildingStatus.UnderConstruction.Value; + UpdateVersionTimestamp(building, message.Message); + }, + ct); + }); + + When>(async (context, message, ct) => + { + await context.FindAndUpdateBuilding( + message.Message.BuildingPersistentLocalId, + building => + { + building.Status = BuildingStatus.Planned.Map(); + building.OsloStatus = BuildingStatus.Planned.Value; + UpdateVersionTimestamp(building, message.Message); + }, + ct); + }); + + When>(async (context, message, ct) => + { + await context.FindAndUpdateBuilding( + message.Message.BuildingPersistentLocalId, + building => + { + building.Status = BuildingStatus.Realized.Map(); + building.OsloStatus = BuildingStatus.Realized.Value; + UpdateVersionTimestamp(building, message.Message); + }, + ct); + }); + + When>(async (context, message, ct) => + { + await context.FindAndUpdateBuilding( + message.Message.BuildingPersistentLocalId, + building => + { + building.Status = BuildingStatus.UnderConstruction.Map(); + building.OsloStatus = BuildingStatus.UnderConstruction.Value; + UpdateVersionTimestamp(building, message.Message); + }, + ct); + }); + + When>(async (context, message, ct) => + { + await context.FindAndUpdateBuilding( + message.Message.BuildingPersistentLocalId, + building => + { + building.Status = BuildingStatus.NotRealized.Map(); + building.OsloStatus = BuildingStatus.NotRealized.Value; + UpdateVersionTimestamp(building, message.Message); + }, + ct); + }); + + When>(async (context, message, ct) => + { + await context.FindAndUpdateBuilding( + message.Message.BuildingPersistentLocalId, + building => + { + building.Status = BuildingStatus.Planned.Map(); + building.OsloStatus = BuildingStatus.Planned.Value; + UpdateVersionTimestamp(building, message.Message); + }, + ct); + }); + + When>(async (context, message, ct) => + { + var geometryAsBinary = message.Message.ExtendedWkbGeometryBuilding.ToByteArray(); + var sysGeometry = wkbReader.Read(geometryAsBinary); + + var nisCode = await context.FindMostIntersectingNisCodeBy(sysGeometry, ct); + + await context.FindAndUpdateBuilding( + message.Message.BuildingPersistentLocalId, + building => + { + building.Geometry = sysGeometry; + building.GeometryMethod = BuildingGeometryMethod.MeasuredByGrb.Map(); + building.OsloGeometryMethod = BuildingGeometryMethod.MeasuredByGrb.Value; + building.NisCode = nisCode; + UpdateVersionTimestamp(building, message.Message); + }, + ct); + }); + + When>(async (context, message, ct) => + { + var geometryAsBinary = message.Message.ExtendedWkbGeometryBuilding.ToByteArray(); + var sysGeometry = wkbReader.Read(geometryAsBinary); + + var nisCode = await context.FindMostIntersectingNisCodeBy(sysGeometry, ct); + + await context.FindAndUpdateBuilding( + message.Message.BuildingPersistentLocalId, + building => + { + building.Geometry = sysGeometry; + building.GeometryMethod = BuildingGeometryMethod.MeasuredByGrb.Map(); + building.OsloGeometryMethod = BuildingGeometryMethod.MeasuredByGrb.Value; + building.NisCode = nisCode; + UpdateVersionTimestamp(building, message.Message); + }, + ct); + }); + + When>(async (context, message, ct) => + { + await context.FindAndUpdateBuilding( + message.Message.BuildingPersistentLocalId, + building => + { + building.Status = BuildingStatus.Retired.Map(); + building.OsloStatus = BuildingStatus.Retired.Value; + UpdateVersionTimestamp(building, message.Message); + }, + ct); + }); + + When>(async (context, message, ct) => + { + await context.FindAndUpdateBuilding( + message.Message.BuildingPersistentLocalId, + building => + { + building.IsRemoved = true; + UpdateVersionTimestamp(building, message.Message); + }, + ct); + }); + + When>(async (context, message, ct) => + { + await context.FindAndUpdateBuilding( + message.Message.BuildingPersistentLocalId, + building => { UpdateVersionTimestamp(building, message.Message); }, + ct); + }); + + When>(async (context, message, ct) => + { + await context.FindAndUpdateBuilding( + message.Message.BuildingPersistentLocalId, + building => { UpdateVersionTimestamp(building, message.Message); }, + ct); + }); + + When>(async (context, message, ct) => + { + await context.FindAndUpdateBuilding( + message.Message.BuildingPersistentLocalId, + building => + { + UpdateVersionTimestamp(building, message.Message); + }, + ct); + }); + + When>(async (context, message, ct) => + { + await context.FindAndUpdateBuilding( + message.Message.BuildingPersistentLocalId, + building => { UpdateVersionTimestamp(building, message.Message); }, + ct); + }); + + When>(async (context, message, ct) => + { + var geometryAsBinary = message.Message.ExtendedWkbGeometry.ToByteArray(); + var sysGeometry = wkbReader.Read(geometryAsBinary); + + var nisCode = await context.FindMostIntersectingNisCodeBy(sysGeometry, ct); + + var building = new BuildingLatestItem + { + BuildingPersistentLocalId = message.Message.BuildingPersistentLocalId, + Status = BuildingStatus.Realized.Map(), + OsloStatus = BuildingStatus.Realized.Value, + GeometryMethod = BuildingGeometryMethod.MeasuredByGrb.Map(), + OsloGeometryMethod = BuildingGeometryMethod.MeasuredByGrb.Value, + Geometry = sysGeometry, + NisCode = nisCode, + IsRemoved = false, + VersionTimestamp = message.Message.Provenance.Timestamp, + Namespace = options.Value.BuildingNamespace, + PuriId = $"{options.Value.BuildingNamespace}/{message.Message.BuildingPersistentLocalId}" + }; + + await context + .BuildingLatestItems + .AddAsync(building, ct); + }); + + When>(async (context, message, ct) => + { + await context.FindAndUpdateBuilding( + message.Message.BuildingPersistentLocalId, + building => { UpdateVersionTimestamp(building, message.Message); }, + ct); + }); + + When>(async (context, message, ct) => + { + await context.FindAndUpdateBuilding( + message.Message.BuildingPersistentLocalId, + building => { UpdateVersionTimestamp(building, message.Message); }, + ct); + }); + + When>(async (context, message, ct) => + { + await context.FindAndUpdateBuilding( + message.Message.BuildingPersistentLocalId, + building => + { + building.Status = BuildingStatus.Retired.Map(); + building.OsloStatus = BuildingStatus.Retired.Value; + UpdateVersionTimestamp(building, message.Message); + }, + ct); + }); + } + + private static void UpdateVersionTimestamp(BuildingLatestItem building, IHasProvenance message) + => building.VersionTimestamp = message.Provenance.Timestamp; + } +} diff --git a/src/BuildingRegistry.Projections.Integration/Building/LatestItem/MunicipalityGeometry.cs b/src/BuildingRegistry.Projections.Integration/Building/LatestItem/MunicipalityGeometry.cs new file mode 100644 index 000000000..c9a6d57fc --- /dev/null +++ b/src/BuildingRegistry.Projections.Integration/Building/LatestItem/MunicipalityGeometry.cs @@ -0,0 +1,33 @@ +namespace BuildingRegistry.Projections.Integration.Building.LatestItem +{ + using BuildingRegistry.Infrastructure; + using Microsoft.EntityFrameworkCore; + using Microsoft.EntityFrameworkCore.Metadata.Builders; + using NetTopologySuite.Geometries; + + public sealed class MunicipalityGeometry + { + public string NisCode { get; set; } + public Geometry Geometry { get; set; } + + public MunicipalityGeometry() + { } + } + + public sealed class MunicipalityGeometryConfiguration : IEntityTypeConfiguration + { + public void Configure(EntityTypeBuilder builder) + { + const string schema = "integration_municipality"; + const string viewName = "municipality_geometries"; + + builder.Property(x => x.NisCode).HasColumnName("nis_code"); + builder.Property(x => x.Geometry).HasColumnName("geometry"); + + builder + .ToView(viewName, schema) // It's actually a table, but to not create EF migrations, we configure it as a view without a key. + .HasNoKey() + .ToSqlQuery($"SELECT nis_code, geometry FROM {schema}.{viewName}"); + } + } +} diff --git a/src/BuildingRegistry.Projections.Integration/BuildingRegistry.Projections.Integration.csproj b/src/BuildingRegistry.Projections.Integration/BuildingRegistry.Projections.Integration.csproj new file mode 100644 index 000000000..4d52b65a9 --- /dev/null +++ b/src/BuildingRegistry.Projections.Integration/BuildingRegistry.Projections.Integration.csproj @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/src/BuildingRegistry.Projections.Integration/BuildingUnit/LatestItem/BuildingUnitAddress.cs b/src/BuildingRegistry.Projections.Integration/BuildingUnit/LatestItem/BuildingUnitAddress.cs new file mode 100644 index 000000000..6c9542141 --- /dev/null +++ b/src/BuildingRegistry.Projections.Integration/BuildingUnit/LatestItem/BuildingUnitAddress.cs @@ -0,0 +1,33 @@ +namespace BuildingRegistry.Projections.Integration.BuildingUnit.LatestItem +{ + using BuildingRegistry.Infrastructure; + using Microsoft.EntityFrameworkCore; + using Microsoft.EntityFrameworkCore.Metadata.Builders; + + public sealed class BuildingUnitAddress + { + public int BuildingUnitPersistentLocalId { get; set; } + public int AddressPersistentLocalId { get; set; } + + public BuildingUnitAddress() + { } + } + + public sealed class BuildingUnitAddressConfiguration : IEntityTypeConfiguration + { + public void Configure(EntityTypeBuilder builder) + { + const string tableName = "building_unit_addresses"; + + builder + .ToTable(tableName, Schema.Integration) // to schema per type + .HasKey(x => new { x.BuildingUnitPersistentLocalId, x.AddressPersistentLocalId }); + + builder.Property(x => x.BuildingUnitPersistentLocalId).HasColumnName("building_unit_persistent_local_id"); + builder.Property(x => x.AddressPersistentLocalId).HasColumnName("address_persistent_local_id"); + + builder.HasIndex(x => x.BuildingUnitPersistentLocalId); + builder.HasIndex(x => x.AddressPersistentLocalId); + } + } +} diff --git a/src/BuildingRegistry.Projections.Integration/BuildingUnit/LatestItem/BuildingUnitLatestItem.cs b/src/BuildingRegistry.Projections.Integration/BuildingUnit/LatestItem/BuildingUnitLatestItem.cs new file mode 100644 index 000000000..46deeb6b4 --- /dev/null +++ b/src/BuildingRegistry.Projections.Integration/BuildingUnit/LatestItem/BuildingUnitLatestItem.cs @@ -0,0 +1,82 @@ +namespace BuildingRegistry.Projections.Integration.BuildingUnit.LatestItem +{ + using System; + using Be.Vlaanderen.Basisregisters.GrAr.Common; + using Be.Vlaanderen.Basisregisters.Utilities; + using BuildingRegistry.Infrastructure; + using Microsoft.EntityFrameworkCore; + using Microsoft.EntityFrameworkCore.Metadata.Builders; + using NetTopologySuite.Geometries; + using NodaTime; + + public sealed class BuildingUnitLatestItem + { + public const string VersionTimestampBackingPropertyName = nameof(VersionTimestampAsDateTimeOffset); + + public int BuildingUnitPersistentLocalId { get; set; } + public int BuildingPersistentLocalId { get; set; } + public string Status { get; set; } + public string OsloStatus { get; set; } + public string Function { get; set; } + public string OsloFunction { get; set; } + public string GeometryMethod { get; set; } + public string OsloGeometryMethod { get; set; } + public Geometry Geometry { get; set; } + public bool HasDeviation { get; set; } + public bool IsRemoved { get; set; } + + public string PuriId { get; set; } + public string Namespace { get; set; } + + public string VersionAsString { get; set; } + private DateTimeOffset VersionTimestampAsDateTimeOffset { get; set; } + public Instant VersionTimestamp + { + get => Instant.FromDateTimeOffset(VersionTimestampAsDateTimeOffset); + set + { + VersionTimestampAsDateTimeOffset = value.ToDateTimeOffset(); + VersionAsString = new Rfc3339SerializableDateTimeOffset(value.ToBelgianDateTimeOffset()).ToString(); + } + } + + public BuildingUnitLatestItem() + { } + } + + public sealed class BuildingUnitLatestItemConfiguration : IEntityTypeConfiguration + { + public void Configure(EntityTypeBuilder builder) + { + const string tableName = "building_unit_latest_items"; + + builder + .ToTable(tableName, Schema.Integration) // to schema per type + .HasKey(x => x.BuildingUnitPersistentLocalId); + + builder.Property(x => x.BuildingUnitPersistentLocalId).HasColumnName("building_unit_persistent_local_id"); + builder.Property(x => x.BuildingPersistentLocalId).HasColumnName("building_persistent_local_id"); + builder.Property(x => x.Status).HasColumnName("status"); + builder.Property(x => x.OsloStatus).HasColumnName("oslo_status"); + builder.Property(x => x.Function).HasColumnName("function"); + builder.Property(x => x.OsloFunction).HasColumnName("oslo_function"); + builder.Property(x => x.GeometryMethod).HasColumnName("geometry_method"); + builder.Property(x => x.OsloGeometryMethod).HasColumnName("oslo_geometry_method"); + builder.Property(x => x.Geometry).HasColumnName("geometry"); + builder.Property(x => x.HasDeviation).HasColumnName("has_deviation"); + builder.Property(x => x.IsRemoved).HasColumnName("is_removed"); + builder.Property(x => x.PuriId).HasColumnName("puri_id"); + builder.Property(x => x.Namespace).HasColumnName("namespace"); + builder.Property(x => x.VersionAsString).HasColumnName("version_as_string"); + builder.Property(BuildingUnitLatestItem.VersionTimestampBackingPropertyName).HasColumnName("version_timestamp"); + + builder.Ignore(x => x.VersionTimestamp); + + builder.HasIndex(x => x.BuildingPersistentLocalId); + builder.HasIndex(x => x.Status); + builder.HasIndex(x => x.OsloStatus); + builder.HasIndex(x => x.IsRemoved); + builder.HasIndex(x => x.Geometry).HasMethod("GIST"); + } + } +} diff --git a/src/BuildingRegistry.Projections.Integration/BuildingUnit/LatestItem/BuildingUnitLatestItemExtensions.cs b/src/BuildingRegistry.Projections.Integration/BuildingUnit/LatestItem/BuildingUnitLatestItemExtensions.cs new file mode 100644 index 000000000..58a84a06f --- /dev/null +++ b/src/BuildingRegistry.Projections.Integration/BuildingUnit/LatestItem/BuildingUnitLatestItemExtensions.cs @@ -0,0 +1,62 @@ +namespace BuildingRegistry.Projections.Integration.BuildingUnit.LatestItem +{ + using System; + using System.Threading; + using System.Threading.Tasks; + using Be.Vlaanderen.Basisregisters.ProjectionHandling.Connector; + + public static class BuildingUnitLatestItemExtensions + { + public static async Task FindAndUpdateBuildingUnit(this IntegrationContext context, + int buildingUnitPersistentLocalId, + Func updateFunc, + CancellationToken ct) + { + var buildingUnit = await context + .BuildingUnitLatestItems + .FindAsync(buildingUnitPersistentLocalId, cancellationToken: ct); + + if (buildingUnit == null) + throw DatabaseItemNotFound(buildingUnitPersistentLocalId); + + await updateFunc(buildingUnit); + + return buildingUnit; + } + + public static async Task AddIdempotentBuildingUnitAddress(this IntegrationContext context, + BuildingUnitLatestItem buildingUnit, + int addressPersistentLocalId, + CancellationToken ct) + { + var buildingUnitAddress = await context.BuildingUnitAddresses.FindAsync( + new object[] { buildingUnit.BuildingUnitPersistentLocalId, addressPersistentLocalId }, ct); + + if (buildingUnitAddress is null) + { + context.BuildingUnitAddresses.Add(new BuildingUnitAddress + { + BuildingUnitPersistentLocalId = buildingUnit.BuildingUnitPersistentLocalId, + AddressPersistentLocalId = addressPersistentLocalId + }); + } + } + + public static async Task RemoveIdempotentBuildingUnitAddress(this IntegrationContext context, + BuildingUnitLatestItem buildingUnit, + int addressPersistentLocalId, + CancellationToken ct) + { + var buildingUnitAddress = await context.BuildingUnitAddresses.FindAsync( + new object[] { buildingUnit.BuildingUnitPersistentLocalId, addressPersistentLocalId }, ct); + + if (buildingUnitAddress is not null) + { + context.BuildingUnitAddresses.Remove(buildingUnitAddress); + } + } + + private static ProjectionItemNotFoundException DatabaseItemNotFound(int buildingUnitPersistentLocalId) + => new ProjectionItemNotFoundException(buildingUnitPersistentLocalId.ToString()); + } +} diff --git a/src/BuildingRegistry.Projections.Integration/BuildingUnit/LatestItem/BuildingUnitLatestItemProjections.cs b/src/BuildingRegistry.Projections.Integration/BuildingUnit/LatestItem/BuildingUnitLatestItemProjections.cs new file mode 100644 index 000000000..68217ecf7 --- /dev/null +++ b/src/BuildingRegistry.Projections.Integration/BuildingUnit/LatestItem/BuildingUnitLatestItemProjections.cs @@ -0,0 +1,613 @@ +namespace BuildingRegistry.Projections.Integration.BuildingUnit.LatestItem +{ + using System.Linq; + using System.Threading.Tasks; + using Be.Vlaanderen.Basisregisters.GrAr.Provenance; + using Be.Vlaanderen.Basisregisters.ProjectionHandling.Connector; + using Be.Vlaanderen.Basisregisters.ProjectionHandling.SqlStreamStore; + using Be.Vlaanderen.Basisregisters.Utilities.HexByteConvertor; + using BuildingRegistry.Building; + using BuildingRegistry.Building.Events; + using Converters; + using Infrastructure; + using Microsoft.Extensions.Options; + + [ConnectedProjectionName("Integratie gebouweenheid latest item")] + [ConnectedProjectionDescription("Projectie die de laatste gebouweenheid data voor de integratie database bijhoudt.")] + public sealed class BuildingUnitLatestItemProjections : ConnectedProjection + { + public BuildingUnitLatestItemProjections(IOptions options) + { + var wkbReader = WKBReaderFactory.Create(); + + #region Building + + When>(async (context, message, ct) => + { + foreach (var buildingUnit in message.Message.BuildingUnits) + { + var geometryAsBinary = buildingUnit.ExtendedWkbGeometry.ToByteArray(); + var sysGeometry = wkbReader.Read(geometryAsBinary); + + var buildingUnitLatestItem = new BuildingUnitLatestItem + { + BuildingUnitPersistentLocalId = buildingUnit.BuildingUnitPersistentLocalId, + BuildingPersistentLocalId = message.Message.BuildingPersistentLocalId, + Status = BuildingUnitStatus.Parse(buildingUnit.Status).Map(), + OsloStatus = buildingUnit.Status, + Function = BuildingUnitFunction.Parse(buildingUnit.Function).Map(), + OsloFunction = buildingUnit.Function, + GeometryMethod = BuildingUnitPositionGeometryMethod.Parse(buildingUnit.GeometryMethod).Map(), + OsloGeometryMethod = buildingUnit.GeometryMethod, + Geometry = sysGeometry, + HasDeviation = false, + IsRemoved = buildingUnit.IsRemoved, + VersionTimestamp = message.Message.Provenance.Timestamp, + Namespace = options.Value.BuildingUnitNamespace, + PuriId = $"{options.Value.BuildingUnitNamespace}/{buildingUnit.BuildingUnitPersistentLocalId}" + }; + + await context + .BuildingUnitLatestItems + .AddAsync(buildingUnitLatestItem, ct); + + var addressPersistentLocalIds = buildingUnit.AddressPersistentLocalIds.Distinct(); + foreach (var addressPersistentLocalId in addressPersistentLocalIds) + { + await context.AddIdempotentBuildingUnitAddress(buildingUnitLatestItem, addressPersistentLocalId, ct); + } + } + }); + + When>(async (context, message, ct) => + { + if (!message.Message.BuildingUnitPersistentLocalIds.Any()) + { + return; + } + + var geometryAsBinary = message.Message.ExtendedWkbGeometryBuildingUnits!.ToByteArray(); + var sysGeometry = wkbReader.Read(geometryAsBinary); + + foreach (var buildingUnitPersistentLocalId in message.Message.BuildingUnitPersistentLocalIds) + { + await context.FindAndUpdateBuildingUnit( + buildingUnitPersistentLocalId, + buildingUnit => + { + buildingUnit.Geometry = sysGeometry; + buildingUnit.GeometryMethod = BuildingUnitPositionGeometryMethod.DerivedFromObject.Map(); + buildingUnit.OsloGeometryMethod = BuildingUnitPositionGeometryMethod.DerivedFromObject.GeometryMethod; + UpdateVersionTimestamp(buildingUnit, message.Message); + return Task.CompletedTask; + }, + ct); + } + }); + + When>(async (context, message, ct) => + { + if (!message.Message.BuildingUnitPersistentLocalIds.Any() + && !message.Message.BuildingUnitPersistentLocalIdsWhichBecameDerived.Any()) + { + return; + } + + var geometryAsBinary = message.Message.ExtendedWkbGeometryBuildingUnits!.ToByteArray(); + var sysGeometry = wkbReader.Read(geometryAsBinary); + + foreach (var buildingUnitPersistentLocalId in message.Message.BuildingUnitPersistentLocalIds + .Concat(message.Message.BuildingUnitPersistentLocalIdsWhichBecameDerived)) + { + await context.FindAndUpdateBuildingUnit( + buildingUnitPersistentLocalId, + buildingUnit => + { + buildingUnit.Geometry = sysGeometry; + buildingUnit.GeometryMethod = BuildingUnitPositionGeometryMethod.DerivedFromObject.Map(); + buildingUnit.OsloGeometryMethod = BuildingUnitPositionGeometryMethod.DerivedFromObject.GeometryMethod; + UpdateVersionTimestamp(buildingUnit, message.Message); + return Task.CompletedTask; + }, + ct); + } + }); + + When>(async (context, message, ct) => + { + if (!message.Message.BuildingUnitPersistentLocalIds.Any() + && !message.Message.BuildingUnitPersistentLocalIdsWhichBecameDerived.Any()) + { + return; + } + + var geometryAsBinary = message.Message.ExtendedWkbGeometryBuildingUnits!.ToByteArray(); + var sysGeometry = wkbReader.Read(geometryAsBinary); + + foreach (var buildingUnitPersistentLocalId in message.Message.BuildingUnitPersistentLocalIds + .Concat(message.Message.BuildingUnitPersistentLocalIdsWhichBecameDerived)) + { + await context.FindAndUpdateBuildingUnit( + buildingUnitPersistentLocalId, + buildingUnit => + { + buildingUnit.Geometry = sysGeometry; + buildingUnit.GeometryMethod = BuildingUnitPositionGeometryMethod.DerivedFromObject.Map(); + buildingUnit.OsloGeometryMethod = BuildingUnitPositionGeometryMethod.DerivedFromObject.GeometryMethod; + UpdateVersionTimestamp(buildingUnit, message.Message); + return Task.CompletedTask; + }, + ct); + } + }); + + When>(async (context, message, ct) => + { + if (!message.Message.BuildingUnitPersistentLocalIds.Any() + && !message.Message.BuildingUnitPersistentLocalIdsWhichBecameDerived.Any()) + { + return; + } + + var geometryAsBinary = message.Message.ExtendedWkbGeometryBuildingUnits!.ToByteArray(); + var sysGeometry = wkbReader.Read(geometryAsBinary); + + foreach (var buildingUnitPersistentLocalId in message.Message.BuildingUnitPersistentLocalIds + .Concat(message.Message.BuildingUnitPersistentLocalIdsWhichBecameDerived)) + { + await context.FindAndUpdateBuildingUnit( + buildingUnitPersistentLocalId, + buildingUnit => + { + buildingUnit.Geometry = sysGeometry; + buildingUnit.GeometryMethod = BuildingUnitPositionGeometryMethod.DerivedFromObject.Map(); + buildingUnit.OsloGeometryMethod = BuildingUnitPositionGeometryMethod.DerivedFromObject.GeometryMethod; + UpdateVersionTimestamp(buildingUnit, message.Message); + return Task.CompletedTask; + }, + ct); + } + }); + + #endregion + + When>(async (context, message, ct) => + { + var geometryAsBinary = message.Message.ExtendedWkbGeometry.ToByteArray(); + var sysGeometry = wkbReader.Read(geometryAsBinary); + + var buildingUnitLatestItem = new BuildingUnitLatestItem + { + BuildingUnitPersistentLocalId = message.Message.BuildingUnitPersistentLocalId, + BuildingPersistentLocalId = message.Message.BuildingPersistentLocalId, + Status = BuildingUnitStatus.Planned.Map(), + OsloStatus = BuildingUnitStatus.Planned.Status, + Function = BuildingUnitFunction.Parse(message.Message.Function).Map(), + OsloFunction = message.Message.Function, + GeometryMethod = BuildingUnitPositionGeometryMethod.Parse(message.Message.GeometryMethod).Map(), + OsloGeometryMethod = message.Message.GeometryMethod, + Geometry = sysGeometry, + HasDeviation = message.Message.HasDeviation, + IsRemoved = false, + VersionTimestamp = message.Message.Provenance.Timestamp, + Namespace = options.Value.BuildingUnitNamespace, + PuriId = $"{options.Value.BuildingUnitNamespace}/{message.Message.BuildingUnitPersistentLocalId}" + }; + + await context + .BuildingUnitLatestItems + .AddAsync(buildingUnitLatestItem, ct); + }); + + When>(async (context, message, ct) => + { + await context.FindAndUpdateBuildingUnit( + message.Message.BuildingUnitPersistentLocalId, + buildingUnit => + { + buildingUnit.Status = BuildingUnitStatus.Realized.Map(); + buildingUnit.OsloStatus = BuildingUnitStatus.Realized.Status; + UpdateVersionTimestamp(buildingUnit, message.Message); + return Task.CompletedTask; + }, + ct); + }); + + When>(async (context, message, ct) => + { + await context.FindAndUpdateBuildingUnit( + message.Message.BuildingUnitPersistentLocalId, + buildingUnit => + { + buildingUnit.Status = BuildingUnitStatus.Realized.Map(); + buildingUnit.OsloStatus = BuildingUnitStatus.Realized.Status; + UpdateVersionTimestamp(buildingUnit, message.Message); + return Task.CompletedTask; + }, + ct); + }); + + When>(async (context, message, ct) => + { + await context.FindAndUpdateBuildingUnit( + message.Message.BuildingUnitPersistentLocalId, + buildingUnit => + { + buildingUnit.Status = BuildingUnitStatus.Planned.Map(); + buildingUnit.OsloStatus = BuildingUnitStatus.Planned.Status; + UpdateVersionTimestamp(buildingUnit, message.Message); + return Task.CompletedTask; + }, + ct); + }); + + When>(async (context, message, ct) => + { + await context.FindAndUpdateBuildingUnit( + message.Message.BuildingUnitPersistentLocalId, + buildingUnit => + { + buildingUnit.Status = BuildingUnitStatus.Planned.Map(); + buildingUnit.OsloStatus = BuildingUnitStatus.Planned.Status; + UpdateVersionTimestamp(buildingUnit, message.Message); + return Task.CompletedTask; + }, + ct); + }); + + When>(async (context, message, ct) => + { + await context.FindAndUpdateBuildingUnit( + message.Message.BuildingUnitPersistentLocalId, + buildingUnit => + { + buildingUnit.Status = BuildingUnitStatus.NotRealized.Map(); + buildingUnit.OsloStatus = BuildingUnitStatus.NotRealized.Status; + UpdateVersionTimestamp(buildingUnit, message.Message); + return Task.CompletedTask; + }, + ct); + }); + + When>(async (context, message, ct) => + { + await context.FindAndUpdateBuildingUnit( + message.Message.BuildingUnitPersistentLocalId, + buildingUnit => + { + buildingUnit.Status = BuildingUnitStatus.NotRealized.Map(); + buildingUnit.OsloStatus = BuildingUnitStatus.NotRealized.Status; + UpdateVersionTimestamp(buildingUnit, message.Message); + return Task.CompletedTask; + }, + ct); + }); + + When>(async (context, message, ct) => + { + await context.FindAndUpdateBuildingUnit( + message.Message.BuildingUnitPersistentLocalId, + buildingUnit => + { + buildingUnit.Status = BuildingUnitStatus.Planned.Map(); + buildingUnit.OsloStatus = BuildingUnitStatus.Planned.Status; + UpdateVersionTimestamp(buildingUnit, message.Message); + return Task.CompletedTask; + }, + ct); + }); + + When>(async (context, message, ct) => + { + await context.FindAndUpdateBuildingUnit( + message.Message.BuildingUnitPersistentLocalId, + buildingUnit => + { + buildingUnit.Status = BuildingUnitStatus.Retired.Map(); + buildingUnit.OsloStatus = BuildingUnitStatus.Retired.Status; + UpdateVersionTimestamp(buildingUnit, message.Message); + return Task.CompletedTask; + }, + ct); + }); + + When>(async (context, message, ct) => + { + await context.FindAndUpdateBuildingUnit( + message.Message.BuildingUnitPersistentLocalId, + buildingUnit => + { + buildingUnit.Status = BuildingUnitStatus.Realized.Map(); + buildingUnit.OsloStatus = BuildingUnitStatus.Realized.Status; + UpdateVersionTimestamp(buildingUnit, message.Message); + return Task.CompletedTask; + }, + ct); + }); + + When>(async (context, message, ct) => + { + await context.FindAndUpdateBuildingUnit( + message.Message.BuildingUnitPersistentLocalId, + buildingUnit => + { + buildingUnit.IsRemoved = true; + UpdateVersionTimestamp(buildingUnit, message.Message); + return Task.CompletedTask; + }, + ct); + }); + + When>(async (context, message, ct) => + { + await context.FindAndUpdateBuildingUnit( + message.Message.BuildingUnitPersistentLocalId, + buildingUnit => + { + buildingUnit.IsRemoved = true; + UpdateVersionTimestamp(buildingUnit, message.Message); + return Task.CompletedTask; + }, + ct); + }); + + When>(async (context, message, ct) => + { + await context.FindAndUpdateBuildingUnit( + message.Message.BuildingUnitPersistentLocalId, + buildingUnit => + { + var geometryAsBinary = message.Message.ExtendedWkbGeometry.ToByteArray(); + var sysGeometry = wkbReader.Read(geometryAsBinary); + + buildingUnit.Status = BuildingUnitStatus.Parse(message.Message.BuildingUnitStatus).Map(); + buildingUnit.OsloStatus = message.Message.BuildingUnitStatus; + buildingUnit.Function = BuildingUnitFunction.Parse(message.Message.Function).Map(); + buildingUnit.OsloFunction = message.Message.Function; + buildingUnit.Geometry = sysGeometry; + buildingUnit.GeometryMethod = BuildingUnitPositionGeometryMethod.Parse(message.Message.GeometryMethod).Map(); + buildingUnit.OsloGeometryMethod = message.Message.GeometryMethod; + buildingUnit.HasDeviation = message.Message.HasDeviation; + buildingUnit.IsRemoved = false; + UpdateVersionTimestamp(buildingUnit, message.Message); + return Task.CompletedTask; + }, + ct); + }); + + When>(async (context, message, ct) => + { + await context.FindAndUpdateBuildingUnit( + message.Message.BuildingUnitPersistentLocalId, + buildingUnit => + { + buildingUnit.HasDeviation = false; + UpdateVersionTimestamp(buildingUnit, message.Message); + return Task.CompletedTask; + }, + ct); + }); + + When>(async (context, message, ct) => + { + await context.FindAndUpdateBuildingUnit( + message.Message.BuildingUnitPersistentLocalId, + buildingUnit => + { + buildingUnit.HasDeviation = true; + UpdateVersionTimestamp(buildingUnit, message.Message); + return Task.CompletedTask; + }, + ct); + }); + + When>(async (context, message, ct) => + { + await context.FindAndUpdateBuildingUnit( + message.Message.BuildingUnitPersistentLocalId, + buildingUnit => + { + buildingUnit.HasDeviation = true; + UpdateVersionTimestamp(buildingUnit, message.Message); + return Task.CompletedTask; + }, + ct); + }); + + When>(async (context, message, ct) => + { + await context.FindAndUpdateBuildingUnit( + message.Message.BuildingUnitPersistentLocalId, + buildingUnit => + { + buildingUnit.HasDeviation = false; + UpdateVersionTimestamp(buildingUnit, message.Message); + return Task.CompletedTask; + }, + ct); + }); + + When>(async (context, message, ct) => + { + var geometryAsBinary = message.Message.ExtendedWkbGeometry.ToByteArray(); + var sysGeometry = wkbReader.Read(geometryAsBinary); + + var buildingUnitLatestItem = new BuildingUnitLatestItem + { + BuildingUnitPersistentLocalId = message.Message.BuildingUnitPersistentLocalId, + BuildingPersistentLocalId = message.Message.BuildingPersistentLocalId, + Status = BuildingUnitStatus.Parse(message.Message.BuildingUnitStatus).Map(), + OsloStatus = message.Message.BuildingUnitStatus, + Function = BuildingUnitFunction.Common.Map(), + OsloFunction = BuildingUnitFunction.Common.Function, + GeometryMethod = BuildingUnitPositionGeometryMethod.Parse(message.Message.GeometryMethod).Map(), + OsloGeometryMethod = message.Message.GeometryMethod, + Geometry = sysGeometry, + HasDeviation = message.Message.HasDeviation, + IsRemoved = false, + VersionTimestamp = message.Message.Provenance.Timestamp, + Namespace = options.Value.BuildingUnitNamespace, + PuriId = $"{options.Value.BuildingUnitNamespace}/{message.Message.BuildingUnitPersistentLocalId}" + }; + + await context + .BuildingUnitLatestItems + .AddAsync(buildingUnitLatestItem, ct); + }); + + When>(async (context, message, ct) => + { + var geometryAsBinary = message.Message.ExtendedWkbGeometry.ToByteArray(); + var sysGeometry = wkbReader.Read(geometryAsBinary); + + await context.FindAndUpdateBuildingUnit( + message.Message.BuildingUnitPersistentLocalId, + buildingUnit => + { + buildingUnit.Geometry = sysGeometry; + buildingUnit.GeometryMethod = BuildingUnitPositionGeometryMethod.Parse(message.Message.GeometryMethod).Map(); + buildingUnit.OsloGeometryMethod = message.Message.GeometryMethod; + UpdateVersionTimestamp(buildingUnit, message.Message); + return Task.CompletedTask; + }, + ct); + }); + + When>(async (context, message, ct) => + { + await context.FindAndUpdateBuildingUnit( + message.Message.BuildingUnitPersistentLocalId, + async buildingUnit => + { + await context.AddIdempotentBuildingUnitAddress(buildingUnit, message.Message.AddressPersistentLocalId, ct); + UpdateVersionTimestamp(buildingUnit, message.Message); + }, + ct); + }); + + When>(async (context, message, ct) => + { + await context.FindAndUpdateBuildingUnit( + message.Message.BuildingUnitPersistentLocalId, + async buildingUnit => + { + await context.RemoveIdempotentBuildingUnitAddress(buildingUnit, message.Message.AddressPersistentLocalId, ct); + UpdateVersionTimestamp(buildingUnit, message.Message); + }, + ct); + }); + + When>(async (context, message, ct) => + { + await context.FindAndUpdateBuildingUnit( + message.Message.BuildingUnitPersistentLocalId, + async buildingUnit => + { + await context.RemoveIdempotentBuildingUnitAddress(buildingUnit, message.Message.AddressPersistentLocalId, ct); + UpdateVersionTimestamp(buildingUnit, message.Message); + }, + ct); + }); + + When>(async (context, message, ct) => + { + await context.FindAndUpdateBuildingUnit( + message.Message.BuildingUnitPersistentLocalId, + async buildingUnit => + { + await context.RemoveIdempotentBuildingUnitAddress(buildingUnit, message.Message.AddressPersistentLocalId, ct); + UpdateVersionTimestamp(buildingUnit, message.Message); + }, + ct); + }); + + When>(async (context, message, ct) => + { + await context.FindAndUpdateBuildingUnit( + message.Message.BuildingUnitPersistentLocalId, + async buildingUnit => + { + await context.RemoveIdempotentBuildingUnitAddress(buildingUnit, message.Message.AddressPersistentLocalId, ct); + UpdateVersionTimestamp(buildingUnit, message.Message); + }, + ct); + }); + + When>(async (context, message, ct) => + { + await context.FindAndUpdateBuildingUnit( + message.Message.BuildingUnitPersistentLocalId, + async buildingUnit => + { + await context.RemoveIdempotentBuildingUnitAddress(buildingUnit, message.Message.PreviousAddressPersistentLocalId, ct); + await context.AddIdempotentBuildingUnitAddress(buildingUnit, message.Message.NewAddressPersistentLocalId, ct); + UpdateVersionTimestamp(buildingUnit, message.Message); + }, + ct); + }); + + When>(async (context, message, ct) => + { + await context.FindAndUpdateBuildingUnit( + message.Message.BuildingUnitPersistentLocalId, + buildingUnit => + { + buildingUnit.Status = BuildingUnitStatus.Retired.Map(); + buildingUnit.OsloStatus = BuildingUnitStatus.Retired.Status; + UpdateVersionTimestamp(buildingUnit, message.Message); + return Task.CompletedTask; + }, + ct); + }); + + When>(async (context, message, ct) => + { + await context.FindAndUpdateBuildingUnit( + message.Message.BuildingUnitPersistentLocalId, + buildingUnit => + { + buildingUnit.Status = BuildingUnitStatus.NotRealized.Map(); + buildingUnit.OsloStatus = BuildingUnitStatus.NotRealized.Status; + UpdateVersionTimestamp(buildingUnit, message.Message); + return Task.CompletedTask; + }, + ct); + }); + + When>(async (context, message, ct) => + { + await context.FindAndUpdateBuildingUnit( + message.Message.BuildingUnitPersistentLocalId, + async buildingUnit => + { + var geometryAsBinary = message.Message.ExtendedWkbGeometry.ToByteArray(); + var sysGeometry = wkbReader.Read(geometryAsBinary); + + buildingUnit.BuildingPersistentLocalId = message.Message.BuildingPersistentLocalId; + buildingUnit.Status = BuildingUnitStatus.Parse(message.Message.Status).Map(); + buildingUnit.OsloStatus = message.Message.Status; + buildingUnit.Function = BuildingUnitFunction.Parse(message.Message.Function).Map(); + buildingUnit.OsloFunction = message.Message.Function; + buildingUnit.Geometry = sysGeometry; + buildingUnit.GeometryMethod = BuildingUnitPositionGeometryMethod.Parse(message.Message.GeometryMethod).Map(); + buildingUnit.OsloGeometryMethod = message.Message.GeometryMethod; + buildingUnit.HasDeviation = message.Message.HasDeviation; + UpdateVersionTimestamp(buildingUnit, message.Message); + + var addressPersistentLocalIds = message.Message.AddressPersistentLocalIds.Distinct(); + foreach (var addressPersistentLocalId in addressPersistentLocalIds) + { + await context.AddIdempotentBuildingUnitAddress(buildingUnit, addressPersistentLocalId, ct); + } + }, + ct); + }); + + // BuildingUnitWasTransferred couples the unit to another building and BuildingUnitMoved is an event applicable on the old building. + When>((_, _, _) => Task.CompletedTask); + } + + private static void UpdateVersionTimestamp(BuildingUnitLatestItem building, IHasProvenance message) + => building.VersionTimestamp = message.Provenance.Timestamp; + } +} diff --git a/src/BuildingRegistry.Projections.Integration/Converters/BuildingExtensions.cs b/src/BuildingRegistry.Projections.Integration/Converters/BuildingExtensions.cs new file mode 100644 index 000000000..f700081df --- /dev/null +++ b/src/BuildingRegistry.Projections.Integration/Converters/BuildingExtensions.cs @@ -0,0 +1,57 @@ +namespace BuildingRegistry.Projections.Integration.Converters +{ + using System; + using Be.Vlaanderen.Basisregisters.GrAr.Legacy.Gebouw; + using BuildingRegistry.Building; + + public static class BuildingStatusExtensions + { + public static string Map(this BuildingStatus status) + { + if (status == BuildingStatus.Planned) + { + return GebouwStatus.Gepland.ToString(); + } + + if (status == BuildingStatus.UnderConstruction) + { + return GebouwStatus.InAanbouw.ToString(); + } + + if (status == BuildingStatus.NotRealized) + { + return GebouwStatus.NietGerealiseerd.ToString(); + } + + if (status == BuildingStatus.Realized) + { + return GebouwStatus.Gerealiseerd.ToString(); + } + + if (status == BuildingStatus.Retired) + { + return GebouwStatus.Gehistoreerd.ToString(); + } + + throw new ArgumentOutOfRangeException(nameof(status), status, null); + } + } + + public static class BuildingGeometryMethodExtensions + { + public static string Map(this BuildingGeometryMethod geometryMethod) + { + if (geometryMethod == BuildingGeometryMethod.Outlined) + { + return GeometrieMethode.Ingeschetst.ToString(); + } + + if (geometryMethod == BuildingGeometryMethod.MeasuredByGrb) + { + return GeometrieMethode.IngemetenGRB.ToString(); + } + + throw new ArgumentOutOfRangeException(nameof(geometryMethod), geometryMethod, null); + } + } +} diff --git a/src/BuildingRegistry.Projections.Integration/Converters/BuildingUnitExtensions.cs b/src/BuildingRegistry.Projections.Integration/Converters/BuildingUnitExtensions.cs new file mode 100644 index 000000000..680ea2e1e --- /dev/null +++ b/src/BuildingRegistry.Projections.Integration/Converters/BuildingUnitExtensions.cs @@ -0,0 +1,71 @@ +namespace BuildingRegistry.Projections.Integration.Converters +{ + using System; + using Be.Vlaanderen.Basisregisters.GrAr.Legacy; + using Be.Vlaanderen.Basisregisters.GrAr.Legacy.Gebouweenheid; + using BuildingRegistry.Building; + + public static class BuildingUnitStatusExtensions + { + public static string Map(this BuildingUnitStatus status) + { + if (BuildingUnitStatus.Planned == status) + { + return GebouweenheidStatus.Gepland.ToString(); + } + + if (BuildingUnitStatus.NotRealized == status) + { + return GebouweenheidStatus.NietGerealiseerd.ToString(); + } + + if (BuildingUnitStatus.Realized == status) + { + return GebouweenheidStatus.Gerealiseerd.ToString(); + } + + if (BuildingUnitStatus.Retired == status) + { + return GebouweenheidStatus.Gehistoreerd.ToString(); + } + + throw new ArgumentOutOfRangeException(nameof(status), status, null); + } + } + + public static class BuildingUnitPositionGeometryMethodExtensions + { + public static string Map(this BuildingUnitPositionGeometryMethod geometryMethod) + { + if (BuildingUnitPositionGeometryMethod.DerivedFromObject == geometryMethod) + { + return PositieGeometrieMethode.AfgeleidVanObject.ToString(); + } + + if (BuildingUnitPositionGeometryMethod.AppointedByAdministrator == geometryMethod) + { + return PositieGeometrieMethode.AangeduidDoorBeheerder.ToString(); + } + + throw new ArgumentOutOfRangeException(nameof(geometryMethod), geometryMethod, null); + } + } + + public static class BuildingUnitFunctionExtensions + { + public static string Map(this BuildingUnitFunction function) + { + if (function == BuildingUnitFunction.Unknown) + { + return GebouweenheidFunctie.NietGekend.ToString(); + } + + if (function == BuildingUnitFunction.Common) + { + return GebouweenheidFunctie.GemeenschappelijkDeel.ToString(); + } + + throw new ArgumentOutOfRangeException(nameof(function), function, null); + } + } +} diff --git a/src/BuildingRegistry.Projections.Integration/Infrastructure/IntegrationModule.cs b/src/BuildingRegistry.Projections.Integration/Infrastructure/IntegrationModule.cs new file mode 100644 index 000000000..51f3081b3 --- /dev/null +++ b/src/BuildingRegistry.Projections.Integration/Infrastructure/IntegrationModule.cs @@ -0,0 +1,62 @@ +namespace BuildingRegistry.Projections.Integration.Infrastructure +{ + using System; + using Autofac; + using BuildingRegistry.Infrastructure; + using Microsoft.EntityFrameworkCore; + using Microsoft.Extensions.Configuration; + using Microsoft.Extensions.DependencyInjection; + using Microsoft.Extensions.Logging; + + public class IntegrationModule : Module + { + public IntegrationModule( + IConfiguration configuration, + IServiceCollection services, + ILoggerFactory loggerFactory) + { + var logger = loggerFactory.CreateLogger(); + var connectionString = configuration.GetConnectionString("IntegrationProjections"); + + var hasConnectionString = !string.IsNullOrWhiteSpace(connectionString); + if (hasConnectionString) + RunOnNpgSqlServer(services, connectionString); + else + RunInMemoryDb(services, loggerFactory, logger); + + logger.LogInformation( + "Added {Context} to services:" + + Environment.NewLine + + "\tSchema: {Schema}" + + Environment.NewLine + + "\tTableName: {TableName}", + nameof(IntegrationContext), Schema.Integration, MigrationTables.Integration); + } + + private static void RunOnNpgSqlServer( + IServiceCollection services, + string connectionString) + { + services + .AddNpgsql(connectionString, sqlServerOptions => + { + sqlServerOptions.EnableRetryOnFailure(); + sqlServerOptions.MigrationsHistoryTable(MigrationTables.Integration, Schema.Integration); + sqlServerOptions.UseNetTopologySuite(); + }); + } + + private static void RunInMemoryDb( + IServiceCollection services, + ILoggerFactory loggerFactory, + ILogger logger) + { + services + .AddDbContext(options => options + .UseLoggerFactory(loggerFactory) + .UseInMemoryDatabase(Guid.NewGuid().ToString(), _ => { })); + + logger.LogWarning("Running InMemory for {Context}!", nameof(IntegrationContext)); + } + } +} diff --git a/src/BuildingRegistry.Projections.Integration/Infrastructure/IntegrationOptions.cs b/src/BuildingRegistry.Projections.Integration/Infrastructure/IntegrationOptions.cs new file mode 100644 index 000000000..44e82d62d --- /dev/null +++ b/src/BuildingRegistry.Projections.Integration/Infrastructure/IntegrationOptions.cs @@ -0,0 +1,9 @@ +namespace BuildingRegistry.Projections.Integration.Infrastructure +{ + public class IntegrationOptions + { + public string BuildingNamespace { get; set; } + public string BuildingUnitNamespace { get; set; } + public bool Enabled { get; set; } + } +} diff --git a/src/BuildingRegistry.Projections.Integration/IntegrationContext.cs b/src/BuildingRegistry.Projections.Integration/IntegrationContext.cs new file mode 100644 index 000000000..cfd264d18 --- /dev/null +++ b/src/BuildingRegistry.Projections.Integration/IntegrationContext.cs @@ -0,0 +1,54 @@ +namespace BuildingRegistry.Projections.Integration +{ + using System.Linq; + using System.Threading; + using System.Threading.Tasks; + using Be.Vlaanderen.Basisregisters.ProjectionHandling.Runner; + using Building.LatestItem; + using BuildingRegistry.Infrastructure; + using BuildingUnit.LatestItem; + using Microsoft.EntityFrameworkCore; + using NetTopologySuite.Geometries; + + public class IntegrationContext : RunnerDbContext + { + public override string ProjectionStateSchema => Schema.Integration; + + public DbSet BuildingLatestItems => Set(); + + public DbSet BuildingUnitLatestItems => Set(); + public DbSet BuildingUnitAddresses => Set(); + + private DbSet MunicipalityGeometries => Set(); + + public async Task FindMostIntersectingNisCodeBy(Geometry sysGeometry, CancellationToken ct) + { + var municipalityGeometries = await MunicipalityGeometries + .Where(x => sysGeometry.Intersects(x.Geometry)) + .ToListAsync(ct); + + if (municipalityGeometries.Count > 1) + { + return municipalityGeometries + .Select(x => new + { + MunicipalityGeometry = x, + Intersection = sysGeometry.Intersection(x.Geometry) + }).MaxBy(x => x.Intersection)! + .MunicipalityGeometry + .NisCode; + } + + return municipalityGeometries.Count == 1 + ? municipalityGeometries.Single().NisCode + : null; + } + + // This needs to be here to please EF + public IntegrationContext() { } + + // This needs to be DbContextOptions for Autofac! + public IntegrationContext(DbContextOptions options) + : base(options) { } + } +} diff --git a/src/BuildingRegistry.Projections.Integration/IntegrationContextMigrationFactory.cs b/src/BuildingRegistry.Projections.Integration/IntegrationContextMigrationFactory.cs new file mode 100644 index 000000000..1865aa2c3 --- /dev/null +++ b/src/BuildingRegistry.Projections.Integration/IntegrationContextMigrationFactory.cs @@ -0,0 +1,29 @@ +namespace BuildingRegistry.Projections.Integration +{ + using Be.Vlaanderen.Basisregisters.ProjectionHandling.Runner.Npgsql; + using Microsoft.EntityFrameworkCore; + using BuildingRegistry.Infrastructure; + using Npgsql.EntityFrameworkCore.PostgreSQL.Infrastructure; + + public class IntegrationContextMigrationFactory : NpgsqlRunnerDbContextMigrationFactory + { + public IntegrationContextMigrationFactory() + : base("IntegrationProjectionsAdmin", HistoryConfiguration) { } + + private static MigrationHistoryConfiguration HistoryConfiguration => + new MigrationHistoryConfiguration + { + Schema = Schema.Integration, + Table = MigrationTables.Integration + }; + + protected override void ConfigureSqlServerOptions(NpgsqlDbContextOptionsBuilder serverOptions) + { + serverOptions.UseNetTopologySuite(); + base.ConfigureSqlServerOptions(serverOptions); + } + + protected override IntegrationContext CreateContext(DbContextOptions migrationContextOptions) + => new IntegrationContext(migrationContextOptions); + } +} diff --git a/src/BuildingRegistry.Projections.Integration/Migrations/20240115145624_Initial.Designer.cs b/src/BuildingRegistry.Projections.Integration/Migrations/20240115145624_Initial.Designer.cs new file mode 100644 index 000000000..5825afb4a --- /dev/null +++ b/src/BuildingRegistry.Projections.Integration/Migrations/20240115145624_Initial.Designer.cs @@ -0,0 +1,258 @@ +// +using System; +using BuildingRegistry.Projections.Integration; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using NetTopologySuite.Geometries; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace BuildingRegistry.Projections.Integration.Migrations +{ + [DbContext(typeof(IntegrationContext))] + [Migration("20240115145624_Initial")] + partial class Initial + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "6.0.3") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Be.Vlaanderen.Basisregisters.ProjectionHandling.Runner.ProjectionStates.ProjectionStateItem", b => + { + b.Property("Name") + .HasColumnType("text"); + + b.Property("DesiredState") + .HasColumnType("text"); + + b.Property("DesiredStateChangedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ErrorMessage") + .HasColumnType("text"); + + b.Property("Position") + .HasColumnType("bigint"); + + b.HasKey("Name"); + + b.ToTable("ProjectionStates", "integration_building"); + }); + + modelBuilder.Entity("BuildingRegistry.Projections.Integration.Building.LatestItem.BuildingLatestItem", b => + { + b.Property("BuildingPersistentLocalId") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("building_persistent_local_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("BuildingPersistentLocalId")); + + b.Property("Geometry") + .IsRequired() + .HasColumnType("geometry") + .HasColumnName("geometry"); + + b.Property("GeometryMethod") + .IsRequired() + .HasColumnType("text") + .HasColumnName("geometry_method"); + + b.Property("IsRemoved") + .HasColumnType("boolean") + .HasColumnName("is_removed"); + + b.Property("Namespace") + .IsRequired() + .HasColumnType("text") + .HasColumnName("namespace"); + + b.Property("NisCode") + .HasColumnType("text") + .HasColumnName("nis_code"); + + b.Property("OsloGeometryMethod") + .IsRequired() + .HasColumnType("text") + .HasColumnName("oslo_geometry_method"); + + b.Property("OsloStatus") + .IsRequired() + .HasColumnType("text") + .HasColumnName("oslo_status"); + + b.Property("PuriId") + .IsRequired() + .HasColumnType("text") + .HasColumnName("puri_id"); + + b.Property("Status") + .IsRequired() + .HasColumnType("text") + .HasColumnName("status"); + + b.Property("VersionAsString") + .IsRequired() + .HasColumnType("text") + .HasColumnName("version_as_string"); + + b.Property("VersionTimestampAsDateTimeOffset") + .HasColumnType("timestamp with time zone") + .HasColumnName("version_timestamp"); + + b.HasKey("BuildingPersistentLocalId"); + + b.HasIndex("Geometry"); + + NpgsqlIndexBuilderExtensions.HasMethod(b.HasIndex("Geometry"), "GIST"); + + b.HasIndex("IsRemoved"); + + b.HasIndex("NisCode"); + + b.HasIndex("OsloStatus"); + + b.HasIndex("Status"); + + b.ToTable("building_latest_items", "integration_building"); + }); + + modelBuilder.Entity("BuildingRegistry.Projections.Integration.Building.LatestItem.MunicipalityGeometry", b => + { + b.Property("Geometry") + .IsRequired() + .HasColumnType("geometry") + .HasColumnName("geometry"); + + b.Property("NisCode") + .IsRequired() + .HasColumnType("text") + .HasColumnName("nis_code"); + + b.ToView("municipality_geometries", "integration_municipality"); + + b.ToSqlQuery("SELECT nis_code, geometry FROM integration_municipality.municipality_geometries"); + }); + + modelBuilder.Entity("BuildingRegistry.Projections.Integration.BuildingUnit.LatestItem.BuildingUnitAddress", b => + { + b.Property("BuildingUnitPersistentLocalId") + .HasColumnType("integer") + .HasColumnName("building_unit_persistent_local_id"); + + b.Property("AddressPersistentLocalId") + .HasColumnType("integer") + .HasColumnName("address_persistent_local_id"); + + b.HasKey("BuildingUnitPersistentLocalId", "AddressPersistentLocalId"); + + b.HasIndex("AddressPersistentLocalId"); + + b.HasIndex("BuildingUnitPersistentLocalId"); + + b.ToTable("building_unit_addresses", "integration_building"); + }); + + modelBuilder.Entity("BuildingRegistry.Projections.Integration.BuildingUnit.LatestItem.BuildingUnitLatestItem", b => + { + b.Property("BuildingUnitPersistentLocalId") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("building_unit_persistent_local_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("BuildingUnitPersistentLocalId")); + + b.Property("BuildingPersistentLocalId") + .HasColumnType("integer") + .HasColumnName("building_persistent_local_id"); + + b.Property("Function") + .IsRequired() + .HasColumnType("text") + .HasColumnName("function"); + + b.Property("Geometry") + .IsRequired() + .HasColumnType("geometry") + .HasColumnName("geometry"); + + b.Property("GeometryMethod") + .IsRequired() + .HasColumnType("text") + .HasColumnName("geometry_method"); + + b.Property("HasDeviation") + .HasColumnType("boolean") + .HasColumnName("has_deviation"); + + b.Property("IsRemoved") + .HasColumnType("boolean") + .HasColumnName("is_removed"); + + b.Property("Namespace") + .IsRequired() + .HasColumnType("text") + .HasColumnName("namespace"); + + b.Property("OsloFunction") + .IsRequired() + .HasColumnType("text") + .HasColumnName("oslo_function"); + + b.Property("OsloGeometryMethod") + .IsRequired() + .HasColumnType("text") + .HasColumnName("oslo_geometry_method"); + + b.Property("OsloStatus") + .IsRequired() + .HasColumnType("text") + .HasColumnName("oslo_status"); + + b.Property("PuriId") + .IsRequired() + .HasColumnType("text") + .HasColumnName("puri_id"); + + b.Property("Status") + .IsRequired() + .HasColumnType("text") + .HasColumnName("status"); + + b.Property("VersionAsString") + .IsRequired() + .HasColumnType("text") + .HasColumnName("version_as_string"); + + b.Property("VersionTimestampAsDateTimeOffset") + .HasColumnType("timestamp with time zone") + .HasColumnName("version_timestamp"); + + b.HasKey("BuildingUnitPersistentLocalId"); + + b.HasIndex("BuildingPersistentLocalId"); + + b.HasIndex("Geometry"); + + NpgsqlIndexBuilderExtensions.HasMethod(b.HasIndex("Geometry"), "GIST"); + + b.HasIndex("IsRemoved"); + + b.HasIndex("OsloStatus"); + + b.HasIndex("Status"); + + b.ToTable("building_unit_latest_items", "integration_building"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/BuildingRegistry.Projections.Integration/Migrations/20240115145624_Initial.cs b/src/BuildingRegistry.Projections.Integration/Migrations/20240115145624_Initial.cs new file mode 100644 index 000000000..ba3e40606 --- /dev/null +++ b/src/BuildingRegistry.Projections.Integration/Migrations/20240115145624_Initial.cs @@ -0,0 +1,191 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; +using NetTopologySuite.Geometries; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace BuildingRegistry.Projections.Integration.Migrations +{ + public partial class Initial : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.EnsureSchema( + name: "integration_building"); + + migrationBuilder.CreateTable( + name: "building_latest_items", + schema: "integration_building", + columns: table => new + { + building_persistent_local_id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + status = table.Column(type: "text", nullable: false), + oslo_status = table.Column(type: "text", nullable: false), + geometry_method = table.Column(type: "text", nullable: false), + oslo_geometry_method = table.Column(type: "text", nullable: false), + geometry = table.Column(type: "geometry", nullable: false), + nis_code = table.Column(type: "text", nullable: true), + is_removed = table.Column(type: "boolean", nullable: false), + puri_id = table.Column(type: "text", nullable: false), + @namespace = table.Column(name: "namespace", type: "text", nullable: false), + version_as_string = table.Column(type: "text", nullable: false), + version_timestamp = table.Column(type: "timestamp with time zone", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_building_latest_items", x => x.building_persistent_local_id); + }); + + migrationBuilder.CreateTable( + name: "building_unit_addresses", + schema: "integration_building", + columns: table => new + { + building_unit_persistent_local_id = table.Column(type: "integer", nullable: false), + address_persistent_local_id = table.Column(type: "integer", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_building_unit_addresses", x => new { x.building_unit_persistent_local_id, x.address_persistent_local_id }); + }); + + migrationBuilder.CreateTable( + name: "building_unit_latest_items", + schema: "integration_building", + columns: table => new + { + building_unit_persistent_local_id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + building_persistent_local_id = table.Column(type: "integer", nullable: false), + status = table.Column(type: "text", nullable: false), + oslo_status = table.Column(type: "text", nullable: false), + function = table.Column(type: "text", nullable: false), + oslo_function = table.Column(type: "text", nullable: false), + geometry_method = table.Column(type: "text", nullable: false), + oslo_geometry_method = table.Column(type: "text", nullable: false), + geometry = table.Column(type: "geometry", nullable: false), + has_deviation = table.Column(type: "boolean", nullable: false), + is_removed = table.Column(type: "boolean", nullable: false), + puri_id = table.Column(type: "text", nullable: false), + @namespace = table.Column(name: "namespace", type: "text", nullable: false), + version_as_string = table.Column(type: "text", nullable: false), + version_timestamp = table.Column(type: "timestamp with time zone", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_building_unit_latest_items", x => x.building_unit_persistent_local_id); + }); + + migrationBuilder.CreateTable( + name: "ProjectionStates", + schema: "integration_building", + columns: table => new + { + Name = table.Column(type: "text", nullable: false), + Position = table.Column(type: "bigint", nullable: false), + DesiredState = table.Column(type: "text", nullable: true), + DesiredStateChangedAt = table.Column(type: "timestamp with time zone", nullable: true), + ErrorMessage = table.Column(type: "text", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_ProjectionStates", x => x.Name); + }); + + migrationBuilder.CreateIndex( + name: "IX_building_latest_items_geometry", + schema: "integration_building", + table: "building_latest_items", + column: "geometry") + .Annotation("Npgsql:IndexMethod", "GIST"); + + migrationBuilder.CreateIndex( + name: "IX_building_latest_items_is_removed", + schema: "integration_building", + table: "building_latest_items", + column: "is_removed"); + + migrationBuilder.CreateIndex( + name: "IX_building_latest_items_nis_code", + schema: "integration_building", + table: "building_latest_items", + column: "nis_code"); + + migrationBuilder.CreateIndex( + name: "IX_building_latest_items_oslo_status", + schema: "integration_building", + table: "building_latest_items", + column: "oslo_status"); + + migrationBuilder.CreateIndex( + name: "IX_building_latest_items_status", + schema: "integration_building", + table: "building_latest_items", + column: "status"); + + migrationBuilder.CreateIndex( + name: "IX_building_unit_addresses_address_persistent_local_id", + schema: "integration_building", + table: "building_unit_addresses", + column: "address_persistent_local_id"); + + migrationBuilder.CreateIndex( + name: "IX_building_unit_addresses_building_unit_persistent_local_id", + schema: "integration_building", + table: "building_unit_addresses", + column: "building_unit_persistent_local_id"); + + migrationBuilder.CreateIndex( + name: "IX_building_unit_latest_items_building_persistent_local_id", + schema: "integration_building", + table: "building_unit_latest_items", + column: "building_persistent_local_id"); + + migrationBuilder.CreateIndex( + name: "IX_building_unit_latest_items_geometry", + schema: "integration_building", + table: "building_unit_latest_items", + column: "geometry") + .Annotation("Npgsql:IndexMethod", "GIST"); + + migrationBuilder.CreateIndex( + name: "IX_building_unit_latest_items_is_removed", + schema: "integration_building", + table: "building_unit_latest_items", + column: "is_removed"); + + migrationBuilder.CreateIndex( + name: "IX_building_unit_latest_items_oslo_status", + schema: "integration_building", + table: "building_unit_latest_items", + column: "oslo_status"); + + migrationBuilder.CreateIndex( + name: "IX_building_unit_latest_items_status", + schema: "integration_building", + table: "building_unit_latest_items", + column: "status"); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "building_latest_items", + schema: "integration_building"); + + migrationBuilder.DropTable( + name: "building_unit_addresses", + schema: "integration_building"); + + migrationBuilder.DropTable( + name: "building_unit_latest_items", + schema: "integration_building"); + + migrationBuilder.DropTable( + name: "ProjectionStates", + schema: "integration_building"); + } + } +} diff --git a/src/BuildingRegistry.Projections.Integration/Migrations/IntegrationContextModelSnapshot.cs b/src/BuildingRegistry.Projections.Integration/Migrations/IntegrationContextModelSnapshot.cs new file mode 100644 index 000000000..19738953f --- /dev/null +++ b/src/BuildingRegistry.Projections.Integration/Migrations/IntegrationContextModelSnapshot.cs @@ -0,0 +1,256 @@ +// +using System; +using BuildingRegistry.Projections.Integration; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using NetTopologySuite.Geometries; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace BuildingRegistry.Projections.Integration.Migrations +{ + [DbContext(typeof(IntegrationContext))] + partial class IntegrationContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "6.0.3") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Be.Vlaanderen.Basisregisters.ProjectionHandling.Runner.ProjectionStates.ProjectionStateItem", b => + { + b.Property("Name") + .HasColumnType("text"); + + b.Property("DesiredState") + .HasColumnType("text"); + + b.Property("DesiredStateChangedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ErrorMessage") + .HasColumnType("text"); + + b.Property("Position") + .HasColumnType("bigint"); + + b.HasKey("Name"); + + b.ToTable("ProjectionStates", "integration_building"); + }); + + modelBuilder.Entity("BuildingRegistry.Projections.Integration.Building.LatestItem.BuildingLatestItem", b => + { + b.Property("BuildingPersistentLocalId") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("building_persistent_local_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("BuildingPersistentLocalId")); + + b.Property("Geometry") + .IsRequired() + .HasColumnType("geometry") + .HasColumnName("geometry"); + + b.Property("GeometryMethod") + .IsRequired() + .HasColumnType("text") + .HasColumnName("geometry_method"); + + b.Property("IsRemoved") + .HasColumnType("boolean") + .HasColumnName("is_removed"); + + b.Property("Namespace") + .IsRequired() + .HasColumnType("text") + .HasColumnName("namespace"); + + b.Property("NisCode") + .HasColumnType("text") + .HasColumnName("nis_code"); + + b.Property("OsloGeometryMethod") + .IsRequired() + .HasColumnType("text") + .HasColumnName("oslo_geometry_method"); + + b.Property("OsloStatus") + .IsRequired() + .HasColumnType("text") + .HasColumnName("oslo_status"); + + b.Property("PuriId") + .IsRequired() + .HasColumnType("text") + .HasColumnName("puri_id"); + + b.Property("Status") + .IsRequired() + .HasColumnType("text") + .HasColumnName("status"); + + b.Property("VersionAsString") + .IsRequired() + .HasColumnType("text") + .HasColumnName("version_as_string"); + + b.Property("VersionTimestampAsDateTimeOffset") + .HasColumnType("timestamp with time zone") + .HasColumnName("version_timestamp"); + + b.HasKey("BuildingPersistentLocalId"); + + b.HasIndex("Geometry"); + + NpgsqlIndexBuilderExtensions.HasMethod(b.HasIndex("Geometry"), "GIST"); + + b.HasIndex("IsRemoved"); + + b.HasIndex("NisCode"); + + b.HasIndex("OsloStatus"); + + b.HasIndex("Status"); + + b.ToTable("building_latest_items", "integration_building"); + }); + + modelBuilder.Entity("BuildingRegistry.Projections.Integration.Building.LatestItem.MunicipalityGeometry", b => + { + b.Property("Geometry") + .IsRequired() + .HasColumnType("geometry") + .HasColumnName("geometry"); + + b.Property("NisCode") + .IsRequired() + .HasColumnType("text") + .HasColumnName("nis_code"); + + b.ToView("municipality_geometries", "integration_municipality"); + + b.ToSqlQuery("SELECT nis_code, geometry FROM integration_municipality.municipality_geometries"); + }); + + modelBuilder.Entity("BuildingRegistry.Projections.Integration.BuildingUnit.LatestItem.BuildingUnitAddress", b => + { + b.Property("BuildingUnitPersistentLocalId") + .HasColumnType("integer") + .HasColumnName("building_unit_persistent_local_id"); + + b.Property("AddressPersistentLocalId") + .HasColumnType("integer") + .HasColumnName("address_persistent_local_id"); + + b.HasKey("BuildingUnitPersistentLocalId", "AddressPersistentLocalId"); + + b.HasIndex("AddressPersistentLocalId"); + + b.HasIndex("BuildingUnitPersistentLocalId"); + + b.ToTable("building_unit_addresses", "integration_building"); + }); + + modelBuilder.Entity("BuildingRegistry.Projections.Integration.BuildingUnit.LatestItem.BuildingUnitLatestItem", b => + { + b.Property("BuildingUnitPersistentLocalId") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("building_unit_persistent_local_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("BuildingUnitPersistentLocalId")); + + b.Property("BuildingPersistentLocalId") + .HasColumnType("integer") + .HasColumnName("building_persistent_local_id"); + + b.Property("Function") + .IsRequired() + .HasColumnType("text") + .HasColumnName("function"); + + b.Property("Geometry") + .IsRequired() + .HasColumnType("geometry") + .HasColumnName("geometry"); + + b.Property("GeometryMethod") + .IsRequired() + .HasColumnType("text") + .HasColumnName("geometry_method"); + + b.Property("HasDeviation") + .HasColumnType("boolean") + .HasColumnName("has_deviation"); + + b.Property("IsRemoved") + .HasColumnType("boolean") + .HasColumnName("is_removed"); + + b.Property("Namespace") + .IsRequired() + .HasColumnType("text") + .HasColumnName("namespace"); + + b.Property("OsloFunction") + .IsRequired() + .HasColumnType("text") + .HasColumnName("oslo_function"); + + b.Property("OsloGeometryMethod") + .IsRequired() + .HasColumnType("text") + .HasColumnName("oslo_geometry_method"); + + b.Property("OsloStatus") + .IsRequired() + .HasColumnType("text") + .HasColumnName("oslo_status"); + + b.Property("PuriId") + .IsRequired() + .HasColumnType("text") + .HasColumnName("puri_id"); + + b.Property("Status") + .IsRequired() + .HasColumnType("text") + .HasColumnName("status"); + + b.Property("VersionAsString") + .IsRequired() + .HasColumnType("text") + .HasColumnName("version_as_string"); + + b.Property("VersionTimestampAsDateTimeOffset") + .HasColumnType("timestamp with time zone") + .HasColumnName("version_timestamp"); + + b.HasKey("BuildingUnitPersistentLocalId"); + + b.HasIndex("BuildingPersistentLocalId"); + + b.HasIndex("Geometry"); + + NpgsqlIndexBuilderExtensions.HasMethod(b.HasIndex("Geometry"), "GIST"); + + b.HasIndex("IsRemoved"); + + b.HasIndex("OsloStatus"); + + b.HasIndex("Status"); + + b.ToTable("building_unit_latest_items", "integration_building"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/BuildingRegistry.Projections.Integration/Properties/AssemblyInfo.cs b/src/BuildingRegistry.Projections.Integration/Properties/AssemblyInfo.cs new file mode 100644 index 000000000..d0f36f40e --- /dev/null +++ b/src/BuildingRegistry.Projections.Integration/Properties/AssemblyInfo.cs @@ -0,0 +1,7 @@ +using System.Reflection; +using System.Runtime.InteropServices; + +[assembly: AssemblyDescription("BuildingRegistry Integration Projections")] + +[assembly: ComVisible(false)] +[assembly: Guid("9dd11e3e-74fd-4fc9-b86a-59741579e787")] diff --git a/src/BuildingRegistry.Projections.Integration/paket.references b/src/BuildingRegistry.Projections.Integration/paket.references new file mode 100644 index 000000000..32209b3e1 --- /dev/null +++ b/src/BuildingRegistry.Projections.Integration/paket.references @@ -0,0 +1,16 @@ +Be.Vlaanderen.Basisregisters.AggregateSource +Be.Vlaanderen.Basisregisters.EventHandling.Autofac +Be.Vlaanderen.Basisregisters.DataDog.Tracing.Sql +Be.Vlaanderen.Basisregisters.ProjectionHandling.Runner.Npgsql +Be.Vlaanderen.Basisregisters.ProjectionHandling.SqlStreamStore.Autofac + +Npgsql.EntityFrameworkCore.PostgreSQL.Design +Npgsql.EntityFrameworkCore.PostgreSQL.NetTopologySuite + +Be.Vlaanderen.Basisregisters.GrAr.Common +Be.Vlaanderen.Basisregisters.GrAr.Legacy + +Dapper + +SourceLink.Embed.AllSourceFiles +SourceLink.Copy.PdbFiles diff --git a/src/BuildingRegistry.Projector/BuildingRegistry.Projector.csproj b/src/BuildingRegistry.Projector/BuildingRegistry.Projector.csproj index e9b3e2a4d..ac900392d 100644 --- a/src/BuildingRegistry.Projector/BuildingRegistry.Projector.csproj +++ b/src/BuildingRegistry.Projector/BuildingRegistry.Projector.csproj @@ -32,6 +32,7 @@ + diff --git a/src/BuildingRegistry.Projector/Infrastructure/Modules/ApiModule.cs b/src/BuildingRegistry.Projector/Infrastructure/Modules/ApiModule.cs index 1552be538..2c3548aca 100755 --- a/src/BuildingRegistry.Projector/Infrastructure/Modules/ApiModule.cs +++ b/src/BuildingRegistry.Projector/Infrastructure/Modules/ApiModule.cs @@ -1,6 +1,5 @@ namespace BuildingRegistry.Projector.Infrastructure.Modules { - using System; using Autofac; using Autofac.Extensions.DependencyInjection; using Be.Vlaanderen.Basisregisters.Api.Exceptions; @@ -19,21 +18,25 @@ namespace BuildingRegistry.Projector.Infrastructure.Modules using BuildingRegistry.Projections.Extract.BuildingExtract; using BuildingRegistry.Projections.Extract.BuildingUnitAddressLinkExtract; using BuildingRegistry.Projections.Extract.BuildingUnitExtract; + using BuildingRegistry.Projections.Integration; + using BuildingRegistry.Projections.Integration.Building.LatestItem; + using BuildingRegistry.Projections.Integration.BuildingUnit.LatestItem; + using BuildingRegistry.Projections.Integration.Infrastructure; using BuildingRegistry.Projections.LastChangedList; using BuildingRegistry.Projections.Legacy; - using BuildingRegistry.Projections.Legacy.BuildingDetail; using BuildingRegistry.Projections.Legacy.BuildingDetailV2; - using BuildingRegistry.Projections.Legacy.BuildingPersistentIdCrabIdMapping; using BuildingRegistry.Projections.Legacy.BuildingSyndication; - using BuildingRegistry.Projections.Legacy.BuildingUnitDetail; using BuildingRegistry.Projections.Legacy.BuildingUnitDetailV2; using BuildingRegistry.Projections.Wfs; using BuildingRegistry.Projections.Wms; + using BuildingRegistry.Projections.Wms.BuildingUnitV2; + using BuildingRegistry.Projections.Wms.BuildingV2; using Microsoft.Data.SqlClient; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; + using LastChangedListContextMigrationFactory = BuildingRegistry.Projections.LastChangedList.LastChangedListContextMigrationFactory; public class ApiModule : Module { @@ -83,6 +86,11 @@ private void RegisterProjectionSetup(ContainerBuilder builder) RegisterLegacyProjectionsV2(builder); RegisterWmsProjectionsV2(builder); RegisterWfsProjectionsV2(builder); + + if (_configuration.GetSection("Integration").GetValue("Enabled", false)) + { + RegisterIntegrationProjections(builder); + } } private void RegisterExtractProjectionsV2(ContainerBuilder builder) @@ -130,7 +138,7 @@ private void RegisterLastChangedProjections(ContainerBuilder builder) _loggerFactory)); builder - .RegisterProjectionMigrator( + .RegisterProjectionMigrator( _configuration, _loggerFactory) .RegisterProjectionMigrator( @@ -184,11 +192,11 @@ private void RegisterWmsProjectionsV2(ContainerBuilder builder) .RegisterProjectionMigrator( _configuration, _loggerFactory) - .RegisterProjections(() => - new BuildingRegistry.Projections.Wms.BuildingV2.BuildingV2Projections(WKBReaderFactory.Create()), + .RegisterProjections(() => + new BuildingV2Projections(WKBReaderFactory.Create()), wmsProjectionSettings) - .RegisterProjections(() => - new BuildingRegistry.Projections.Wms.BuildingUnitV2.BuildingUnitV2Projections(WKBReaderFactory.Create()), + .RegisterProjections(() => + new BuildingUnitV2Projections(WKBReaderFactory.Create()), wmsProjectionSettings); } @@ -216,5 +224,25 @@ private void RegisterWfsProjectionsV2(ContainerBuilder builder) new BuildingRegistry.Projections.Wfs.BuildingUnitV2.BuildingUnitV2Projections(WKBReaderFactory.Create()), wfsProjectionSettings); } + + private void RegisterIntegrationProjections(ContainerBuilder builder) + { + builder + .RegisterModule( + new IntegrationModule( + _configuration, + _services, + _loggerFactory)); + builder + .RegisterProjectionMigrator( + _configuration, + _loggerFactory) + .RegisterProjections( + context => new BuildingLatestItemProjections(context.Resolve>()), + ConnectedProjectionSettings.Default) + .RegisterProjections( + context => new BuildingUnitLatestItemProjections(context.Resolve>()), + ConnectedProjectionSettings.Default); + } } } diff --git a/src/BuildingRegistry.Projector/Infrastructure/Startup.cs b/src/BuildingRegistry.Projector/Infrastructure/Startup.cs index f653be6f4..2add9884f 100755 --- a/src/BuildingRegistry.Projector/Infrastructure/Startup.cs +++ b/src/BuildingRegistry.Projector/Infrastructure/Startup.cs @@ -11,6 +11,7 @@ namespace BuildingRegistry.Projector.Infrastructure using Be.Vlaanderen.Basisregisters.ProjectionHandling.LastChangedList; using Be.Vlaanderen.Basisregisters.Projector.ConnectedProjections; using BuildingRegistry.Projections.Extract; + using BuildingRegistry.Projections.Integration.Infrastructure; using BuildingRegistry.Projections.Legacy; using BuildingRegistry.Projections.Wfs; using BuildingRegistry.Projections.Wms; @@ -94,13 +95,33 @@ public IServiceProvider ConfigureServices(IServiceCollection services) { var connectionStrings = _configuration .GetSection("ConnectionStrings") - .GetChildren(); + .GetChildren() + .ToList(); - foreach (var connectionString in connectionStrings) + if (!_configuration.GetSection("Integration").GetValue("Enabled", false)) + { + connectionStrings = connectionStrings + .Where(x => !x.Key.StartsWith("Integration", StringComparison.OrdinalIgnoreCase)) + .ToList(); + } + + foreach (var connectionString in connectionStrings + .Where(x => !x.Value.Contains("host", StringComparison.OrdinalIgnoreCase))) + { health.AddSqlServer( connectionString.Value, name: $"sqlserver-{connectionString.Key.ToLowerInvariant()}", tags: new[] {DatabaseTag, "sql", "sqlserver"}); + } + + foreach (var connectionString in connectionStrings + .Where(x => x.Value.Contains("host", StringComparison.OrdinalIgnoreCase))) + { + health.AddNpgSql( + connectionString.Value, + name: $"npgsql-{connectionString.Key.ToLowerInvariant()}", + tags: new[] {DatabaseTag, "sql", "npgsql"}); + } health.AddDbContextCheck( $"dbcontext-{nameof(ExtractContext).ToLowerInvariant()}", @@ -125,6 +146,7 @@ public IServiceProvider ConfigureServices(IServiceCollection services) } }) .Configure(_configuration.GetSection("Extract")) + .Configure(_configuration.GetSection("Integration")) .Configure(_configuration.GetSection(FeatureToggleOptions.ConfigurationKey)) .AddSingleton(c => new UseProjectionsV2Toggle(c.GetRequiredService>().Value.UseProjectionsV2)); diff --git a/src/BuildingRegistry.Projector/Projections/ProjectionsController.cs b/src/BuildingRegistry.Projector/Projections/ProjectionsController.cs index 4aea6a9cd..34d89f566 100644 --- a/src/BuildingRegistry.Projector/Projections/ProjectionsController.cs +++ b/src/BuildingRegistry.Projector/Projections/ProjectionsController.cs @@ -21,6 +21,7 @@ public ProjectionsController( RegisterConnectionString(Schema.Legacy, configuration.GetConnectionString("LegacyProjections")); RegisterConnectionString(Schema.Extract, configuration.GetConnectionString("ExtractProjections")); RegisterConnectionString(Schema.Wms, configuration.GetConnectionString("WmsProjections")); + RegisterConnectionString(Schema.Integration, configuration.GetConnectionString("IntegrationProjections")); } } } diff --git a/src/BuildingRegistry.Projector/appsettings.json b/src/BuildingRegistry.Projector/appsettings.json index 6e69d0cff..16cf00837 100755 --- a/src/BuildingRegistry.Projector/appsettings.json +++ b/src/BuildingRegistry.Projector/appsettings.json @@ -12,7 +12,9 @@ "WfsProjections": "Server=(localdb)\\mssqllocaldb;Database=EFProviders.InMemory.BuildingRegistry;Trusted_Connection=True;TrustServerCertificate=True;", "WfsProjectionsAdmin": "Server=(localdb)\\mssqllocaldb;Database=EFProviders.InMemory.BuildingRegistry;Trusted_Connection=True;TrustServerCertificate=True;", "Syndication": "Server=(localdb)\\mssqllocaldb;Database=EFProviders.InMemory.BuildingRegistry;Trusted_Connection=True;TrustServerCertificate=True;", - "ConsumerAddress": "Server=(localdb)\\mssqllocaldb;Database=EFProviders.InMemory.BuildingRegistry;Trusted_Connection=True;TrustServerCertificate=True;" + "ConsumerAddress": "Server=(localdb)\\mssqllocaldb;Database=EFProviders.InMemory.BuildingRegistry;Trusted_Connection=True;TrustServerCertificate=True;", + "IntegrationProjections": ".", + "IntegrationProjectionsAdmin": "." }, "DataDog": { @@ -50,6 +52,12 @@ "DataVlaanderenNamespaceBuildingUnit": "https://data.vlaanderen.be/id/gebouweenheid" }, + "Integration": { + "BuildingNamespace": "https://data.vlaanderen.be/id/gebouw", + "BuildingUnitNamespace": "https://data.vlaanderen.be/id/gebouweenheid", + "Enabled": false + }, + "DistributedLock": { "Region": "eu-west-1", "TableName": "__DistributedLocks__", diff --git a/src/BuildingRegistry.Projector/paket.references b/src/BuildingRegistry.Projector/paket.references index b348639d6..f75974aad 100644 --- a/src/BuildingRegistry.Projector/paket.references +++ b/src/BuildingRegistry.Projector/paket.references @@ -4,7 +4,12 @@ Be.Vlaanderen.Basisregisters.EventHandling.Autofac Be.Vlaanderen.Basisregisters.DataDog.Tracing.Autofac Be.Vlaanderen.Basisregisters.Projector +Npgsql +Npgsql.EntityFrameworkCore.PostgreSQL +Npgsql.EntityFrameworkCore.PostgreSQL.NetTopologySuite + AspNetCore.HealthChecks.SqlServer +AspNetCore.HealthChecks.NpgSql SourceLink.Embed.AllSourceFiles SourceLink.Copy.PdbFiles diff --git a/src/EF.MigrationsHelper/EF.MigrationHelper.csproj b/src/EF.MigrationsHelper/EF.MigrationHelper.csproj index b6f202157..6bb64c316 100644 --- a/src/EF.MigrationsHelper/EF.MigrationHelper.csproj +++ b/src/EF.MigrationsHelper/EF.MigrationHelper.csproj @@ -29,6 +29,7 @@ + diff --git a/src/EF.MigrationsHelper/appsettings.json b/src/EF.MigrationsHelper/appsettings.json index 7d5194ed9..2c8faae1a 100644 --- a/src/EF.MigrationsHelper/appsettings.json +++ b/src/EF.MigrationsHelper/appsettings.json @@ -12,7 +12,8 @@ "BackOffice": "Server=(localdb)\\mssqllocaldb;Database=EFProviders.InMemory.BuildingRegistry;Trusted_Connection=True;", "ProducerProjectionsAdmin": "Server=(localdb)\\mssqllocaldb;Database=EFProviders.InMemory.BuildingRegistry;Trusted_Connection=True;", "BackOfficeProjectionsAdmin": "Server=(localdb)\\mssqllocaldb;Database=EFProviders.InMemory.BuildingRegistry;Trusted_Connection=True;", - "BuildingGrbAdmin": "Server=(localdb)\\mssqllocaldb;Database=EFProviders.InMemory.BuildingRegistry;Trusted_Connection=True;" + "BuildingGrbAdmin": "Server=(localdb)\\mssqllocaldb;Database=EFProviders.InMemory.BuildingRegistry;Trusted_Connection=True;", + "IntegrationProjectionsAdmin": "Host=vbr-integrationdb-test.postgres.database.azure.com;Database=postgres;Username=basisregisters;Password=Rj7Hnudk02zie4eA2N2jkSeIAEflbQe7e6DwN7Dhe9gDAFR7akvfvLVx2KOXo5Pkjd3BFiALyUkcbjoxI29R1zGT3dWhdJDhu0A" }, "Logging": { diff --git a/src/EF.MigrationsHelper/paket.references b/src/EF.MigrationsHelper/paket.references index 2ccd1f4fb..2751ec9c3 100644 --- a/src/EF.MigrationsHelper/paket.references +++ b/src/EF.MigrationsHelper/paket.references @@ -1,4 +1,7 @@ Microsoft.EntityFrameworkCore.Design +Npgsql.EntityFrameworkCore.PostgreSQL +Npgsql.EntityFrameworkCore.PostgreSQL.Design + SourceLink.Embed.AllSourceFiles SourceLink.Copy.PdbFiles \ No newline at end of file diff --git a/test/BuildingRegistry.Tests/BuildingRegistry.Tests.csproj b/test/BuildingRegistry.Tests/BuildingRegistry.Tests.csproj index e06103498..001a77ab1 100644 --- a/test/BuildingRegistry.Tests/BuildingRegistry.Tests.csproj +++ b/test/BuildingRegistry.Tests/BuildingRegistry.Tests.csproj @@ -9,6 +9,7 @@ + diff --git a/test/BuildingRegistry.Tests/ProjectionTests/Integration/BuildingLatestItemProjectionsTests.cs b/test/BuildingRegistry.Tests/ProjectionTests/Integration/BuildingLatestItemProjectionsTests.cs new file mode 100644 index 000000000..67301db82 --- /dev/null +++ b/test/BuildingRegistry.Tests/ProjectionTests/Integration/BuildingLatestItemProjectionsTests.cs @@ -0,0 +1,894 @@ +namespace BuildingRegistry.Tests.ProjectionTests.Integration +{ + using System.Collections.Generic; + using System.Threading.Tasks; + using AutoFixture; + using Be.Vlaanderen.Basisregisters.GrAr.Common.Pipes; + using Be.Vlaanderen.Basisregisters.ProjectionHandling.SqlStreamStore; + using Be.Vlaanderen.Basisregisters.Utilities.HexByteConvertor; + using Building; + using Building.Events; + using Fixtures; + using FluentAssertions; + using Microsoft.Extensions.Options; + using NetTopologySuite.IO; + using Projections.Integration.Building.LatestItem; + using Projections.Integration.Converters; + using Projections.Integration.Infrastructure; + using Tests.Legacy.Autofixture; + using Xunit; + + public class BuildingLatestItemProjectionsTests : IntegrationProjectionTest + { + private const string BuildingNamespace = "https://data.vlaanderen.be/id/gebouw"; + private const string BuildingUnitNamespace = "https://data.vlaanderen.be/id/gebouweenheid"; + + private readonly Fixture _fixture; + private readonly WKBReader _wkbReader = WKBReaderFactory.Create(); + + public BuildingLatestItemProjectionsTests() + { + _fixture = new Fixture(); + _fixture.Customize(new InfrastructureCustomization()); + _fixture.Customize(new WithFixedBuildingPersistentLocalId()); + _fixture.Customize(new WithBuildingStatus()); + _fixture.Customize(new WithBuildingGeometryMethod()); + _fixture.Customize(new WithValidExtendedWkbPolygon()); + _fixture.Customize(new WithBuildingUnitStatus()); + _fixture.Customize(new WithBuildingUnitFunction()); + _fixture.Customize(new WithBuildingUnitPositionGeometryMethod()); + } + + [Fact] + public async Task WhenBuildingWasMigrated() + { + var buildingWasMigrated = _fixture.Create(); + + var position = _fixture.Create(); + var metadata = new Dictionary + { + { AddEventHashPipe.HashMetadataKey, buildingWasMigrated.GetHash() }, + { Envelope.PositionMetadataKey, position } + }; + + await Sut + .Given(new Envelope(new Envelope(buildingWasMigrated, metadata))) + .Then(async ct => + { + var buildingLatestItem = await ct.BuildingLatestItems.FindAsync(buildingWasMigrated.BuildingPersistentLocalId); + buildingLatestItem.Should().NotBeNull(); + + buildingLatestItem!.Status.Should().Be(BuildingStatus.Parse(buildingWasMigrated.BuildingStatus).Map()); + buildingLatestItem.OsloStatus.Should().Be(buildingWasMigrated.BuildingStatus); + buildingLatestItem.Geometry.Should().BeEquivalentTo(_wkbReader.Read(buildingWasMigrated.ExtendedWkbGeometry.ToByteArray())); + buildingLatestItem.GeometryMethod.Should().Be(BuildingGeometryMethod.Parse(buildingWasMigrated.GeometryMethod).Map()); + buildingLatestItem.OsloGeometryMethod.Should().Be(buildingWasMigrated.GeometryMethod); + buildingLatestItem.IsRemoved.Should().Be(buildingWasMigrated.IsRemoved); + buildingLatestItem.Namespace.Should().Be(BuildingNamespace); + buildingLatestItem.PuriId.Should().Be($"{BuildingNamespace}/{buildingWasMigrated.BuildingPersistentLocalId}"); + buildingLatestItem.VersionTimestamp.Should().Be(buildingWasMigrated.Provenance.Timestamp); + }); + } + + [Fact] + public async Task WhenBuildingWasPlannedV2() + { + var buildingWasPlannedV2 = _fixture.Create(); + + var position = _fixture.Create(); + var metadata = new Dictionary + { + { AddEventHashPipe.HashMetadataKey, buildingWasPlannedV2.GetHash() }, + { Envelope.PositionMetadataKey, position } + }; + + await Sut + .Given(new Envelope(new Envelope(buildingWasPlannedV2, metadata))) + .Then(async ct => + { + var buildingLatestItem = await ct.BuildingLatestItems.FindAsync(buildingWasPlannedV2.BuildingPersistentLocalId); + buildingLatestItem.Should().NotBeNull(); + + buildingLatestItem!.Status.Should().Be(BuildingStatus.Planned.Map()); + buildingLatestItem.OsloStatus.Should().Be("Planned"); + buildingLatestItem.Geometry.Should().BeEquivalentTo(_wkbReader.Read(buildingWasPlannedV2.ExtendedWkbGeometry.ToByteArray())); + buildingLatestItem.GeometryMethod.Should().Be(BuildingGeometryMethod.Outlined.Map()); + buildingLatestItem.OsloGeometryMethod.Should().Be("Outlined"); + buildingLatestItem.IsRemoved.Should().BeFalse(); + buildingLatestItem.Namespace.Should().Be(BuildingNamespace); + buildingLatestItem.PuriId.Should().Be($"{BuildingNamespace}/{buildingWasPlannedV2.BuildingPersistentLocalId}"); + buildingLatestItem.VersionTimestamp.Should().Be(buildingWasPlannedV2.Provenance.Timestamp); + }); + } + + [Fact] + public async Task WhenUnplannedBuildingWasRealizedAndMeasured() + { + var unplannedBuildingWasRealizedAndMeasured = _fixture.Create(); + + var position = _fixture.Create(); + var metadata = new Dictionary + { + { AddEventHashPipe.HashMetadataKey, unplannedBuildingWasRealizedAndMeasured.GetHash() }, + { Envelope.PositionMetadataKey, position } + }; + + await Sut + .Given(new Envelope(new Envelope(unplannedBuildingWasRealizedAndMeasured, metadata))) + .Then(async ct => + { + var buildingLatestItem = + await ct.BuildingLatestItems.FindAsync(unplannedBuildingWasRealizedAndMeasured.BuildingPersistentLocalId); + buildingLatestItem.Should().NotBeNull(); + + buildingLatestItem!.Status.Should().Be(BuildingStatus.Realized.Map()); + buildingLatestItem.OsloStatus.Should().Be("Realized"); + buildingLatestItem.GeometryMethod.Should().Be(BuildingGeometryMethod.MeasuredByGrb.Map()); + buildingLatestItem.OsloGeometryMethod.Should().Be("MeasuredByGrb"); + buildingLatestItem.Geometry.Should() + .BeEquivalentTo(_wkbReader.Read(unplannedBuildingWasRealizedAndMeasured.ExtendedWkbGeometry.ToByteArray())); + buildingLatestItem.IsRemoved.Should().BeFalse(); + buildingLatestItem.Namespace.Should().Be(BuildingNamespace); + buildingLatestItem.PuriId.Should().Be($"{BuildingNamespace}/{unplannedBuildingWasRealizedAndMeasured.BuildingPersistentLocalId}"); + buildingLatestItem.VersionTimestamp.Should().Be(unplannedBuildingWasRealizedAndMeasured.Provenance.Timestamp); + }); + } + + [Fact] + public async Task WhenBuildingUnitWasPlanned() + { + var buildingWasPlannedV2 = _fixture.Create(); + var buildingUnitWasPlannedV2 = _fixture.Create(); + + var position = _fixture.Create(); + var buildingWasPlannedMetadata = new Dictionary + { + { AddEventHashPipe.HashMetadataKey, buildingWasPlannedV2.GetHash() }, + { Envelope.PositionMetadataKey, position } + }; + var buildingUnitWasPlannedMetadata = new Dictionary + { + { AddEventHashPipe.HashMetadataKey, buildingUnitWasPlannedV2.GetHash() }, + { Envelope.PositionMetadataKey, position + 1 } + }; + + await Sut + .Given(new Envelope(new Envelope(buildingWasPlannedV2, buildingWasPlannedMetadata)), + new Envelope(new Envelope(buildingUnitWasPlannedV2, buildingUnitWasPlannedMetadata))) + .Then(async ct => + { + var buildingLatestItem = await ct.BuildingLatestItems.FindAsync(buildingUnitWasPlannedV2.BuildingPersistentLocalId); + buildingLatestItem.Should().NotBeNull(); + + buildingLatestItem!.VersionTimestamp.Should().Be(buildingUnitWasPlannedV2.Provenance.Timestamp); + }); + } + + [Fact] + public async Task WhenCommonBuildingUnitWasAdded() + { + var buildingWasPlannedV2 = _fixture.Create(); + var commonBuildingUnitWasAddedV2 = _fixture.Create(); + + var position = _fixture.Create(); + var buildingWasPlannedMetadata = new Dictionary + { + { AddEventHashPipe.HashMetadataKey, buildingWasPlannedV2.GetHash() }, + { Envelope.PositionMetadataKey, position } + }; + var commonBuildingUnitWasAddedMetadata = new Dictionary + { + { AddEventHashPipe.HashMetadataKey, commonBuildingUnitWasAddedV2.GetHash() }, + { Envelope.PositionMetadataKey, position + 1 } + }; + + await Sut + .Given(new Envelope(new Envelope(buildingWasPlannedV2, buildingWasPlannedMetadata)), + new Envelope(new Envelope(commonBuildingUnitWasAddedV2, commonBuildingUnitWasAddedMetadata))) + .Then(async ct => + { + var buildingLatestItem = await ct.BuildingLatestItems.FindAsync(commonBuildingUnitWasAddedV2.BuildingPersistentLocalId); + buildingLatestItem.Should().NotBeNull(); + + buildingLatestItem!.VersionTimestamp.Should().Be(commonBuildingUnitWasAddedV2.Provenance.Timestamp); + }); + } + + [Fact] + public async Task WhenBuildingUnitWasRemovedV2() + { + var position = _fixture.Create(); + + var buildingWasPlannedV2 = _fixture.Create(); + var buildingWasPlannedV2Metadata = new Dictionary + { + { AddEventHashPipe.HashMetadataKey, buildingWasPlannedV2.GetHash() }, + { Envelope.PositionMetadataKey, position } + }; + + var buildingUnitWasPlannedV2 = _fixture.Create(); + var buildingUnitWasPlannedV2Metadata = new Dictionary + { + { AddEventHashPipe.HashMetadataKey, buildingUnitWasPlannedV2.GetHash() }, + { Envelope.PositionMetadataKey, position + 1 } + }; + + var buildingUnitWasRemovedV2 = _fixture.Create(); + var buildingUnitWasRemovedV2Metadata = new Dictionary + { + { AddEventHashPipe.HashMetadataKey, buildingUnitWasRemovedV2.GetHash() }, + { Envelope.PositionMetadataKey, position + 2 } + }; + + await Sut + .Given( + new Envelope(new Envelope(buildingWasPlannedV2, buildingWasPlannedV2Metadata)), + new Envelope(new Envelope(buildingUnitWasPlannedV2, buildingUnitWasPlannedV2Metadata)), + new Envelope(new Envelope(buildingUnitWasRemovedV2, buildingUnitWasRemovedV2Metadata))) + .Then(async ct => + { + var buildingLatestItem = await ct.BuildingLatestItems.FindAsync(buildingUnitWasRemovedV2.BuildingPersistentLocalId); + buildingLatestItem.Should().NotBeNull(); + + buildingLatestItem!.VersionTimestamp.Should().Be(buildingUnitWasRemovedV2.Provenance.Timestamp); + }); + } + + [Fact] + public async Task WhenBuildingUnitRemovalWasCorrected() + { + var position = _fixture.Create(); + + var buildingWasPlannedV2 = _fixture.Create(); + var buildingWasPlannedV2Metadata = new Dictionary + { + { AddEventHashPipe.HashMetadataKey, buildingWasPlannedV2.GetHash() }, + { Envelope.PositionMetadataKey, position } + }; + + var buildingUnitWasPlannedV2 = _fixture.Create(); + var buildingUnitWasPlannedV2Metadata = new Dictionary + { + { AddEventHashPipe.HashMetadataKey, buildingUnitWasPlannedV2.GetHash() }, + { Envelope.PositionMetadataKey, position + 1 } + }; + + var buildingUnitWasRemovedV2 = _fixture.Create(); + var buildingUnitWasRemovedV2Metadata = new Dictionary + { + { AddEventHashPipe.HashMetadataKey, buildingUnitWasRemovedV2.GetHash() }, + { Envelope.PositionMetadataKey, position + 2 } + }; + + var buildingUnitRemovalWasCorrected = _fixture.Create(); + var buildingUnitRemovalWasCorrectedMetadata = new Dictionary + { + { AddEventHashPipe.HashMetadataKey, buildingUnitRemovalWasCorrected.GetHash() }, + { Envelope.PositionMetadataKey, position + 3 } + }; + + await Sut + .Given( + new Envelope(new Envelope(buildingWasPlannedV2, buildingWasPlannedV2Metadata)), + new Envelope(new Envelope(buildingUnitWasPlannedV2, buildingUnitWasPlannedV2Metadata)), + new Envelope(new Envelope(buildingUnitWasRemovedV2, buildingUnitWasRemovedV2Metadata)), + new Envelope(new Envelope(buildingUnitRemovalWasCorrected, buildingUnitRemovalWasCorrectedMetadata)) + ) + .Then(async ct => + { + var buildingLatestItem = await ct.BuildingLatestItems.FindAsync(buildingUnitRemovalWasCorrected.BuildingPersistentLocalId); + buildingLatestItem.Should().NotBeNull(); + + buildingLatestItem!.IsRemoved.Should().BeFalse(); + buildingLatestItem.VersionTimestamp.Should().Be(buildingUnitRemovalWasCorrected.Provenance.Timestamp); + }); + } + + [Fact] + public async Task WhenBuildingOutlineWasChanged() + { + var position = _fixture.Create(); + + var buildingWasPlannedV2 = _fixture.Create(); + var buildingOutlineWasChanged = _fixture.Create(); + + var buildingWasPlannedMetadata = new Dictionary + { + { AddEventHashPipe.HashMetadataKey, buildingWasPlannedV2.GetHash() }, + { Envelope.PositionMetadataKey, position } + }; + + var buildingOutlineWasChangedMetadata = new Dictionary + { + { AddEventHashPipe.HashMetadataKey, buildingOutlineWasChanged.GetHash() }, + { Envelope.PositionMetadataKey, position + 1 } + }; + + await Sut + .Given( + new Envelope(new Envelope(buildingWasPlannedV2, buildingWasPlannedMetadata)), + new Envelope(new Envelope(buildingOutlineWasChanged, buildingOutlineWasChangedMetadata))) + .Then(async ct => + { + var buildingLatestItem = await ct.BuildingLatestItems.FindAsync(buildingOutlineWasChanged.BuildingPersistentLocalId); + buildingLatestItem.Should().NotBeNull(); + + buildingLatestItem!.Geometry.Should() + .BeEquivalentTo(_wkbReader.Read(buildingOutlineWasChanged.ExtendedWkbGeometryBuilding.ToByteArray())); + buildingLatestItem.GeometryMethod.Should().Be(BuildingGeometryMethod.Outlined.Map()); + buildingLatestItem.OsloGeometryMethod.Should().Be("Outlined"); + buildingLatestItem.VersionTimestamp.Should().Be(buildingOutlineWasChanged.Provenance.Timestamp); + }); + } + + [Fact] + public async Task WhenBuildingMeasurementWasChanged() + { + var position = _fixture.Create(); + + var buildingWasPlannedV2 = _fixture.Create(); + var buildingOMeasurementWasChanged = _fixture.Create(); + + var buildingWasPlannedMetadata = new Dictionary + { + { AddEventHashPipe.HashMetadataKey, buildingWasPlannedV2.GetHash() }, + { Envelope.PositionMetadataKey, position } + }; + + var buildingMeasurementWasChangedMetadata = new Dictionary + { + { AddEventHashPipe.HashMetadataKey, buildingOMeasurementWasChanged.GetHash() }, + { Envelope.PositionMetadataKey, position + 1 } + }; + + await Sut + .Given( + new Envelope(new Envelope(buildingWasPlannedV2, buildingWasPlannedMetadata)), + new Envelope(new Envelope(buildingOMeasurementWasChanged, buildingMeasurementWasChangedMetadata))) + .Then(async ct => + { + var buildingLatestItem = await ct.BuildingLatestItems.FindAsync(buildingOMeasurementWasChanged.BuildingPersistentLocalId); + buildingLatestItem.Should().NotBeNull(); + + buildingLatestItem!.Geometry.Should() + .BeEquivalentTo(_wkbReader.Read(buildingOMeasurementWasChanged.ExtendedWkbGeometryBuilding.ToByteArray())); + buildingLatestItem.GeometryMethod.Should().Be(BuildingGeometryMethod.MeasuredByGrb.Map()); + buildingLatestItem.OsloGeometryMethod.Should().Be("MeasuredByGrb"); + buildingLatestItem.VersionTimestamp.Should().Be(buildingOMeasurementWasChanged.Provenance.Timestamp); + }); + } + + [Fact] + public async Task WhenBuildingBecameUnderConstructionV2() + { + var position = _fixture.Create(); + + var buildingWasPlannedV2 = _fixture.Create(); + var buildingBecameUnderConstructionV2 = _fixture.Create(); + + var buildingWasPlannedMetadata = new Dictionary + { + { AddEventHashPipe.HashMetadataKey, buildingWasPlannedV2.GetHash() }, + { Envelope.PositionMetadataKey, position } + }; + + var buildingBecameUnderConstructionMetadata = new Dictionary + { + { AddEventHashPipe.HashMetadataKey, buildingBecameUnderConstructionV2.GetHash() }, + { Envelope.PositionMetadataKey, position + 1 } + }; + + await Sut + .Given( + new Envelope(new Envelope(buildingWasPlannedV2, buildingWasPlannedMetadata)), + new Envelope(new Envelope(buildingBecameUnderConstructionV2, + buildingBecameUnderConstructionMetadata))) + .Then(async ct => + { + var buildingLatestItem = await ct.BuildingLatestItems.FindAsync(buildingBecameUnderConstructionV2.BuildingPersistentLocalId); + buildingLatestItem.Should().NotBeNull(); + + buildingLatestItem!.Status.Should().Be(BuildingStatus.UnderConstruction.Map()); + buildingLatestItem.OsloStatus.Should().Be("UnderConstruction"); + buildingLatestItem.VersionTimestamp.Should().Be(buildingBecameUnderConstructionV2.Provenance.Timestamp); + }); + } + + [Fact] + public async Task WhenBuildingWasCorrectedFromUnderConstructionToPlanned() + { + var buildingWasPlannedV2 = _fixture.Create(); + var buildingWasCorrectedFromUnderConstructionToPlanned = _fixture.Create(); + + var position = _fixture.Create(); + + var buildingWasPlannedMetadata = new Dictionary + { + { AddEventHashPipe.HashMetadataKey, buildingWasPlannedV2.GetHash() }, + { Envelope.PositionMetadataKey, position } + }; + + var buildingWasCorrectedToPlannedMetadata = new Dictionary + { + { AddEventHashPipe.HashMetadataKey, buildingWasCorrectedFromUnderConstructionToPlanned.GetHash() }, + { Envelope.PositionMetadataKey, position + 1 } + }; + + await Sut + .Given( + new Envelope(new Envelope(buildingWasPlannedV2, buildingWasPlannedMetadata)), + new Envelope( + new Envelope(buildingWasCorrectedFromUnderConstructionToPlanned, buildingWasCorrectedToPlannedMetadata))) + .Then(async ct => + { + var buildingLatestItem = + await ct.BuildingLatestItems.FindAsync(buildingWasCorrectedFromUnderConstructionToPlanned.BuildingPersistentLocalId); + buildingLatestItem.Should().NotBeNull(); + + buildingLatestItem!.Status.Should().Be(BuildingStatus.Planned.Map()); + buildingLatestItem.OsloStatus.Should().Be("Planned"); + buildingLatestItem.VersionTimestamp.Should().Be(buildingWasCorrectedFromUnderConstructionToPlanned.Provenance.Timestamp); + }); + } + + [Fact] + public async Task WhenBuildingWasRealizedV2() + { + var buildingWasPlannedV2 = _fixture.Create(); + var buildingWasRealizedV2 = _fixture.Create(); + + var position = _fixture.Create(); + + var buildingWasPlannedMetadata = new Dictionary + { + { AddEventHashPipe.HashMetadataKey, buildingWasPlannedV2.GetHash() }, + { Envelope.PositionMetadataKey, position } + }; + + var buildingWasRealizedMetadata = new Dictionary + { + { AddEventHashPipe.HashMetadataKey, buildingWasRealizedV2.GetHash() }, + { Envelope.PositionMetadataKey, position + 1 } + }; + + await Sut + .Given( + new Envelope(new Envelope(buildingWasPlannedV2, buildingWasPlannedMetadata)), + new Envelope(new Envelope(buildingWasRealizedV2, buildingWasRealizedMetadata))) + .Then(async ct => + { + var buildingLatestItem = await ct.BuildingLatestItems.FindAsync(buildingWasRealizedV2.BuildingPersistentLocalId); + buildingLatestItem.Should().NotBeNull(); + + buildingLatestItem!.Status.Should().Be(BuildingStatus.Realized.Map()); + buildingLatestItem.OsloStatus.Should().Be("Realized"); + buildingLatestItem.VersionTimestamp.Should().Be(buildingWasRealizedV2.Provenance.Timestamp); + }); + } + + [Fact] + public async Task WhenBuildingWasCorrectedFromRealizedToUnderConstruction() + { + var buildingWasPlannedV2 = _fixture.Create(); + var buildingWasRealizedV2 = _fixture.Create(); + var buildingWasCorrectedFromRealizedToUnderConstruction = _fixture.Create(); + + var position = _fixture.Create(); + + var buildingWasPlannedMetadata = new Dictionary + { + { AddEventHashPipe.HashMetadataKey, buildingWasPlannedV2.GetHash() }, + { Envelope.PositionMetadataKey, position } + }; + var buildingWasRealizedMetadata = new Dictionary + { + { AddEventHashPipe.HashMetadataKey, buildingWasRealizedV2.GetHash() }, + { Envelope.PositionMetadataKey, position + 1 } + }; + var buildingWasCorrectedToUnderConstructionMetadata = new Dictionary + { + { AddEventHashPipe.HashMetadataKey, buildingWasCorrectedFromRealizedToUnderConstruction.GetHash() }, + { Envelope.PositionMetadataKey, position + 2 } + }; + + await Sut + .Given( + new Envelope( + new Envelope( + buildingWasPlannedV2, + buildingWasPlannedMetadata)), + new Envelope( + new Envelope( + buildingWasRealizedV2, + buildingWasRealizedMetadata)), + new Envelope( + new Envelope( + buildingWasCorrectedFromRealizedToUnderConstruction, + buildingWasCorrectedToUnderConstructionMetadata))) + .Then(async ct => + { + var buildingLatestItem = await ct.BuildingLatestItems.FindAsync(buildingWasRealizedV2.BuildingPersistentLocalId); + buildingLatestItem.Should().NotBeNull(); + + buildingLatestItem!.Status.Should().Be(BuildingStatus.UnderConstruction.Map()); + buildingLatestItem.OsloStatus.Should().Be("UnderConstruction"); + buildingLatestItem.VersionTimestamp.Should().Be(buildingWasCorrectedFromRealizedToUnderConstruction.Provenance.Timestamp); + }); + } + + [Fact] + public async Task WhenBuildingWasNotRealizedV2() + { + var buildingWasPlannedV2 = _fixture.Create(); + var buildingWasNotRealizedV2 = _fixture.Create(); + + var position = _fixture.Create(); + + var buildingWasPlannedMetadata = new Dictionary + { + { AddEventHashPipe.HashMetadataKey, buildingWasPlannedV2.GetHash() }, + { Envelope.PositionMetadataKey, position } + }; + var buildingWasNotRealizedMetadata = new Dictionary + { + { AddEventHashPipe.HashMetadataKey, buildingWasNotRealizedV2.GetHash() }, + { Envelope.PositionMetadataKey, position + 1 } + }; + await Sut + .Given(new Envelope(new Envelope(buildingWasPlannedV2, buildingWasPlannedMetadata)), + new Envelope(new Envelope(buildingWasNotRealizedV2, buildingWasNotRealizedMetadata))) + .Then(async ct => + { + var buildingLatestItem = await ct.BuildingLatestItems.FindAsync(buildingWasNotRealizedV2.BuildingPersistentLocalId); + buildingLatestItem.Should().NotBeNull(); + + buildingLatestItem!.Status.Should().Be(BuildingStatus.NotRealized.Map()); + buildingLatestItem.OsloStatus.Should().Be("NotRealized"); + buildingLatestItem.VersionTimestamp.Should().Be(buildingWasNotRealizedV2.Provenance.Timestamp); + }); + } + + [Fact] + public async Task WhenBuildingWasCorrectedFromNotRealizedToPlanned() + { + var buildingWasPlannedV2 = _fixture.Create(); + var buildingWasNotRealizedV2 = _fixture.Create(); + var buildingWasCorrectedFromNotRealizedToPlanned = _fixture.Create(); + + var position = _fixture.Create(); + + var buildingWasPlannedMetadata = new Dictionary + { + { AddEventHashPipe.HashMetadataKey, buildingWasPlannedV2.GetHash() }, + { Envelope.PositionMetadataKey, position } + }; + var buildingWasNotRealizedMetadata = new Dictionary + { + { AddEventHashPipe.HashMetadataKey, buildingWasNotRealizedV2.GetHash() }, + { Envelope.PositionMetadataKey, position + 1 } + }; + var buildingWasCorrectedToPlannedMetadata = new Dictionary + { + { AddEventHashPipe.HashMetadataKey, buildingWasCorrectedFromNotRealizedToPlanned.GetHash() }, + { Envelope.PositionMetadataKey, position + 2 } + }; + + await Sut + .Given( + new Envelope( + new Envelope( + buildingWasPlannedV2, + buildingWasPlannedMetadata)), + new Envelope( + new Envelope( + buildingWasNotRealizedV2, + buildingWasNotRealizedMetadata)), + new Envelope( + new Envelope( + buildingWasCorrectedFromNotRealizedToPlanned, + buildingWasCorrectedToPlannedMetadata))) + .Then(async ct => + { + var buildingLatestItem = await ct.BuildingLatestItems.FindAsync(buildingWasNotRealizedV2.BuildingPersistentLocalId); + buildingLatestItem.Should().NotBeNull(); + + buildingLatestItem!.Status.Should().Be(BuildingStatus.Planned.Map()); + buildingLatestItem.OsloStatus.Should().Be("Planned"); + buildingLatestItem.VersionTimestamp.Should().Be(buildingWasCorrectedFromNotRealizedToPlanned.Provenance.Timestamp); + }); + } + + [Fact] + public async Task WhenBuildingWasRemovedV2() + { + var buildingWasPlannedV2 = _fixture.Create(); + var buildingWasRemovedV2 = _fixture.Create(); + + var position = _fixture.Create(); + + var buildingWasPlannedMetadata = new Dictionary + { + { AddEventHashPipe.HashMetadataKey, buildingWasPlannedV2.GetHash() }, + { Envelope.PositionMetadataKey, position } + }; + var buildingWasRemovedMetadata = new Dictionary + { + { AddEventHashPipe.HashMetadataKey, buildingWasRemovedV2.GetHash() }, + { Envelope.PositionMetadataKey, position + 1 } + }; + + await Sut + .Given( + new Envelope( + new Envelope( + buildingWasPlannedV2, + buildingWasPlannedMetadata)), + new Envelope( + new Envelope( + buildingWasRemovedV2, + buildingWasRemovedMetadata))) + .Then(async ct => + { + var buildingLatestItem = await ct.BuildingLatestItems.FindAsync(buildingWasRemovedV2.BuildingPersistentLocalId); + buildingLatestItem.Should().NotBeNull(); + + buildingLatestItem!.IsRemoved.Should().BeTrue(); + buildingLatestItem.VersionTimestamp.Should().Be(buildingWasRemovedV2.Provenance.Timestamp); + }); + } + + [Fact] + public async Task WhenBuildingWasMeasured() + { + var buildingWasPlannedV2 = _fixture.Create(); + var buildingWasMeasured = _fixture.Create(); + + var position = _fixture.Create(); + + var buildingWasPlannedMetadata = new Dictionary + { + { AddEventHashPipe.HashMetadataKey, buildingWasPlannedV2.GetHash() }, + { Envelope.PositionMetadataKey, position } + }; + var buildingWasMeasuredMetadata = new Dictionary + { + { AddEventHashPipe.HashMetadataKey, buildingWasMeasured.GetHash() }, + { Envelope.PositionMetadataKey, position + 1 } + }; + + await Sut + .Given(new Envelope( + new Envelope( + buildingWasPlannedV2, + buildingWasPlannedMetadata)), + new Envelope( + new Envelope( + buildingWasMeasured, + buildingWasMeasuredMetadata))) + .Then(async ct => + { + var buildingLatestItem = await ct.BuildingLatestItems.FindAsync(buildingWasMeasured.BuildingPersistentLocalId); + buildingLatestItem.Should().NotBeNull(); + + buildingLatestItem!.Geometry.Should().BeEquivalentTo(_wkbReader.Read(buildingWasMeasured.ExtendedWkbGeometryBuilding.ToByteArray())); + buildingLatestItem.GeometryMethod.Should().Be(BuildingGeometryMethod.MeasuredByGrb.Map()); + buildingLatestItem.OsloGeometryMethod.Should().Be("MeasuredByGrb"); + buildingLatestItem.VersionTimestamp.Should().Be(buildingWasMeasured.Provenance.Timestamp); + }); + } + + [Fact] + public async Task WhenBuildingMeasurementWasCorrected() + { + var buildingWasPlannedV2 = _fixture.Create(); + var buildingWasMeasured = _fixture.Create(); + var buildingMeasurementWasCorrected = _fixture.Create(); + + var position = _fixture.Create(); + + var buildingWasPlannedMetadata = new Dictionary + { + { AddEventHashPipe.HashMetadataKey, buildingWasPlannedV2.GetHash() }, + { Envelope.PositionMetadataKey, position } + }; + await Sut + .Given( + new Envelope( + new Envelope( + buildingWasPlannedV2, + buildingWasPlannedMetadata)), + new Envelope( + new Envelope( + buildingWasMeasured, + new Dictionary + { + { AddEventHashPipe.HashMetadataKey, buildingMeasurementWasCorrected.GetHash() }, + { Envelope.PositionMetadataKey, position + 1 } + })), + new Envelope( + new Envelope( + buildingMeasurementWasCorrected, + new Dictionary + { + { AddEventHashPipe.HashMetadataKey, buildingMeasurementWasCorrected.GetHash() }, + { Envelope.PositionMetadataKey, position + 2 } + }))) + .Then(async ct => + { + var buildingLatestItem = await ct.BuildingLatestItems.FindAsync(buildingMeasurementWasCorrected.BuildingPersistentLocalId); + buildingLatestItem.Should().NotBeNull(); + + buildingLatestItem!.Geometry.Should().BeEquivalentTo(_wkbReader.Read(buildingMeasurementWasCorrected.ExtendedWkbGeometryBuilding.ToByteArray())); + buildingLatestItem.GeometryMethod.Should().Be(BuildingGeometryMethod.MeasuredByGrb.Map()); + buildingLatestItem.OsloGeometryMethod.Should().Be("MeasuredByGrb"); + buildingLatestItem.VersionTimestamp.Should().Be(buildingMeasurementWasCorrected.Provenance.Timestamp); + }); + } + + [Fact] + public async Task WhenBuildingWasDemolished() + { + _fixture.Customize(new WithFixedBuildingPersistentLocalId()); + _fixture.Customize(new WithFixedBuildingUnitPersistentLocalId()); + + var buildingWasPlannedV2 = _fixture.Create(); + var buildingWasDemolished = _fixture.Create(); + + var position = _fixture.Create(); + + var buildingWasPlannedMetadata = new Dictionary + { + { AddEventHashPipe.HashMetadataKey, buildingWasPlannedV2.GetHash() }, + { Envelope.PositionMetadataKey, position } + }; + var buildingWasDemolishedMetdata = new Dictionary + { + { AddEventHashPipe.HashMetadataKey, buildingWasDemolished.GetHash() }, + { Envelope.PositionMetadataKey, position + 1 } + }; + + await Sut + .Given( + new Envelope( + new Envelope( + buildingWasPlannedV2, + buildingWasPlannedMetadata)), + new Envelope( + new Envelope( + buildingWasDemolished, + buildingWasDemolishedMetdata))) + .Then(async ct => + { + var buildingLatestItem = await ct.BuildingLatestItems.FindAsync(buildingWasDemolished.BuildingPersistentLocalId); + buildingLatestItem.Should().NotBeNull(); + + buildingLatestItem!.Status.Should().Be(BuildingStatus.Retired.Map()); + buildingLatestItem.OsloStatus.Should().Be("Retired"); + buildingLatestItem.VersionTimestamp.Should().Be(buildingWasDemolished.Provenance.Timestamp); + }); + } + + [Fact] + public async Task WhenBuildingMergerWasRealized() + { + var buildingMergerWasRealized = _fixture.Create(); + + var position = _fixture.Create(); + + var buildingMergerWasRealizedMetadata = new Dictionary + { + { AddEventHashPipe.HashMetadataKey, buildingMergerWasRealized.GetHash() }, + { Envelope.PositionMetadataKey, position } + }; + + await Sut + .Given( + new Envelope( + new Envelope( + buildingMergerWasRealized, + buildingMergerWasRealizedMetadata))) + .Then(async ct => + { + var buildingLatestItem = await ct.BuildingLatestItems.FindAsync(buildingMergerWasRealized.BuildingPersistentLocalId); + buildingLatestItem.Should().NotBeNull(); + + buildingLatestItem!.BuildingPersistentLocalId.Should().Be(buildingMergerWasRealized.BuildingPersistentLocalId); + buildingLatestItem.Status.Should().Be(BuildingStatus.Realized.Map()); + buildingLatestItem.OsloStatus.Should().Be("Realized"); + buildingLatestItem.Geometry.Should().BeEquivalentTo(_wkbReader.Read(buildingMergerWasRealized.ExtendedWkbGeometry.ToByteArray())); + buildingLatestItem.GeometryMethod.Should().Be(BuildingGeometryMethod.MeasuredByGrb.Map()); + buildingLatestItem.OsloGeometryMethod.Should().Be("MeasuredByGrb"); + buildingLatestItem.VersionTimestamp.Should().Be(buildingMergerWasRealized.Provenance.Timestamp); + }); + } + + [Fact] + public async Task WhenBuildingUnitWasMoved() + { + var buildingWasPlanned = _fixture.Create(); + var buildingUnitWasPlanned = _fixture.Create(); + var buildingUnitWasMoved = _fixture.Create(); + + var position = _fixture.Create(); + + var buildingWasPlannedMetadata = new Dictionary + { + { AddEventHashPipe.HashMetadataKey, buildingWasPlanned.GetHash() }, + { Envelope.PositionMetadataKey, position } + }; + var buildingUnitWasPlannedMetadata = new Dictionary + { + { AddEventHashPipe.HashMetadataKey, buildingUnitWasPlanned.GetHash() }, + { Envelope.PositionMetadataKey, position + 1 } + }; + var buildingUnitWasMovedMetadata = new Dictionary + { + { AddEventHashPipe.HashMetadataKey, buildingUnitWasMoved.GetHash() }, + { Envelope.PositionMetadataKey, position + 2 } + }; + + await Sut + .Given( + new Envelope(new Envelope(buildingWasPlanned, buildingWasPlannedMetadata)), + new Envelope(new Envelope(buildingUnitWasPlanned, buildingUnitWasPlannedMetadata)), + new Envelope(new Envelope(buildingUnitWasMoved, buildingUnitWasMovedMetadata))) + .Then(async ct => + { + var buildingLatestItem = await ct.BuildingLatestItems.FindAsync(buildingUnitWasMoved.BuildingPersistentLocalId); + buildingLatestItem.Should().NotBeNull(); + + buildingLatestItem!.BuildingPersistentLocalId.Should().Be(buildingUnitWasMoved.BuildingPersistentLocalId); + buildingLatestItem.VersionTimestamp.Should().Be(buildingUnitWasMoved.Provenance.Timestamp); + }); + } + + [Fact] + public async Task WhenBuildingWasMerged() + { + _fixture.Customize(new WithFixedBuildingPersistentLocalId()); + + var buildingWasPlanned = _fixture.Create(); + var buildingWasMerged = _fixture.Create(); + + var position = _fixture.Create(); + + var buildingWasPlannedMetadata = new Dictionary + { + { AddEventHashPipe.HashMetadataKey, buildingWasPlanned.GetHash() }, + { Envelope.PositionMetadataKey, position } + }; + var buildingWasMergedMetadata = new Dictionary + { + { AddEventHashPipe.HashMetadataKey, buildingWasMerged.GetHash() }, + { Envelope.PositionMetadataKey, position + 1 } + }; + + await Sut + .Given( + new Envelope( + new Envelope( + buildingWasPlanned, + buildingWasPlannedMetadata)), + new Envelope( + new Envelope( + buildingWasMerged, + buildingWasMergedMetadata))) + .Then(async ct => + { + var buildingLatestItem = await ct.BuildingLatestItems.FindAsync(buildingWasMerged.BuildingPersistentLocalId); + buildingLatestItem.Should().NotBeNull(); + + buildingLatestItem!.Status.Should().Be(BuildingStatus.Retired.Map()); + buildingLatestItem.OsloStatus.Should().Be("Retired"); + buildingLatestItem.VersionTimestamp.Should().Be(buildingWasMerged.Provenance.Timestamp); + }); + } + + protected override BuildingLatestItemProjections CreateProjection() => + new BuildingLatestItemProjections(new OptionsWrapper(new IntegrationOptions + { + BuildingNamespace = BuildingNamespace, + BuildingUnitNamespace = BuildingUnitNamespace + })); + } +} diff --git a/test/BuildingRegistry.Tests/ProjectionTests/Integration/BuildingUnitLatestItemProjectionsTests.cs b/test/BuildingRegistry.Tests/ProjectionTests/Integration/BuildingUnitLatestItemProjectionsTests.cs new file mode 100644 index 000000000..01117ea34 --- /dev/null +++ b/test/BuildingRegistry.Tests/ProjectionTests/Integration/BuildingUnitLatestItemProjectionsTests.cs @@ -0,0 +1,1491 @@ +namespace BuildingRegistry.Tests.ProjectionTests.Integration +{ + using System.Collections.Generic; + using System.Linq; + using System.Threading.Tasks; + using AutoFixture; + using Be.Vlaanderen.Basisregisters.GrAr.Common.Pipes; + using Be.Vlaanderen.Basisregisters.GrAr.Provenance; + using Be.Vlaanderen.Basisregisters.ProjectionHandling.SqlStreamStore; + using Be.Vlaanderen.Basisregisters.Utilities.HexByteConvertor; + using Building; + using Building.Events; + using Extensions; + using Fixtures; + using FluentAssertions; + using Microsoft.Extensions.Options; + using NetTopologySuite.IO; + using Projections.Integration.BuildingUnit.LatestItem; + using Projections.Integration.Converters; + using Projections.Integration.Infrastructure; + using Tests.Legacy.Autofixture; + using Xunit; + + public class BuildingUnitLatestItemProjectionsTests : IntegrationProjectionTest + { + private const string BuildingNamespace = "https://data.vlaanderen.be/id/gebouw"; + private const string BuildingUnitNamespace = "https://data.vlaanderen.be/id/gebouweenheid"; + + private readonly Fixture _fixture; + private readonly WKBReader _wkbReader = WKBReaderFactory.Create(); + + public BuildingUnitLatestItemProjectionsTests() + { + _fixture = new Fixture(); + _fixture.Customizations.Add(new WithUniqueInteger()); + _fixture.Customize(new InfrastructureCustomization()); + _fixture.Customize(new WithBuildingStatus()); + _fixture.Customize(new WithBuildingGeometryMethod()); + _fixture.Customize(new WithValidExtendedWkbPolygon()); + _fixture.Customize(new WithBuildingUnitStatus()); + _fixture.Customize(new WithBuildingUnitFunction()); + _fixture.Customize(new WithBuildingUnitPositionGeometryMethod()); + } + + [Theory] + [InlineData("Planned")] + [InlineData("UnderConstruction")] + [InlineData("Realized")] + [InlineData("Retired")] + [InlineData("NotRealized")] + public async Task WhenBuildingWasMigrated(string buildingStatus) + { + _fixture.Register(() => BuildingStatus.Parse(buildingStatus)); + + var buildingWasMigrated = _fixture.Create(); + + var position = _fixture.Create(); + var metadata = new Dictionary + { + { AddEventHashPipe.HashMetadataKey, buildingWasMigrated.GetHash() }, + { Envelope.PositionMetadataKey, position } + }; + + await Sut + .Given(new Envelope(new Envelope(buildingWasMigrated, metadata))) + .Then(context => + { + var buildingUnits = context.BuildingUnitLatestItems + .Where(x => x.BuildingPersistentLocalId == buildingWasMigrated.BuildingPersistentLocalId) + .ToList(); + + foreach (var buildingUnit in buildingWasMigrated.BuildingUnits) + { + var buildingUnitLatestItem = buildingUnits + .Single(x => x.BuildingUnitPersistentLocalId == buildingUnit.BuildingUnitPersistentLocalId); + + buildingUnitLatestItem.BuildingPersistentLocalId.Should().Be(buildingWasMigrated.BuildingPersistentLocalId); + buildingUnitLatestItem.Status.Should().Be(BuildingUnitStatus.Parse(buildingUnit.Status).Map()); + buildingUnitLatestItem.OsloStatus.Should().Be(buildingUnit.Status); + buildingUnitLatestItem.Function.Should().Be(BuildingUnitFunction.Parse(buildingUnit.Function).Map()); + buildingUnitLatestItem.OsloFunction.Should().Be(buildingUnit.Function); + buildingUnitLatestItem.GeometryMethod.Should() + .Be(BuildingUnitPositionGeometryMethod.Parse(buildingUnit.GeometryMethod).Map()); + buildingUnitLatestItem.Geometry.Should().BeEquivalentTo(_wkbReader.Read(buildingUnit.ExtendedWkbGeometry.ToByteArray())); + buildingUnitLatestItem.HasDeviation.Should().BeFalse(); + buildingUnitLatestItem.IsRemoved.Should().Be(buildingUnit.IsRemoved); + buildingUnitLatestItem.Namespace.Should().Be(BuildingUnitNamespace); + buildingUnitLatestItem.PuriId.Should().Be($"{BuildingUnitNamespace}/{buildingUnitLatestItem.BuildingUnitPersistentLocalId}"); + buildingUnitLatestItem.VersionTimestamp.Should().Be(buildingWasMigrated.Provenance.Timestamp); + + var buildingUnitAddresses = context.BuildingUnitAddresses + .Where(x => x.BuildingUnitPersistentLocalId == buildingUnitLatestItem.BuildingUnitPersistentLocalId) + .ToList(); + + buildingUnitAddresses.Should().HaveCount(buildingUnit.AddressPersistentLocalIds.Count); + foreach (var addressPersistentLocalId in buildingUnit.AddressPersistentLocalIds) + { + buildingUnitAddresses.SingleOrDefault(x => x.AddressPersistentLocalId == addressPersistentLocalId) + .Should().NotBeNull(); + } + } + + return Task.CompletedTask; + }); + } + + [Fact] + public async Task WhenBuildingOutlineWasChanged() + { + _fixture.Customize(new WithFixedBuildingPersistentLocalId()); + _fixture.Customize(new WithFixedBuildingUnitPersistentLocalId()); + + var buildingUnitWasPlannedV2 = _fixture.Create(); + var buildingOutlineWasChanged = _fixture.Create(); + + var position = _fixture.Create(); + + var buildingUnitWasPlannedMetadata = new Dictionary + { + { AddEventHashPipe.HashMetadataKey, buildingUnitWasPlannedV2.GetHash() }, + { Envelope.PositionMetadataKey, position } + }; + var buildingOutLineWasChangedMetadata = new Dictionary + { + { AddEventHashPipe.HashMetadataKey, buildingOutlineWasChanged.GetHash() }, + { Envelope.PositionMetadataKey, position + 1 } + }; + + await Sut + .Given( + new Envelope(new Envelope(buildingUnitWasPlannedV2, buildingUnitWasPlannedMetadata)), + new Envelope(new Envelope(buildingOutlineWasChanged, buildingOutLineWasChangedMetadata))) + .Then(async context => + { + var buildingUnitLatestItem = + await context.BuildingUnitLatestItems.FindAsync(buildingUnitWasPlannedV2.BuildingUnitPersistentLocalId); + buildingUnitLatestItem.Should().NotBeNull(); + + buildingUnitLatestItem!.GeometryMethod.Should().Be(BuildingUnitPositionGeometryMethod.DerivedFromObject.Map()); + buildingUnitLatestItem.Geometry.Should().BeEquivalentTo( + _wkbReader.Read(buildingOutlineWasChanged.ExtendedWkbGeometryBuildingUnits!.ToByteArray())); + buildingUnitLatestItem.VersionTimestamp.Should().Be(buildingOutlineWasChanged.Provenance.Timestamp); + }); + } + + [Fact] + public async Task WhenBuildingWasMeasured() + { + _fixture.Customize(new WithFixedBuildingPersistentLocalId()); + _fixture.Customize(new WithFixedBuildingUnitPersistentLocalId()); + + var buildingUnitWasPlannedV2 = _fixture.Create(); + var buildingWasMeasured = _fixture.Create(); + + var position = _fixture.Create(); + + var buildingUnitWasPlannedMetadata = new Dictionary + { + { AddEventHashPipe.HashMetadataKey, buildingUnitWasPlannedV2.GetHash() }, + { Envelope.PositionMetadataKey, position } + }; + var buildingWasMeasuredMetadata = new Dictionary + { + { AddEventHashPipe.HashMetadataKey, buildingWasMeasured.GetHash() }, + { Envelope.PositionMetadataKey, position + 1 } + }; + await Sut + .Given( + new Envelope(new Envelope(buildingUnitWasPlannedV2, buildingUnitWasPlannedMetadata)), + new Envelope(new Envelope(buildingWasMeasured, buildingWasMeasuredMetadata))) + .Then(async context => + { + var buildingUnitLatestItem = + await context.BuildingUnitLatestItems.FindAsync(buildingUnitWasPlannedV2.BuildingUnitPersistentLocalId); + buildingUnitLatestItem.Should().NotBeNull(); + + buildingUnitLatestItem!.GeometryMethod.Should().Be(BuildingUnitPositionGeometryMethod.DerivedFromObject.Map()); + buildingUnitLatestItem.Geometry.Should().BeEquivalentTo( + _wkbReader.Read(buildingWasMeasured.ExtendedWkbGeometryBuildingUnits!.ToByteArray())); + buildingUnitLatestItem.VersionTimestamp.Should().Be(buildingWasMeasured.Provenance.Timestamp); + }); + } + + [Fact] + public async Task WhenBuildingMeasurementWasCorrected() + { + _fixture.Customize(new WithFixedBuildingPersistentLocalId()); + _fixture.Customize(new WithFixedBuildingUnitPersistentLocalId()); + + var buildingUnitWasPlannedV2 = _fixture.Create(); + var buildingMeasurementWasCorrected = _fixture.Create(); + + var position = _fixture.Create(); + + var buildingUnitWasPlannedMetadata = new Dictionary + { + { AddEventHashPipe.HashMetadataKey, buildingUnitWasPlannedV2.GetHash() }, + { Envelope.PositionMetadataKey, position } + }; + var buildingMeasurementWasCorrectedMetadata = new Dictionary + { + { AddEventHashPipe.HashMetadataKey, buildingMeasurementWasCorrected.GetHash() }, + { Envelope.PositionMetadataKey, position + 1 } + }; + + await Sut + .Given( + new Envelope(new Envelope(buildingUnitWasPlannedV2, buildingUnitWasPlannedMetadata)), + new Envelope(new Envelope(buildingMeasurementWasCorrected, buildingMeasurementWasCorrectedMetadata))) + .Then(async context => + { + var buildingUnitLatestItem = + await context.BuildingUnitLatestItems.FindAsync(buildingUnitWasPlannedV2.BuildingUnitPersistentLocalId); + buildingUnitLatestItem.Should().NotBeNull(); + + buildingUnitLatestItem!.GeometryMethod.Should().Be(BuildingUnitPositionGeometryMethod.DerivedFromObject.Map()); + buildingUnitLatestItem.Geometry.Should() + .BeEquivalentTo(_wkbReader.Read(buildingMeasurementWasCorrected.ExtendedWkbGeometryBuildingUnits!.ToByteArray())); + buildingUnitLatestItem.VersionTimestamp.Should().Be(buildingMeasurementWasCorrected.Provenance.Timestamp); + }); + } + + [Fact] + public async Task WhenBuildingMeasurementWasChanged() + { + _fixture.Customize(new WithFixedBuildingPersistentLocalId()); + _fixture.Customize(new WithFixedBuildingUnitPersistentLocalId()); + + var buildingUnitWasPlannedV2 = _fixture.Create(); + var buildingMeasurementWasChanged = _fixture.Create(); + + var position = _fixture.Create(); + + var buildingUnitWasPlannedMetadata = new Dictionary + { + { AddEventHashPipe.HashMetadataKey, buildingUnitWasPlannedV2.GetHash() }, + { Envelope.PositionMetadataKey, position } + }; + var buildingMeasurementWasChangedMetadata = new Dictionary + { + { AddEventHashPipe.HashMetadataKey, buildingMeasurementWasChanged.GetHash() }, + { Envelope.PositionMetadataKey, position + 1 } + }; + + await Sut + .Given( + new Envelope(new Envelope(buildingUnitWasPlannedV2, buildingUnitWasPlannedMetadata)), + new Envelope(new Envelope(buildingMeasurementWasChanged, buildingMeasurementWasChangedMetadata))) + .Then(async context => + { + var buildingUnitLatestItem = await context.BuildingUnitLatestItems.FindAsync(buildingUnitWasPlannedV2 + .BuildingUnitPersistentLocalId); + buildingUnitLatestItem.Should().NotBeNull(); + + buildingUnitLatestItem!.GeometryMethod.Should().Be(BuildingUnitPositionGeometryMethod.DerivedFromObject.Map()); + buildingUnitLatestItem.Geometry.Should().BeEquivalentTo( + _wkbReader.Read(buildingMeasurementWasChanged.ExtendedWkbGeometryBuildingUnits!.ToByteArray())); + buildingUnitLatestItem.VersionTimestamp.Should().Be(buildingMeasurementWasChanged.Provenance.Timestamp); + }); + } + + [Fact] + public async Task WhenBuildingUnitWasPlannedV2() + { + _fixture.Customize(new WithFixedBuildingPersistentLocalId()); + _fixture.Customize(new WithFixedBuildingUnitPersistentLocalId()); + + var buildingUnitWasPlannedV2 = _fixture.Create(); + + var position = _fixture.Create(); + var metadata = new Dictionary + { + { AddEventHashPipe.HashMetadataKey, buildingUnitWasPlannedV2.GetHash() }, + { Envelope.PositionMetadataKey, position } + }; + + await Sut + .Given(new Envelope(new Envelope(buildingUnitWasPlannedV2, metadata))) + .Then(async context => + { + var buildingUnitLatestItem = await context.BuildingUnitLatestItems.FindAsync(buildingUnitWasPlannedV2.BuildingUnitPersistentLocalId); + buildingUnitLatestItem.Should().NotBeNull(); + + buildingUnitLatestItem!.BuildingPersistentLocalId.Should().Be(buildingUnitWasPlannedV2.BuildingPersistentLocalId); + buildingUnitLatestItem.Geometry.Should().BeEquivalentTo( + _wkbReader.Read(buildingUnitWasPlannedV2.ExtendedWkbGeometry.ToByteArray())); + buildingUnitLatestItem.GeometryMethod.Should() + .Be(BuildingUnitPositionGeometryMethod.Parse(buildingUnitWasPlannedV2.GeometryMethod).Map()); + buildingUnitLatestItem.Function.Should().Be(BuildingUnitFunction.Parse(buildingUnitWasPlannedV2.Function).Map()); + buildingUnitLatestItem.OsloFunction.Should().Be(buildingUnitWasPlannedV2.Function); + buildingUnitLatestItem.Status.Should().Be(BuildingUnitStatus.Planned.Map()); + buildingUnitLatestItem.OsloStatus.Should().Be("Planned"); + buildingUnitLatestItem.HasDeviation.Should().Be(buildingUnitWasPlannedV2.HasDeviation); + buildingUnitLatestItem.IsRemoved.Should().BeFalse(); + buildingUnitLatestItem.VersionTimestamp.Should().Be(buildingUnitWasPlannedV2.Provenance.Timestamp); + }); + } + + [Fact] + public async Task WhenBuildingUnitWasRealizedV2() + { + _fixture.Customize(new WithFixedBuildingPersistentLocalId()); + _fixture.Customize(new WithFixedBuildingUnitPersistentLocalId()); + + var buildingUnitWasPlannedV2 = _fixture.Create(); + var buildingUnitWasRealizedV2 = _fixture.Create(); + + var position = _fixture.Create(); + + var buildingUnitWasPlannedMetadata = new Dictionary + { + { AddEventHashPipe.HashMetadataKey, buildingUnitWasPlannedV2.GetHash() }, + { Envelope.PositionMetadataKey, position } + }; + var buildingUnitWasRealizedMetadata = new Dictionary + { + { AddEventHashPipe.HashMetadataKey, buildingUnitWasRealizedV2.GetHash() }, + { Envelope.PositionMetadataKey, position + 1 } + }; + + await Sut + .Given( + new Envelope(new Envelope(buildingUnitWasPlannedV2, buildingUnitWasPlannedMetadata)), + new Envelope(new Envelope(buildingUnitWasRealizedV2, buildingUnitWasRealizedMetadata))) + .Then(async context => + { + var buildingUnitLatestItem = + await context.BuildingUnitLatestItems.FindAsync(buildingUnitWasRealizedV2.BuildingUnitPersistentLocalId); + buildingUnitLatestItem.Should().NotBeNull(); + + buildingUnitLatestItem!.Status.Should().Be(BuildingUnitStatus.Realized.Map()); + buildingUnitLatestItem.OsloStatus.Should().Be("Realized"); + buildingUnitLatestItem.VersionTimestamp.Should().Be(buildingUnitWasRealizedV2.Provenance.Timestamp); + }); + } + + [Fact] + public async Task WhenBuildingUnitWasRealizedBecauseBuildingWasRealized() + { + _fixture.Customize(new WithFixedBuildingPersistentLocalId()); + _fixture.Customize(new WithFixedBuildingUnitPersistentLocalId()); + + var buildingUnitWasPlannedV2 = _fixture.Create(); + var buildingUnitWasRealized = _fixture.Create(); + + var position = _fixture.Create(); + + var buildingUnitWasPlannedMetadata = new Dictionary + { + { AddEventHashPipe.HashMetadataKey, buildingUnitWasPlannedV2.GetHash() }, + { Envelope.PositionMetadataKey, position } + }; + var buildingUnitWasRealizedMetadata = new Dictionary + { + { AddEventHashPipe.HashMetadataKey, buildingUnitWasRealized.GetHash() }, + { Envelope.PositionMetadataKey, position + 1 } + }; + + await Sut + .Given( + new Envelope(new Envelope(buildingUnitWasPlannedV2, buildingUnitWasPlannedMetadata)), + new Envelope(new Envelope(buildingUnitWasRealized, buildingUnitWasRealizedMetadata))) + .Then(async context => + { + var buildingUnitLatestItem = + await context.BuildingUnitLatestItems.FindAsync(buildingUnitWasRealized.BuildingUnitPersistentLocalId); + buildingUnitLatestItem.Should().NotBeNull(); + + buildingUnitLatestItem!.Status.Should().Be(BuildingUnitStatus.Realized.Map()); + buildingUnitLatestItem.OsloStatus.Should().Be("Realized"); + buildingUnitLatestItem.VersionTimestamp.Should().Be(buildingUnitWasRealized.Provenance.Timestamp); + }); + } + + [Fact] + public async Task WhenBuildingUnitWasCorrectedFromRealizedToPlanned() + { + _fixture.Customize(new WithFixedBuildingPersistentLocalId()); + _fixture.Customize(new WithFixedBuildingUnitPersistentLocalId()); + + var buildingUnitWasPlannedV2 = _fixture.Create(); + var buildingUnitWasRealizedV2 = _fixture.Create(); + var buildingUnitWasCorrectedFromRealizedToPlanned = _fixture.Create(); + + var position = _fixture.Create(); + + var buildingUnitWasPlannedMetadata = new Dictionary + { + { AddEventHashPipe.HashMetadataKey, buildingUnitWasPlannedV2.GetHash() }, + { Envelope.PositionMetadataKey, position } + }; + var buildingUnitWasRealizedMetadata = new Dictionary + { + { AddEventHashPipe.HashMetadataKey, buildingUnitWasRealizedV2.GetHash() }, + { Envelope.PositionMetadataKey, position + 1 } + }; + var buildingUnitWasCorrectedToPlannedMetadata = new Dictionary + { + { AddEventHashPipe.HashMetadataKey, buildingUnitWasCorrectedFromRealizedToPlanned.GetHash() }, + { Envelope.PositionMetadataKey, position + 2 } + }; + + await Sut + .Given( + new Envelope(new Envelope(buildingUnitWasPlannedV2, buildingUnitWasPlannedMetadata)), + new Envelope(new Envelope(buildingUnitWasRealizedV2, buildingUnitWasRealizedMetadata)), + new Envelope( + new Envelope(buildingUnitWasCorrectedFromRealizedToPlanned, buildingUnitWasCorrectedToPlannedMetadata))) + .Then(async context => + { + var buildingUnitLatestItem = + await context.BuildingUnitLatestItems.FindAsync(buildingUnitWasCorrectedFromRealizedToPlanned.BuildingUnitPersistentLocalId); + buildingUnitLatestItem.Should().NotBeNull(); + + buildingUnitLatestItem!.Status.Should().Be(BuildingUnitStatus.Planned.Map()); + buildingUnitLatestItem.OsloStatus.Should().Be("Planned"); + buildingUnitLatestItem.VersionTimestamp.Should().Be(buildingUnitWasCorrectedFromRealizedToPlanned.Provenance.Timestamp); + }); + } + + [Fact] + public async Task WhenBuildingUnitWasCorrectedFromRealizedToPlannedBecauseBuildingWasCorrected() + { + _fixture.Customize(new WithFixedBuildingPersistentLocalId()); + _fixture.Customize(new WithFixedBuildingUnitPersistentLocalId()); + + var buildingUnitWasPlannedV2 = _fixture.Create(); + var buildingUnitWasRealizedV2 = _fixture.Create(); + var buildingUnitWasCorrectedToPlanned = _fixture.Create(); + + var position = _fixture.Create(); + + var buildingUnitWasPlannedMetadata = new Dictionary + { + { AddEventHashPipe.HashMetadataKey, buildingUnitWasPlannedV2.GetHash() }, + { Envelope.PositionMetadataKey, position } + }; + var buildingUnitWasRealizedMetadata = new Dictionary + { + { AddEventHashPipe.HashMetadataKey, buildingUnitWasRealizedV2.GetHash() }, + { Envelope.PositionMetadataKey, position + 1 } + }; + var buildingUnitWasCorrectedToPlannedMetadata = new Dictionary + { + { AddEventHashPipe.HashMetadataKey, buildingUnitWasCorrectedToPlanned.GetHash() }, + { Envelope.PositionMetadataKey, position + 2 } + }; + + await Sut + .Given( + new Envelope(new Envelope(buildingUnitWasPlannedV2, buildingUnitWasPlannedMetadata)), + new Envelope(new Envelope(buildingUnitWasRealizedV2, buildingUnitWasRealizedMetadata)), + new Envelope( + new Envelope(buildingUnitWasCorrectedToPlanned, buildingUnitWasCorrectedToPlannedMetadata))) + .Then(async context => + { + var buildingUnitLatestItem = + await context.BuildingUnitLatestItems.FindAsync(buildingUnitWasCorrectedToPlanned.BuildingUnitPersistentLocalId); + buildingUnitLatestItem.Should().NotBeNull(); + + buildingUnitLatestItem!.Status.Should().Be(BuildingUnitStatus.Planned.Map()); + buildingUnitLatestItem.OsloStatus.Should().Be("Planned"); + buildingUnitLatestItem.VersionTimestamp.Should().Be(buildingUnitWasCorrectedToPlanned.Provenance.Timestamp); + }); + } + + [Fact] + public async Task WhenBuildingUnitWasNotRealizedV2() + { + _fixture.Customize(new WithFixedBuildingPersistentLocalId()); + _fixture.Customize(new WithFixedBuildingUnitPersistentLocalId()); + + var buildingUnitWasPlannedV2 = _fixture.Create(); + var buildingUnitWasNotRealizedV2 = _fixture.Create(); + + var position = _fixture.Create(); + + var buildingUnitWasPlannedMetadata = new Dictionary + { + { AddEventHashPipe.HashMetadataKey, buildingUnitWasPlannedV2.GetHash() }, + { Envelope.PositionMetadataKey, position } + }; + var buildingUnitWasNotRealizedMetadata = new Dictionary + { + { AddEventHashPipe.HashMetadataKey, buildingUnitWasNotRealizedV2.GetHash() }, + { Envelope.PositionMetadataKey, position + 1 } + }; + + await Sut + .Given( + new Envelope(new Envelope(buildingUnitWasPlannedV2, buildingUnitWasPlannedMetadata)), + new Envelope(new Envelope(buildingUnitWasNotRealizedV2, buildingUnitWasNotRealizedMetadata))) + .Then(async context => + { + var buildingUnitLatestItem = + await context.BuildingUnitLatestItems.FindAsync(buildingUnitWasNotRealizedV2.BuildingUnitPersistentLocalId); + buildingUnitLatestItem.Should().NotBeNull(); + + buildingUnitLatestItem!.Status.Should().Be(BuildingUnitStatus.NotRealized.Map()); + buildingUnitLatestItem.OsloStatus.Should().Be("NotRealized"); + buildingUnitLatestItem.VersionTimestamp.Should().Be(buildingUnitWasNotRealizedV2.Provenance.Timestamp); + }); + } + + [Fact] + public async Task WhenBuildingUnitWasNotRealizedBecauseBuildingWasNotRealized() + { + _fixture.Customize(new WithFixedBuildingPersistentLocalId()); + _fixture.Customize(new WithFixedBuildingUnitPersistentLocalId()); + + var buildingUnitWasPlannedV2 = _fixture.Create(); + var buildingUnitWasNotRealized = _fixture.Create(); + + var position = _fixture.Create(); + + var buildingUnitWasPlannedMetadata = new Dictionary + { + { AddEventHashPipe.HashMetadataKey, buildingUnitWasPlannedV2.GetHash() }, + { Envelope.PositionMetadataKey, position } + }; + var buildingUnitWasNotRealizedMetadata = new Dictionary + { + { AddEventHashPipe.HashMetadataKey, buildingUnitWasNotRealized.GetHash() }, + { Envelope.PositionMetadataKey, position + 1 } + }; + + await Sut + .Given( + new Envelope(new Envelope(buildingUnitWasPlannedV2, buildingUnitWasPlannedMetadata)), + new Envelope( + new Envelope(buildingUnitWasNotRealized, buildingUnitWasNotRealizedMetadata))) + .Then(async context => + { + var buildingUnitLatestItem = + await context.BuildingUnitLatestItems.FindAsync(buildingUnitWasNotRealized.BuildingUnitPersistentLocalId); + buildingUnitLatestItem.Should().NotBeNull(); + + buildingUnitLatestItem!.Status.Should().Be(BuildingUnitStatus.NotRealized.Map()); + buildingUnitLatestItem.OsloStatus.Should().Be("NotRealized"); + buildingUnitLatestItem.VersionTimestamp.Should().Be(buildingUnitWasNotRealized.Provenance.Timestamp); + }); + } + + [Fact] + public async Task WhenBuildingUnitWasCorrectedFromNotRealizedToPlanned() + { + _fixture.Customize(new WithFixedBuildingPersistentLocalId()); + _fixture.Customize(new WithFixedBuildingUnitPersistentLocalId()); + + var buildingUnitWasPlannedV2 = _fixture.Create(); + var buildingUnitWasNotRealizedV2 = _fixture.Create(); + var buildingUnitWasCorrectedToPlanned = _fixture.Create(); + + var position = _fixture.Create(); + + var buildingUnitWasPlannedMetadata = new Dictionary + { + { AddEventHashPipe.HashMetadataKey, buildingUnitWasPlannedV2.GetHash() }, + { Envelope.PositionMetadataKey, position } + }; + var buildingUnitWasNotRealizedMetadata = new Dictionary + { + { AddEventHashPipe.HashMetadataKey, buildingUnitWasNotRealizedV2.GetHash() }, + { Envelope.PositionMetadataKey, position + 1 } + }; + var buildingUnitWasCorrectedToPlannedMetadata = new Dictionary + { + { AddEventHashPipe.HashMetadataKey, buildingUnitWasCorrectedToPlanned.GetHash() }, + { Envelope.PositionMetadataKey, position + 2 } + }; + + await Sut + .Given( + new Envelope(new Envelope(buildingUnitWasPlannedV2, buildingUnitWasPlannedMetadata)), + new Envelope(new Envelope(buildingUnitWasNotRealizedV2, buildingUnitWasNotRealizedMetadata)), + new Envelope( + new Envelope(buildingUnitWasCorrectedToPlanned, buildingUnitWasCorrectedToPlannedMetadata))) + .Then(async context => + { + var buildingUnitLatestItem = + await context.BuildingUnitLatestItems.FindAsync(buildingUnitWasCorrectedToPlanned.BuildingUnitPersistentLocalId); + buildingUnitLatestItem.Should().NotBeNull(); + + buildingUnitLatestItem!.Status.Should().Be(BuildingUnitStatus.Planned.Map()); + buildingUnitLatestItem.OsloStatus.Should().Be("Planned"); + buildingUnitLatestItem.VersionTimestamp.Should().Be(buildingUnitWasCorrectedToPlanned.Provenance.Timestamp); + }); + } + + [Fact] + public async Task WhenBuildingUnitWasRetiredV2() + { + _fixture.Customize(new WithFixedBuildingPersistentLocalId()); + _fixture.Customize(new WithFixedBuildingUnitPersistentLocalId()); + + var buildingUnitWasPlannedV2 = _fixture.Create(); + var buildingUnitWasRealizedV2 = _fixture.Create(); + var buildingUnitWasRetiredV2 = _fixture.Create(); + + var position = _fixture.Create(); + + var buildingUnitWasPlannedMetadata = new Dictionary + { + { AddEventHashPipe.HashMetadataKey, buildingUnitWasPlannedV2.GetHash() }, + { Envelope.PositionMetadataKey, position } + }; + var buildingUnitWasRealizedMetadata = new Dictionary + { + { AddEventHashPipe.HashMetadataKey, buildingUnitWasRealizedV2.GetHash() }, + { Envelope.PositionMetadataKey, position + 1 } + }; + var buildingUnitWasRetiredMetadata = new Dictionary + { + { AddEventHashPipe.HashMetadataKey, buildingUnitWasRetiredV2.GetHash() }, + { Envelope.PositionMetadataKey, position + 2 } + }; + + await Sut + .Given( + new Envelope(new Envelope(buildingUnitWasPlannedV2, buildingUnitWasPlannedMetadata)), + new Envelope(new Envelope(buildingUnitWasRealizedV2, buildingUnitWasRealizedMetadata)), + new Envelope(new Envelope(buildingUnitWasRetiredV2, buildingUnitWasRetiredMetadata))) + .Then(async context => + { + var buildingUnitLatestItem = + await context.BuildingUnitLatestItems.FindAsync(buildingUnitWasRetiredV2.BuildingUnitPersistentLocalId); + buildingUnitLatestItem.Should().NotBeNull(); + + buildingUnitLatestItem!.Status.Should().Be(BuildingUnitStatus.Retired.Map()); + buildingUnitLatestItem.OsloStatus.Should().Be("Retired"); + buildingUnitLatestItem.VersionTimestamp.Should().Be(buildingUnitWasRetiredV2.Provenance.Timestamp); + }); + } + + [Fact] + public async Task WhenBuildingUnitWasCorrectedFromRetiredToRealized() + { + _fixture.Customize(new WithFixedBuildingPersistentLocalId()); + _fixture.Customize(new WithFixedBuildingUnitPersistentLocalId()); + + var buildingUnitWasPlannedV2 = _fixture.Create(); + var buildingUnitWasRetiredV2 = _fixture.Create(); + var buildingUnitWasCorrectedToRealized = _fixture.Create(); + + var position = _fixture.Create(); + + var buildingUnitWasPlannedMetadata = new Dictionary + { + { AddEventHashPipe.HashMetadataKey, buildingUnitWasPlannedV2.GetHash() }, + { Envelope.PositionMetadataKey, position } + }; + var buildingUnitWasRetiredMetadata = new Dictionary + { + { AddEventHashPipe.HashMetadataKey, buildingUnitWasRetiredV2.GetHash() }, + { Envelope.PositionMetadataKey, position + 1 } + }; + var buildingUnitWasCorrectedToRealizedMetadata = new Dictionary + { + { AddEventHashPipe.HashMetadataKey, buildingUnitWasCorrectedToRealized.GetHash() }, + { Envelope.PositionMetadataKey, position + 2 } + }; + + await Sut + .Given( + new Envelope(new Envelope(buildingUnitWasPlannedV2, buildingUnitWasPlannedMetadata)), + new Envelope(new Envelope(buildingUnitWasRetiredV2, buildingUnitWasRetiredMetadata)), + new Envelope( + new Envelope(buildingUnitWasCorrectedToRealized, buildingUnitWasCorrectedToRealizedMetadata))) + .Then(async context => + { + var buildingUnitLatestItem = + await context.BuildingUnitLatestItems.FindAsync(buildingUnitWasCorrectedToRealized.BuildingUnitPersistentLocalId); + buildingUnitLatestItem.Should().NotBeNull(); + + buildingUnitLatestItem!.Status.Should().Be(BuildingUnitStatus.Realized.Map()); + buildingUnitLatestItem.OsloStatus.Should().Be("Realized"); + buildingUnitLatestItem.VersionTimestamp.Should().Be(buildingUnitWasCorrectedToRealized.Provenance.Timestamp); + }); + } + + [Fact] + public async Task WhenBuildingUnitPositionWasCorrected() + { + _fixture.Customize(new WithFixedBuildingPersistentLocalId()); + _fixture.Customize(new WithFixedBuildingUnitPersistentLocalId()); + + var buildingUnitWasPlannedV2 = _fixture.Create(); + var buildingUnitPositionWasCorrected = _fixture.Create(); + + var position = _fixture.Create(); + + var buildingUnitWasPlannedMetadata = new Dictionary + { + { AddEventHashPipe.HashMetadataKey, buildingUnitWasPlannedV2.GetHash() }, + { Envelope.PositionMetadataKey, position } + }; + var buildingUnitPositionWasCorrectedMetadata = new Dictionary + { + { AddEventHashPipe.HashMetadataKey, buildingUnitPositionWasCorrected.GetHash() }, + { Envelope.PositionMetadataKey, position + 1 } + }; + await Sut + .Given( + new Envelope(new Envelope(buildingUnitWasPlannedV2, buildingUnitWasPlannedMetadata)), + new Envelope(new Envelope(buildingUnitPositionWasCorrected, buildingUnitPositionWasCorrectedMetadata))) + .Then(async context => + { + var buildingUnitLatestItem = + await context.BuildingUnitLatestItems.FindAsync(buildingUnitPositionWasCorrected.BuildingUnitPersistentLocalId); + buildingUnitLatestItem.Should().NotBeNull(); + + buildingUnitLatestItem!.BuildingPersistentLocalId.Should().Be(buildingUnitPositionWasCorrected.BuildingPersistentLocalId); + buildingUnitLatestItem.Geometry.Should().BeEquivalentTo( + _wkbReader.Read(buildingUnitPositionWasCorrected.ExtendedWkbGeometry.ToByteArray())); + buildingUnitLatestItem.GeometryMethod.Should() + .Be(BuildingUnitPositionGeometryMethod.Parse(buildingUnitPositionWasCorrected.GeometryMethod).Map()); + buildingUnitLatestItem.VersionTimestamp.Should().Be(buildingUnitPositionWasCorrected.Provenance.Timestamp); + }); + } + + [Fact] + public async Task WhenBuildingUnitWasRemovedV2() + { + _fixture.Customize(new WithFixedBuildingPersistentLocalId()); + _fixture.Customize(new WithFixedBuildingUnitPersistentLocalId()); + + var buildingUnitWasPlannedV2 = _fixture.Create(); + var buildingUnitWasRemovedV2 = _fixture.Create(); + + var position = _fixture.Create(); + + var buildingUnitWasPlannedMetadata = new Dictionary + { + { AddEventHashPipe.HashMetadataKey, buildingUnitWasPlannedV2.GetHash() }, + { Envelope.PositionMetadataKey, position } + }; + var buildingUnitWasRemovedMetadata = new Dictionary + { + { AddEventHashPipe.HashMetadataKey, buildingUnitWasRemovedV2.GetHash() }, + { Envelope.PositionMetadataKey, position + 1 } + }; + + await Sut + .Given( + new Envelope(new Envelope(buildingUnitWasPlannedV2, buildingUnitWasPlannedMetadata)), + new Envelope(new Envelope(buildingUnitWasRemovedV2, buildingUnitWasRemovedMetadata))) + .Then(async context => + { + var buildingUnitLatestItem = + await context.BuildingUnitLatestItems.FindAsync(buildingUnitWasRemovedV2.BuildingUnitPersistentLocalId); + buildingUnitLatestItem.Should().NotBeNull(); + + buildingUnitLatestItem!.IsRemoved.Should().BeTrue(); + buildingUnitLatestItem.VersionTimestamp.Should().Be(buildingUnitWasRemovedV2.Provenance.Timestamp); + }); + } + + [Fact] + public async Task WhenBuildingUnitWasRemovedBecauseBuildingWasRemoved() + { + _fixture.Customize(new WithFixedBuildingPersistentLocalId()); + _fixture.Customize(new WithFixedBuildingUnitPersistentLocalId()); + + var buildingUnitWasPlannedV2 = _fixture.Create(); + var buildingUnitWasRemoved = _fixture.Create(); + + var position = _fixture.Create(); + + var buildingUnitWasPlannedMetadata = new Dictionary + { + { AddEventHashPipe.HashMetadataKey, buildingUnitWasPlannedV2.GetHash() }, + { Envelope.PositionMetadataKey, position } + }; + var buildingUnitWasRemovedMetadata = new Dictionary + { + { AddEventHashPipe.HashMetadataKey, buildingUnitWasRemoved.GetHash() }, + { Envelope.PositionMetadataKey, position + 1 } + }; + + await Sut + .Given( + new Envelope(new Envelope(buildingUnitWasPlannedV2, buildingUnitWasPlannedMetadata)), + new Envelope(new Envelope(buildingUnitWasRemoved, buildingUnitWasRemovedMetadata))) + .Then(async context => + { + var buildingUnitLatestItem = + await context.BuildingUnitLatestItems.FindAsync(buildingUnitWasRemoved.BuildingUnitPersistentLocalId); + buildingUnitLatestItem.Should().NotBeNull(); + + buildingUnitLatestItem!.IsRemoved.Should().BeTrue(); + buildingUnitLatestItem.VersionTimestamp.Should().Be(buildingUnitWasRemoved.Provenance.Timestamp); + }); + } + + [Fact] + public async Task WhenBuildingUnitRemovalWasCorrected() + { + _fixture.Customize(new WithFixedBuildingPersistentLocalId()); + _fixture.Customize(new WithFixedBuildingUnitPersistentLocalId()); + + var buildingUnitWasPlannedV2 = _fixture.Create(); + var buildingUnitWasRemovedV2 = _fixture.Create(); + var buildingUnitRemovalWasCorrected = _fixture.Create(); + + var position = _fixture.Create(); + + var buildingUnitWasPlannedMetadata = new Dictionary + { + { AddEventHashPipe.HashMetadataKey, buildingUnitWasPlannedV2.GetHash() }, + { Envelope.PositionMetadataKey, position } + }; + var buildingUnitWasRemovedMetadata = new Dictionary + { + { AddEventHashPipe.HashMetadataKey, buildingUnitWasRemovedV2.GetHash() }, + { Envelope.PositionMetadataKey, position + 1 } + }; + var buildingUnitRemovalWasCorrectedMetadata = new Dictionary + { + { AddEventHashPipe.HashMetadataKey, buildingUnitRemovalWasCorrected.GetHash() }, + { Envelope.PositionMetadataKey, position + 2 } + }; + + await Sut + .Given( + new Envelope(new Envelope(buildingUnitWasPlannedV2, buildingUnitWasPlannedMetadata)), + new Envelope(new Envelope(buildingUnitWasRemovedV2, buildingUnitWasRemovedMetadata)), + new Envelope(new Envelope(buildingUnitRemovalWasCorrected, buildingUnitRemovalWasCorrectedMetadata))) + .Then(async context => + { + var buildingUnitLatestItem = + await context.BuildingUnitLatestItems.FindAsync(buildingUnitRemovalWasCorrected.BuildingUnitPersistentLocalId); + buildingUnitLatestItem.Should().NotBeNull(); + + buildingUnitLatestItem!.Status.Should().Be(BuildingUnitStatus.Parse(buildingUnitRemovalWasCorrected.BuildingUnitStatus).Map()); + buildingUnitLatestItem.OsloStatus.Should().Be(buildingUnitRemovalWasCorrected.BuildingUnitStatus); + buildingUnitLatestItem.Function.Should().Be(BuildingUnitFunction.Parse(buildingUnitRemovalWasCorrected.Function).Map()); + buildingUnitLatestItem.OsloFunction.Should().Be(buildingUnitRemovalWasCorrected.Function); + buildingUnitLatestItem.Geometry.Should().BeEquivalentTo( + _wkbReader.Read(buildingUnitRemovalWasCorrected.ExtendedWkbGeometry.ToByteArray())); + buildingUnitLatestItem.GeometryMethod.Should() + .Be(BuildingUnitPositionGeometryMethod.Parse(buildingUnitRemovalWasCorrected.GeometryMethod).Map()); + buildingUnitLatestItem.HasDeviation.Should().Be(buildingUnitRemovalWasCorrected.HasDeviation); + buildingUnitLatestItem.IsRemoved.Should().BeFalse(); + buildingUnitLatestItem.VersionTimestamp.Should().Be(buildingUnitRemovalWasCorrected.Provenance.Timestamp); + }); + } + + [Fact] + public async Task WhenBuildingUnitWasRegularized() + { + _fixture.Customize(new WithFixedBuildingPersistentLocalId()); + _fixture.Customize(new WithFixedBuildingUnitPersistentLocalId()); + + var buildingUnitWasPlannedV2 = _fixture.Create().WithDeviation(true); + var buildingUnitWasRegularized = _fixture.Create(); + + var position = _fixture.Create(); + + var buildingUnitWasPlannedMetadata = new Dictionary + { + { AddEventHashPipe.HashMetadataKey, buildingUnitWasPlannedV2.GetHash() }, + { Envelope.PositionMetadataKey, position } + }; + var buildingUnitWasRegularizedMetadata = new Dictionary + { + { AddEventHashPipe.HashMetadataKey, buildingUnitWasRegularized.GetHash() }, + { Envelope.PositionMetadataKey, position + 1 } + }; + + await Sut + .Given( + new Envelope(new Envelope(buildingUnitWasPlannedV2, buildingUnitWasPlannedMetadata)), + new Envelope(new Envelope(buildingUnitWasRegularized, buildingUnitWasRegularizedMetadata))) + .Then(async context => + { + var buildingUnitLatestItem = + await context.BuildingUnitLatestItems.FindAsync(buildingUnitWasRegularized.BuildingUnitPersistentLocalId); + buildingUnitLatestItem.Should().NotBeNull(); + + buildingUnitLatestItem!.HasDeviation.Should().BeFalse(); + buildingUnitLatestItem.VersionTimestamp.Should().Be(buildingUnitWasRegularized.Provenance.Timestamp); + }); + } + + [Fact] + public async Task WhenBuildingUnitRegularizationWasCorrected() + { + _fixture.Customize(new WithFixedBuildingPersistentLocalId()); + _fixture.Customize(new WithFixedBuildingUnitPersistentLocalId()); + + var buildingUnitWasPlannedV2 = _fixture.Create().WithDeviation(false); + var buildingUnitRegularizationWasCorrected = _fixture.Create(); + + var position = _fixture.Create(); + + var buildingUnitWasPlannedMetadata = new Dictionary + { + { AddEventHashPipe.HashMetadataKey, buildingUnitWasPlannedV2.GetHash() }, + { Envelope.PositionMetadataKey, position } + }; + var buildingUnitRegularizationWasCorrectedMetadata = new Dictionary + { + { AddEventHashPipe.HashMetadataKey, buildingUnitRegularizationWasCorrected.GetHash() }, + { Envelope.PositionMetadataKey, position + 1 } + }; + + await Sut + .Given( + new Envelope(new Envelope(buildingUnitWasPlannedV2, buildingUnitWasPlannedMetadata)), + new Envelope( + new Envelope(buildingUnitRegularizationWasCorrected, buildingUnitRegularizationWasCorrectedMetadata))) + .Then(async context => + { + var buildingUnitLatestItem = + await context.BuildingUnitLatestItems.FindAsync(buildingUnitRegularizationWasCorrected.BuildingUnitPersistentLocalId); + buildingUnitLatestItem.Should().NotBeNull(); + + buildingUnitLatestItem!.HasDeviation.Should().BeTrue(); + buildingUnitLatestItem.VersionTimestamp.Should().Be(buildingUnitRegularizationWasCorrected.Provenance.Timestamp); + }); + } + + [Fact] + public async Task WhenBuildingUnitWasDeregulated() + { + _fixture.Customize(new WithFixedBuildingPersistentLocalId()); + _fixture.Customize(new WithFixedBuildingUnitPersistentLocalId()); + + var buildingUnitWasPlannedV2 = _fixture.Create().WithDeviation(false); + var buildingUnitWasDeregulated = _fixture.Create(); + + var position = _fixture.Create(); + + var buildingUnitWasPlannedMetadata = new Dictionary + { + { AddEventHashPipe.HashMetadataKey, buildingUnitWasPlannedV2.GetHash() }, + { Envelope.PositionMetadataKey, position } + }; + var buildingUnitWasDeregulatedMetadata = new Dictionary + { + { AddEventHashPipe.HashMetadataKey, buildingUnitWasDeregulated.GetHash() }, + { Envelope.PositionMetadataKey, position + 1 } + }; + + await Sut + .Given( + new Envelope(new Envelope(buildingUnitWasPlannedV2, buildingUnitWasPlannedMetadata)), + new Envelope(new Envelope(buildingUnitWasDeregulated, buildingUnitWasDeregulatedMetadata))) + .Then(async context => + { + var buildingUnitLatestItem = + await context.BuildingUnitLatestItems.FindAsync(buildingUnitWasDeregulated.BuildingUnitPersistentLocalId); + buildingUnitLatestItem.Should().NotBeNull(); + + buildingUnitLatestItem!.HasDeviation.Should().BeTrue(); + buildingUnitLatestItem.VersionTimestamp.Should().Be(buildingUnitWasDeregulated.Provenance.Timestamp); + }); + } + + [Fact] + public async Task WhenBuildingUnitDeregulationWasCorrected() + { + _fixture.Customize(new WithFixedBuildingPersistentLocalId()); + _fixture.Customize(new WithFixedBuildingUnitPersistentLocalId()); + + var buildingUnitWasPlannedV2 = _fixture.Create().WithDeviation(true); + var buildingUnitDeregulationWasCorrected = _fixture.Create(); + + var position = _fixture.Create(); + + var buildingUnitWasPlannedMetadata = new Dictionary + { + { AddEventHashPipe.HashMetadataKey, buildingUnitWasPlannedV2.GetHash() }, + { Envelope.PositionMetadataKey, position } + }; + var buildingUnitDeregulationWasCorrectedMetadata = new Dictionary + { + { AddEventHashPipe.HashMetadataKey, buildingUnitDeregulationWasCorrected.GetHash() }, + { Envelope.PositionMetadataKey, position + 1 } + }; + await Sut + .Given( + new Envelope(new Envelope(buildingUnitWasPlannedV2, buildingUnitWasPlannedMetadata)), + new Envelope( + new Envelope(buildingUnitDeregulationWasCorrected, buildingUnitDeregulationWasCorrectedMetadata))) + .Then(async context => + { + var buildingUnitLatestItem = + await context.BuildingUnitLatestItems.FindAsync(buildingUnitDeregulationWasCorrected.BuildingUnitPersistentLocalId); + buildingUnitLatestItem.Should().NotBeNull(); + + buildingUnitLatestItem!.HasDeviation.Should().BeFalse(); + buildingUnitLatestItem.VersionTimestamp.Should().Be(buildingUnitDeregulationWasCorrected.Provenance.Timestamp); + }); + } + + [Fact] + public async Task WhenCommonBuildingUnitWasAddedV2() + { + _fixture.Customize(new WithFixedBuildingPersistentLocalId()); + _fixture.Customize(new WithFixedBuildingUnitPersistentLocalId()); + + var commonBuildingUnitWasAddedV2 = new CommonBuildingUnitWasAddedV2( + _fixture.Create(), + _fixture.Create(), + BuildingUnitStatus.Planned, + BuildingUnitPositionGeometryMethod.DerivedFromObject, + _fixture.Create(), + false); + ((ISetProvenance)commonBuildingUnitWasAddedV2).SetProvenance(_fixture.Create()); + + var position = _fixture.Create(); + + var commonBuildingUnitWasAddedMetadata = new Dictionary + { + { AddEventHashPipe.HashMetadataKey, commonBuildingUnitWasAddedV2.GetHash() }, + { Envelope.PositionMetadataKey, position } + }; + + await Sut + .Given(new Envelope(new Envelope(commonBuildingUnitWasAddedV2, commonBuildingUnitWasAddedMetadata))) + .Then(async context => + { + var buildingUnitLatestItem = await context.BuildingUnitLatestItems.FindAsync(commonBuildingUnitWasAddedV2 + .BuildingUnitPersistentLocalId); + buildingUnitLatestItem.Should().NotBeNull(); + + buildingUnitLatestItem!.BuildingPersistentLocalId.Should().Be(commonBuildingUnitWasAddedV2.BuildingPersistentLocalId); + buildingUnitLatestItem.Geometry.Should().BeEquivalentTo( + _wkbReader.Read(commonBuildingUnitWasAddedV2.ExtendedWkbGeometry.ToByteArray())); + buildingUnitLatestItem.GeometryMethod.Should() + .Be(BuildingUnitPositionGeometryMethod.Parse(commonBuildingUnitWasAddedV2.GeometryMethod).Map()); + buildingUnitLatestItem.Function.Should().Be(BuildingUnitFunction.Common.Map()); + buildingUnitLatestItem.OsloFunction.Should().Be(BuildingUnitFunction.Common.Function); + buildingUnitLatestItem.Status.Should().Be(BuildingUnitStatus.Parse(commonBuildingUnitWasAddedV2.BuildingUnitStatus).Map()); + buildingUnitLatestItem.OsloStatus.Should().Be(commonBuildingUnitWasAddedV2.BuildingUnitStatus); + buildingUnitLatestItem.HasDeviation.Should().Be(commonBuildingUnitWasAddedV2.HasDeviation); + buildingUnitLatestItem.IsRemoved.Should().BeFalse(); + buildingUnitLatestItem.VersionTimestamp.Should().Be(commonBuildingUnitWasAddedV2.Provenance.Timestamp); + }); + } + + [Fact] + public async Task WhenBuildingUnitAddressWasAttachedV2() + { + _fixture.Customize(new WithFixedBuildingPersistentLocalId()); + _fixture.Customize(new WithFixedBuildingUnitPersistentLocalId()); + + var buildingUnitWasPlannedV2 = _fixture.Create(); + var buildingUnitAddressWasAttachedV2 = _fixture.Create(); + + var position = _fixture.Create(); + + var buildingUnitWasPlannedMetadata = new Dictionary + { + { AddEventHashPipe.HashMetadataKey, buildingUnitWasPlannedV2.GetHash() }, + { Envelope.PositionMetadataKey, position } + }; + var buildingUnitAddressWasAttachedMetadata = new Dictionary + { + { AddEventHashPipe.HashMetadataKey, buildingUnitAddressWasAttachedV2.GetHash() }, + { Envelope.PositionMetadataKey, position + 1 } + }; + + await Sut + .Given( + new Envelope(new Envelope(buildingUnitWasPlannedV2, buildingUnitWasPlannedMetadata)), + new Envelope(new Envelope(buildingUnitAddressWasAttachedV2, buildingUnitAddressWasAttachedMetadata))) + .Then(async context => + { + var buildingUnitLatestItem = + await context.BuildingUnitLatestItems.FindAsync(buildingUnitAddressWasAttachedV2.BuildingUnitPersistentLocalId); + buildingUnitLatestItem.Should().NotBeNull(); + + buildingUnitLatestItem!.VersionTimestamp.Should().Be(buildingUnitAddressWasAttachedV2.Provenance.Timestamp); + + var buildingUnitAddresses = context.BuildingUnitAddresses + .Where(x => x.BuildingUnitPersistentLocalId == buildingUnitLatestItem.BuildingUnitPersistentLocalId) + .ToList(); + buildingUnitAddresses.Should().HaveCount(1); + buildingUnitAddresses.Single().AddressPersistentLocalId.Should().Be(buildingUnitAddressWasAttachedV2.AddressPersistentLocalId); + }); + } + + [Fact] + public async Task WhenBuildingUnitAddressWasDetachedV2() + { + _fixture.Customize(new WithFixedBuildingPersistentLocalId()); + _fixture.Customize(new WithFixedBuildingUnitPersistentLocalId()); + _fixture.Customize(new WithFixedAddressPersistentLocalId()); + + var buildingUnitWasPlannedV2 = _fixture.Create(); + var buildingUnitAddressWasAttached = _fixture.Create(); + var buildingUnitAddressWasDetachedV2 = _fixture.Create(); + + var position = _fixture.Create(); + + var buildingUnitWasPlannedMetadata = new Dictionary + { + { AddEventHashPipe.HashMetadataKey, buildingUnitWasPlannedV2.GetHash() }, + { Envelope.PositionMetadataKey, position } + }; + var buildingUnitAddressWasAttachedMetadata = new Dictionary + { + { AddEventHashPipe.HashMetadataKey, buildingUnitAddressWasAttached.GetHash() }, + { Envelope.PositionMetadataKey, position + 1 } + }; + var buildingUnitAddressWasDetachedMetadata = new Dictionary + { + { AddEventHashPipe.HashMetadataKey, buildingUnitAddressWasDetachedV2.GetHash() }, + { Envelope.PositionMetadataKey, position + 2 } + }; + + await Sut + .Given( + new Envelope(new Envelope(buildingUnitWasPlannedV2, buildingUnitWasPlannedMetadata)), + new Envelope(new Envelope(buildingUnitAddressWasAttached, buildingUnitAddressWasAttachedMetadata)), + new Envelope(new Envelope(buildingUnitAddressWasDetachedV2, buildingUnitAddressWasDetachedMetadata))) + .Then(async context => + { + var buildingUnitLatestItem = + await context.BuildingUnitLatestItems.FindAsync(buildingUnitAddressWasDetachedV2.BuildingUnitPersistentLocalId); + buildingUnitLatestItem.Should().NotBeNull(); + + buildingUnitLatestItem!.VersionTimestamp.Should().Be(buildingUnitAddressWasDetachedV2.Provenance.Timestamp); + + var buildingUnitAddresses = context.BuildingUnitAddresses + .Where(x => x.BuildingUnitPersistentLocalId == buildingUnitLatestItem.BuildingUnitPersistentLocalId) + .ToList(); + buildingUnitAddresses.Should().BeEmpty(); + }); + } + + [Fact] + public async Task WhenBuildingUnitAddressWasDetachedBecauseAddressWasRetired() + { + _fixture.Customize(new WithFixedBuildingPersistentLocalId()); + _fixture.Customize(new WithFixedBuildingUnitPersistentLocalId()); + _fixture.Customize(new WithFixedAddressPersistentLocalId()); + + var buildingUnitWasPlannedV2 = _fixture.Create(); + var buildingUnitAddressWasAttached = _fixture.Create(); + var buildingUnitAddressWasDetached = _fixture.Create(); + + var position = _fixture.Create(); + + var buildingUnitWasPlannedMetadata = new Dictionary + { + { AddEventHashPipe.HashMetadataKey, buildingUnitWasPlannedV2.GetHash() }, + { Envelope.PositionMetadataKey, position } + }; + var buildingUnitAddressWasAttachedMetadata = new Dictionary + { + { AddEventHashPipe.HashMetadataKey, buildingUnitAddressWasAttached.GetHash() }, + { Envelope.PositionMetadataKey, position + 1 } + }; + var buildingUnitAddressWasDetachedMetadata = new Dictionary + { + { AddEventHashPipe.HashMetadataKey, buildingUnitAddressWasDetached.GetHash() }, + { Envelope.PositionMetadataKey, position + 2 } + }; + + await Sut + .Given( + new Envelope( + new Envelope(buildingUnitWasPlannedV2, buildingUnitWasPlannedMetadata)), + new Envelope( + new Envelope(buildingUnitAddressWasAttached, buildingUnitAddressWasAttachedMetadata)), + new Envelope( + new Envelope(buildingUnitAddressWasDetached, buildingUnitAddressWasDetachedMetadata))) + .Then(async context => + { + var buildingUnitLatestItem = + await context.BuildingUnitLatestItems.FindAsync(buildingUnitAddressWasDetached.BuildingUnitPersistentLocalId); + buildingUnitLatestItem.Should().NotBeNull(); + + buildingUnitLatestItem!.VersionTimestamp.Should().Be(buildingUnitAddressWasDetached.Provenance.Timestamp); + + var buildingUnitAddresses = context.BuildingUnitAddresses + .Where(x => x.BuildingUnitPersistentLocalId == buildingUnitLatestItem.BuildingUnitPersistentLocalId) + .ToList(); + buildingUnitAddresses.Should().BeEmpty(); + }); + } + + [Fact] + public async Task WhenBuildingUnitAddressWasDetachedBecauseAddressWasRejected() + { + _fixture.Customize(new WithFixedBuildingPersistentLocalId()); + _fixture.Customize(new WithFixedBuildingUnitPersistentLocalId()); + _fixture.Customize(new WithFixedAddressPersistentLocalId()); + + var buildingUnitWasPlannedV2 = _fixture.Create(); + var buildingUnitAddressWasAttached = _fixture.Create(); + var buildingUnitAddressWasDetached = _fixture.Create(); + + var position = _fixture.Create(); + + var buildingUnitWasPlannedMetadata = new Dictionary + { + { AddEventHashPipe.HashMetadataKey, buildingUnitWasPlannedV2.GetHash() }, + { Envelope.PositionMetadataKey, position } + }; + var buildingUnitAddressWasAttachedMetadata = new Dictionary + { + { AddEventHashPipe.HashMetadataKey, buildingUnitAddressWasAttached.GetHash() }, + { Envelope.PositionMetadataKey, position + 1 } + }; + var buildingUnitAddressWasDetachedMetadata = new Dictionary + { + { AddEventHashPipe.HashMetadataKey, buildingUnitAddressWasDetached.GetHash() }, + { Envelope.PositionMetadataKey, position + 2 } + }; + + await Sut + .Given( + new Envelope(new Envelope(buildingUnitWasPlannedV2, buildingUnitWasPlannedMetadata)), + new Envelope( + new Envelope(buildingUnitAddressWasAttached, buildingUnitAddressWasAttachedMetadata)), + new Envelope( + new Envelope(buildingUnitAddressWasDetached, buildingUnitAddressWasDetachedMetadata))) + .Then(async context => + { + var buildingUnitLatestItem = + await context.BuildingUnitLatestItems.FindAsync(buildingUnitAddressWasDetached.BuildingUnitPersistentLocalId); + buildingUnitLatestItem.Should().NotBeNull(); + + buildingUnitLatestItem!.VersionTimestamp.Should().Be(buildingUnitAddressWasDetached.Provenance.Timestamp); + + var buildingUnitAddresses = context.BuildingUnitAddresses + .Where(x => x.BuildingUnitPersistentLocalId == buildingUnitLatestItem.BuildingUnitPersistentLocalId) + .ToList(); + buildingUnitAddresses.Should().BeEmpty(); + }); + } + + [Fact] + public async Task WhenBuildingUnitAddressWasDetachedBecauseAddressWasRemoved() + { + _fixture.Customize(new WithFixedBuildingPersistentLocalId()); + _fixture.Customize(new WithFixedBuildingUnitPersistentLocalId()); + _fixture.Customize(new WithFixedAddressPersistentLocalId()); + + var buildingUnitWasPlannedV2 = _fixture.Create(); + var buildingUnitAddressWasAttached = _fixture.Create(); + var buildingUnitAddressWasDetached = _fixture.Create(); + + var position = _fixture.Create(); + + var buildingUnitWasPlannedMetadata = new Dictionary + { + { AddEventHashPipe.HashMetadataKey, buildingUnitWasPlannedV2.GetHash() }, + { Envelope.PositionMetadataKey, position } + }; + var buildingUnitAddressWasAttachedMetadata = new Dictionary + { + { AddEventHashPipe.HashMetadataKey, buildingUnitAddressWasAttached.GetHash() }, + { Envelope.PositionMetadataKey, position + 1 } + }; + var buildingUnitAddressWasDetachedMetadata = new Dictionary + { + { AddEventHashPipe.HashMetadataKey, buildingUnitAddressWasDetached.GetHash() }, + { Envelope.PositionMetadataKey, position + 2 } + }; + + await Sut + .Given( + new Envelope(new Envelope(buildingUnitWasPlannedV2, buildingUnitWasPlannedMetadata)), + new Envelope( + new Envelope(buildingUnitAddressWasAttached, buildingUnitAddressWasAttachedMetadata)), + new Envelope( + new Envelope(buildingUnitAddressWasDetached, buildingUnitAddressWasDetachedMetadata))) + .Then(async context => + { + var buildingUnitLatestItem = + await context.BuildingUnitLatestItems.FindAsync(buildingUnitAddressWasDetached.BuildingUnitPersistentLocalId); + buildingUnitLatestItem.Should().NotBeNull(); + + buildingUnitLatestItem!.VersionTimestamp.Should().Be(buildingUnitAddressWasDetached.Provenance.Timestamp); + + var buildingUnitAddresses = context.BuildingUnitAddresses + .Where(x => x.BuildingUnitPersistentLocalId == buildingUnitLatestItem.BuildingUnitPersistentLocalId) + .ToList(); + buildingUnitAddresses.Should().BeEmpty(); + }); + } + + [Fact] + public async Task WhenBuildingUnitAddressWasReplacedBecauseAddressWasReaddressed() + { + _fixture.Customize(new WithFixedBuildingPersistentLocalId()); + _fixture.Customize(new WithFixedBuildingUnitPersistentLocalId()); + _fixture.Customize(new WithFixedAddressPersistentLocalId()); + + var position = _fixture.Create(); + + var buildingUnitWasPlannedV2 = _fixture.Create(); + var buildingUnitAddressWasAttached = _fixture.Create(); + var buildingUnitAddressWasReplacedBecauseAddressWasReaddressed = new BuildingUnitAddressWasReplacedBecauseAddressWasReaddressed( + _fixture.Create(), + _fixture.Create(), + new AddressPersistentLocalId(buildingUnitAddressWasAttached.AddressPersistentLocalId), + new AddressPersistentLocalId(buildingUnitAddressWasAttached.AddressPersistentLocalId + 1)); + ((ISetProvenance)buildingUnitAddressWasReplacedBecauseAddressWasReaddressed).SetProvenance(_fixture.Create()); + + var buildingUnitWasPlannedMetadata = new Dictionary + { + { AddEventHashPipe.HashMetadataKey, buildingUnitWasPlannedV2.GetHash() }, + { Envelope.PositionMetadataKey, position } + }; + var buildingUnitAddressWasAttachedMetadata = new Dictionary + { + { AddEventHashPipe.HashMetadataKey, buildingUnitAddressWasAttached.GetHash() }, + { Envelope.PositionMetadataKey, position + 1 } + }; + var buildingUnitAddressWasReplacedBecauseAddressWasReaddressedMetadata = new Dictionary + { + { AddEventHashPipe.HashMetadataKey, buildingUnitAddressWasReplacedBecauseAddressWasReaddressed.GetHash() }, + { Envelope.PositionMetadataKey, position + 2 } + }; + await Sut + .Given( + new Envelope(new Envelope(buildingUnitWasPlannedV2, buildingUnitWasPlannedMetadata)), + new Envelope(new Envelope(buildingUnitAddressWasAttached, buildingUnitAddressWasAttachedMetadata)), + new Envelope( + new Envelope( + buildingUnitAddressWasReplacedBecauseAddressWasReaddressed, + buildingUnitAddressWasReplacedBecauseAddressWasReaddressedMetadata))) + .Then(async context => + { + var buildingUnitLatestItem = + await context.BuildingUnitLatestItems.FindAsync(buildingUnitAddressWasReplacedBecauseAddressWasReaddressed.BuildingUnitPersistentLocalId); + buildingUnitLatestItem.Should().NotBeNull(); + + buildingUnitLatestItem!.VersionTimestamp.Should().Be(buildingUnitAddressWasReplacedBecauseAddressWasReaddressed.Provenance.Timestamp); + + var newAddress = context.BuildingUnitAddresses.SingleOrDefault(x => + x.AddressPersistentLocalId == buildingUnitAddressWasReplacedBecauseAddressWasReaddressed.NewAddressPersistentLocalId); + newAddress.Should().NotBeNull(); + + var oldAddress = context.BuildingUnitAddresses.SingleOrDefault(x => + x.AddressPersistentLocalId == buildingUnitAddressWasReplacedBecauseAddressWasReaddressed.PreviousAddressPersistentLocalId); + oldAddress.Should().BeNull(); + }); + } + + [Fact] + public async Task WhenBuildingUnitWasRetiredBecauseBuildingWasDemolished() + { + _fixture.Customize(new WithFixedBuildingPersistentLocalId()); + _fixture.Customize(new WithFixedBuildingUnitPersistentLocalId()); + + var buildingUnitWasPlannedV2 = _fixture.Create(); + var buildingUnitWasRetired = _fixture.Create(); + + var position = _fixture.Create(); + + var buildingUnitWasPlannedMetadata = new Dictionary + { + { AddEventHashPipe.HashMetadataKey, buildingUnitWasPlannedV2.GetHash() }, + { Envelope.PositionMetadataKey, position } + }; + var buildingUnitWasRetiredMetadata = new Dictionary + { + { AddEventHashPipe.HashMetadataKey, buildingUnitWasRetired.GetHash() }, + { Envelope.PositionMetadataKey, position + 1 } + }; + + await Sut + .Given( + new Envelope(new Envelope(buildingUnitWasPlannedV2, buildingUnitWasPlannedMetadata)), + new Envelope(new Envelope(buildingUnitWasRetired, buildingUnitWasRetiredMetadata))) + .Then(async context => + { + var buildingUnitLatestItem = + await context.BuildingUnitLatestItems.FindAsync(buildingUnitWasRetired.BuildingUnitPersistentLocalId); + buildingUnitLatestItem.Should().NotBeNull(); + + buildingUnitLatestItem!.Status.Should().Be(BuildingUnitStatus.Retired.Map()); + buildingUnitLatestItem.OsloStatus.Should().Be("Retired"); + buildingUnitLatestItem.VersionTimestamp.Should().Be(buildingUnitWasRetired.Provenance.Timestamp); + }); + } + + [Fact] + public async Task WhenBuildingUnitWasNotRealizedBecauseBuildingWasDemolished() + { + _fixture.Customize(new WithFixedBuildingPersistentLocalId()); + _fixture.Customize(new WithFixedBuildingUnitPersistentLocalId()); + + var buildingUnitWasPlannedV2 = _fixture.Create(); + var buildingUnitWasNotRealized = _fixture.Create(); + + var position = _fixture.Create(); + + var buildingUnitWasPlannedMetadata = new Dictionary + { + { AddEventHashPipe.HashMetadataKey, buildingUnitWasPlannedV2.GetHash() }, + { Envelope.PositionMetadataKey, position } + }; + var buildingUnitWasNotRealizedMetadata = new Dictionary + { + { AddEventHashPipe.HashMetadataKey, buildingUnitWasNotRealized.GetHash() }, + { Envelope.PositionMetadataKey, position + 1 } + }; + + await Sut + .Given( + new Envelope(new Envelope(buildingUnitWasPlannedV2, buildingUnitWasPlannedMetadata)), + new Envelope(new Envelope(buildingUnitWasNotRealized, buildingUnitWasNotRealizedMetadata))) + .Then(async context => + { + var buildingUnitLatestItem = + await context.BuildingUnitLatestItems.FindAsync(buildingUnitWasNotRealized.BuildingUnitPersistentLocalId); + buildingUnitLatestItem.Should().NotBeNull(); + + buildingUnitLatestItem!.Status.Should().Be(BuildingUnitStatus.NotRealized.Map()); + buildingUnitLatestItem.OsloStatus.Should().Be("NotRealized"); + buildingUnitLatestItem.VersionTimestamp.Should().Be(buildingUnitWasNotRealized.Provenance.Timestamp); + }); + } + + [Fact] + public async Task WhenBuildingUnitWasTransferred() + { + _fixture.Customize(new WithFixedBuildingUnitPersistentLocalId()); + + var buildingUnitWasPlannedV2 = _fixture.Create(); + var buildingUnitWasTransferred = new BuildingUnitWasTransferredBuilder(_fixture) + .WithBuildingUnitPersistentLocalId(buildingUnitWasPlannedV2.BuildingUnitPersistentLocalId) + .WithSourceBuildingPersistentLocalId(buildingUnitWasPlannedV2.BuildingPersistentLocalId) + .Build(); + + var position = _fixture.Create(); + + var buildingUnitWasPlannedMetadata = new Dictionary + { + { AddEventHashPipe.HashMetadataKey, buildingUnitWasPlannedV2.GetHash() }, + { Envelope.PositionMetadataKey, position } + }; + var buildingUnitWasTransferredMetadata = new Dictionary + { + { AddEventHashPipe.HashMetadataKey, buildingUnitWasTransferred.GetHash() }, + { Envelope.PositionMetadataKey, position + 1 } + }; + + await Sut + .Given( + new Envelope(new Envelope(buildingUnitWasPlannedV2, buildingUnitWasPlannedMetadata)), + new Envelope(new Envelope(buildingUnitWasTransferred, buildingUnitWasTransferredMetadata))) + .Then(async context => + { + var buildingUnitLatestItem = + await context.BuildingUnitLatestItems.FindAsync(buildingUnitWasTransferred.BuildingUnitPersistentLocalId); + buildingUnitLatestItem.Should().NotBeNull(); + + buildingUnitLatestItem!.BuildingPersistentLocalId.Should().Be(buildingUnitWasTransferred.BuildingPersistentLocalId); + buildingUnitLatestItem.Status.Should().Be( + BuildingUnitStatus.Parse(buildingUnitWasTransferred.Status).Map()); + buildingUnitLatestItem.OsloStatus.Should().Be(buildingUnitWasTransferred.Status); + buildingUnitLatestItem.Function.Should().Be( + BuildingUnitFunction.Parse(buildingUnitWasTransferred.Function).Map()); + buildingUnitLatestItem.OsloFunction.Should().Be(buildingUnitWasTransferred.Function); + buildingUnitLatestItem.GeometryMethod.Should().Be( + BuildingUnitPositionGeometryMethod.Parse(buildingUnitWasTransferred.GeometryMethod).Map()); + buildingUnitLatestItem.Geometry.Should().BeEquivalentTo( + _wkbReader.Read(buildingUnitWasTransferred.ExtendedWkbGeometry.ToByteArray())); + buildingUnitLatestItem.HasDeviation.Should().BeFalse(); + buildingUnitLatestItem.VersionTimestamp.Should().Be(buildingUnitWasTransferred.Provenance.Timestamp); + + var buildingUnitAddresses = context.BuildingUnitAddresses + .Where(x => x.BuildingUnitPersistentLocalId == buildingUnitLatestItem.BuildingUnitPersistentLocalId) + .ToList(); + + buildingUnitAddresses.Should().HaveCount(buildingUnitWasTransferred.AddressPersistentLocalIds.Count); + foreach (var addressPersistentLocalId in buildingUnitWasTransferred.AddressPersistentLocalIds) + { + buildingUnitAddresses.SingleOrDefault(x => x.AddressPersistentLocalId == addressPersistentLocalId) + .Should().NotBeNull(); + } + }); + } + + protected override BuildingUnitLatestItemProjections CreateProjection() => + new BuildingUnitLatestItemProjections(new OptionsWrapper(new IntegrationOptions + { + BuildingNamespace = BuildingNamespace, + BuildingUnitNamespace = BuildingUnitNamespace + })); + } +} diff --git a/test/BuildingRegistry.Tests/ProjectionTests/Integration/IntegrationProjectionTest.cs b/test/BuildingRegistry.Tests/ProjectionTests/Integration/IntegrationProjectionTest.cs new file mode 100644 index 000000000..8da760613 --- /dev/null +++ b/test/BuildingRegistry.Tests/ProjectionTests/Integration/IntegrationProjectionTest.cs @@ -0,0 +1,30 @@ +namespace BuildingRegistry.Tests.ProjectionTests.Integration +{ + using System; + using Be.Vlaanderen.Basisregisters.ProjectionHandling.Connector; + using Be.Vlaanderen.Basisregisters.ProjectionHandling.Testing; + using Microsoft.EntityFrameworkCore; + using Projections.Integration; + + public abstract class IntegrationProjectionTest + where TProjection : ConnectedProjection + { + protected ConnectedProjectionTest Sut { get; } + + protected IntegrationProjectionTest() + { + Sut = new ConnectedProjectionTest(CreateContext, CreateProjection); + } + + protected virtual IntegrationContext CreateContext() + { + var options = new DbContextOptionsBuilder() + .UseInMemoryDatabase(Guid.NewGuid().ToString()) + .Options; + + return new IntegrationContext(options); + } + + protected abstract TProjection CreateProjection(); + } +}