diff --git a/src/AssociationRegistry.Admin.Api/Adapters/DuplicateVerenigingDetectionService/SearchDuplicateVerenigingDetectionService.cs b/src/AssociationRegistry.Admin.Api/Adapters/DuplicateVerenigingDetectionService/SearchDuplicateVerenigingDetectionService.cs index 778ee2ad0..c3b58f33c 100644 --- a/src/AssociationRegistry.Admin.Api/Adapters/DuplicateVerenigingDetectionService/SearchDuplicateVerenigingDetectionService.cs +++ b/src/AssociationRegistry.Admin.Api/Adapters/DuplicateVerenigingDetectionService/SearchDuplicateVerenigingDetectionService.cs @@ -3,16 +3,19 @@ using Schema.Search; using DuplicateVerenigingDetection; using Vereniging; +using Microsoft.Extensions.Logging.Abstractions; using Nest; using System.Collections.Immutable; public class SearchDuplicateVerenigingDetectionService : IDuplicateVerenigingDetectionService { private readonly IElasticClient _client; + private readonly ILogger _logger; - public SearchDuplicateVerenigingDetectionService(IElasticClient client) + public SearchDuplicateVerenigingDetectionService(IElasticClient client, ILogger logger = null) { _client = client; + _logger = logger ?? NullLogger.Instance; } public async Task> GetDuplicates(VerenigingsNaam naam, Locatie[] locaties, @@ -33,19 +36,35 @@ await _client s => s .Explain(includeScore) .TrackScores(includeScore) - //.MinScore(minimumScoreOverride.Value) - .Query( - q => q.Bool( - b => b.Must( - MatchOpNaam(naam), - IsNietGestopt, - IsNietDubbel - ) - .MustNot(BeVerwijderd) - .Filter(MatchOpPostcodeOfGemeente(gemeentes, postcodes) - ) - ) - )); + //.MinScore(minimumScoreOverride.Value) + .Query( + q => q.Bool( + b => b + .Should( + // Original must query + s1 => s1.Bool( + + b => b.Must( + MatchOpNaam(naam) + )), + s2 => s2.Bool( + b => b.Must( + MatchOpFullNaam(naam)) + )) + .MinimumShouldMatch(1) // At least one of the clauses must match + .Filter(MatchOpPostcodeOfGemeente(gemeentes, postcodes), + IsNietGestopt, + IsNietDubbel, + IsNietVerwijderd) + + ) + )); + + _logger.LogInformation("Score for query: {Score}", string.Join(", ", searchResponse.Hits.Select(x => $"{x.Score} {x.Source.Naam}"))); + searchResponse.Hits.ToList().ForEach(x => + { + _logger.LogInformation("Query: {Query}Explanation for Score {Score} of '{Naam}': {@Explanation}", naam, x.Score, x.Source.Naam, x.Explanation); + }); return searchResponse.Hits .Select(ToDuplicateVereniging) @@ -81,13 +100,13 @@ private static QueryContainer IsNietDubbel(QueryContainerDescriptor shouldDescriptor) + private static QueryContainer IsNietVerwijderd(QueryContainerDescriptor shouldDescriptor) { return shouldDescriptor .Term(termDescriptor => termDescriptor .Field(document => document.IsVerwijderd) - .Value(true)); + .Value(false)); } private static Func, QueryContainer> MatchOpPostcode(string[] postcodes) @@ -133,18 +152,26 @@ private static IEnumerable, QueryContainer> MatchOpNaam(VerenigingsNaam naam) + { + return must => must + .Match(m => m + .Field(f => f.Naam) + .Query(naam) + .Analyzer(DuplicateDetectionDocumentMapping.DuplicateAnalyzer) + .Fuzziness(Fuzziness.AutoLength(2, 3)) + .MinimumShouldMatch("3<75%")); + } + + private static Func, QueryContainer> MatchOpFullNaam(VerenigingsNaam naam) { return must => must .Match(m => m - .Field(f => f.Naam) - .Query(naam) - .Analyzer(DuplicateDetectionDocumentMapping - .DuplicateAnalyzer) - .Fuzziness( - Fuzziness - .Auto) // Assumes this analyzer applies lowercase and asciifolding - .MinimumShouldMatch("90%") // You can adjust this percentage as needed - ); + .Field("naam.naamFull") + .Query(naam)//.ToString().Replace(" ", "")) + .Analyzer(DuplicateDetectionDocumentMapping.DuplicateFullNameAnalyzer) + .Fuzziness(Fuzziness.AutoLength(3,3)) + .MinimumShouldMatch("75%") + ); // You can adjust this percentage as needed); } private static DuplicaatVereniging ToDuplicateVereniging(IHit document) diff --git a/src/AssociationRegistry.Admin.ProjectionHost/Infrastructure/ElasticSearch/ElasticClientExtensions.cs b/src/AssociationRegistry.Admin.ProjectionHost/Infrastructure/ElasticSearch/ElasticClientExtensions.cs index f2e41d756..ec8714be2 100644 --- a/src/AssociationRegistry.Admin.ProjectionHost/Infrastructure/ElasticSearch/ElasticClientExtensions.cs +++ b/src/AssociationRegistry.Admin.ProjectionHost/Infrastructure/ElasticSearch/ElasticClientExtensions.cs @@ -91,6 +91,12 @@ private static AnalyzersDescriptor AddDuplicateDetectionAnalyzer(AnalyzersDescri .Tokenizer("standard") .CharFilters("underscore_replace", "dot_replace") .Filters("lowercase", "asciifolding", "dutch_stop") + ).Custom(DuplicateDetectionDocumentMapping.DuplicateFullNameAnalyzer, + selector: ca + => ca + .Tokenizer("keyword") + .CharFilters("underscore_replace", "dot_replace") + .Filters("lowercase", "asciifolding", "dutch_stop") ); private static NormalizersDescriptor AddVerenigingZoekNormalizer(NormalizersDescriptor ad) diff --git a/src/AssociationRegistry.Admin.Schema/Search/DuplicateDetectionDocumentMapping.cs b/src/AssociationRegistry.Admin.Schema/Search/DuplicateDetectionDocumentMapping.cs index 94b14a53f..dd49772b1 100644 --- a/src/AssociationRegistry.Admin.Schema/Search/DuplicateDetectionDocumentMapping.cs +++ b/src/AssociationRegistry.Admin.Schema/Search/DuplicateDetectionDocumentMapping.cs @@ -5,6 +5,7 @@ namespace AssociationRegistry.Admin.Schema.Search; public static class DuplicateDetectionDocumentMapping { public const string DuplicateAnalyzer = "duplicate_analyzer"; + public const string DuplicateFullNameAnalyzer = "duplicate_fullname_analyzer"; public static TypeMappingDescriptor Get(TypeMappingDescriptor map) => map @@ -15,8 +16,16 @@ public static TypeMappingDescriptor Get(TypeMappingD .Name(document => document.VCode)) .Text( propertyDescriptor => propertyDescriptor - .Name(document => document.Naam) - .Analyzer(DuplicateAnalyzer)) + .Name(document => document.Naam) + .Fields(fields => fields + .Text(subField => subField + .Name(x => x.Naam) + .Analyzer(DuplicateAnalyzer) + ) + .Text(subField => subField + .Name("naamFull") + .Analyzer(DuplicateFullNameAnalyzer) + ))) .Text(propertyDescriptor => propertyDescriptor .Name(document => document.KorteNaam) ) diff --git a/test/AssociationRegistry.Test.Admin.Api/AssociationRegistry.Test.Admin.Api.csproj b/test/AssociationRegistry.Test.Admin.Api/AssociationRegistry.Test.Admin.Api.csproj index 0f7500b0b..0c2f98ff7 100644 --- a/test/AssociationRegistry.Test.Admin.Api/AssociationRegistry.Test.Admin.Api.csproj +++ b/test/AssociationRegistry.Test.Admin.Api/AssociationRegistry.Test.Admin.Api.csproj @@ -23,6 +23,8 @@ + + diff --git a/test/AssociationRegistry.Test.Admin.Api/DuplicateDetection/Given_An_Extensive_DataSet/Seed/DuplicateDetectionSeedLine.cs b/test/AssociationRegistry.Test.Admin.Api/DuplicateDetection/Given_An_Extensive_DataSet/Seed/DuplicateDetectionSeedLine.cs new file mode 100644 index 000000000..7c12850cb --- /dev/null +++ b/test/AssociationRegistry.Test.Admin.Api/DuplicateDetection/Given_An_Extensive_DataSet/Seed/DuplicateDetectionSeedLine.cs @@ -0,0 +1,7 @@ +namespace AssociationRegistry.Test.Admin.Api.DuplicateDetection.Given_An_Extensive_DataSet; + +using CsvHelper.Configuration.Attributes; + +public record DuplicateDetectionSeedLine( + [property: Name("Naam")] string Naam, + [property: Name("TeRegistrerenNaam")] string TeRegistrerenNaam); diff --git a/test/AssociationRegistry.Test.Admin.Api/DuplicateDetection/Given_An_Extensive_DataSet/Seed/DuplicateDetectionTest.cs b/test/AssociationRegistry.Test.Admin.Api/DuplicateDetection/Given_An_Extensive_DataSet/Seed/DuplicateDetectionTest.cs new file mode 100644 index 000000000..d9a116089 --- /dev/null +++ b/test/AssociationRegistry.Test.Admin.Api/DuplicateDetection/Given_An_Extensive_DataSet/Seed/DuplicateDetectionTest.cs @@ -0,0 +1,215 @@ +namespace AssociationRegistry.Test.Admin.Api.DuplicateDetection.Given_An_Extensive_DataSet; + +using AssociationRegistry.Admin.Api.Adapters.DuplicateVerenigingDetectionService; +using AssociationRegistry.Admin.ProjectionHost.Infrastructure.ElasticSearch; +using AssociationRegistry.Admin.Schema.Search; +using AutoFixture; +using Common.AutoFixture; +using CsvHelper; +using CsvHelper.Configuration; +using FluentAssertions; +using Microsoft.Extensions.Logging.Abstractions; +using Nest; +using System.Collections.ObjectModel; +using System.Globalization; +using Vereniging; +using Xunit; +using AssociationRegistry.Admin.Api.Infrastructure.Extensions; +using AssociationRegistry.Hosts.Configuration.ConfigurationBindings; +using DuplicateVerenigingDetection; +using Microsoft.Extensions.Logging; +using Xunit.Abstractions; +using LogLevel = Nest.LogLevel; + +public class DuplicateDetectionTest +{ + private readonly Adres? _adres; + private readonly Fixture _fixture; + private readonly ElasticClient _elastic; + private readonly string _duplicateDetectionIndex; + private SearchDuplicateVerenigingDetectionService _duplicateVerenigingDetectionService; + public IReadOnlyCollection VerwachteDubbels { get; private set; } + public IReadOnlyCollection VerwachteUnieke { get; private set; } + + public DuplicateDetectionTest(string duplicateDetectionIndex, ITestOutputHelper helper) + { + _fixture = new Fixture().CustomizeAdminApi(); + _duplicateDetectionIndex = duplicateDetectionIndex; + + _elastic = ElasticSearchExtensions.CreateElasticClient(new ElasticSearchOptionsSection() + { + Uri = "http://localhost:9200", + Username = "elastic", + Password = "local_development", + Indices = new ElasticSearchOptionsSection.IndicesOptionsSection() + { + DuplicateDetection = _duplicateDetectionIndex, + } + }, new TestOutputLogger(helper, duplicateDetectionIndex)); + + _adres = _fixture.Create() with + { + Postcode = "8500", + Gemeente = Gemeentenaam.Hydrate("Kortrijk"), + }; + + InitializeAsync().GetAwaiter().GetResult(); + } + + + + public async Task InsertGeregistreerdeVerenigingen(IReadOnlyCollection readVerwachtDubbels) + { + var toRegisterDuplicateDetectionDocuments = readVerwachtDubbels.Select(x => new DuplicateDetectionDocument() with + { + Naam = x.Naam, + VerenigingsTypeCode = Verenigingstype.FeitelijkeVereniging.Code, + HoofdactiviteitVerenigingsloket = [], + Locaties = [_fixture.Create() with{ Gemeente = _adres.Gemeente.Naam, Postcode = _adres.Postcode}] + }); + + foreach (var doc in toRegisterDuplicateDetectionDocuments) + { + await _elastic.IndexDocumentAsync(doc); + } + + await _elastic.Indices.RefreshAsync(Indices.AllIndices); + } + + public static IReadOnlyCollection ReadSeed(string associationregistryTestAdminApiDuplicatedetectionGivenAnExtensiveDatasetVerwachtdubbelsCsv) + => ReadSeedFile(associationregistryTestAdminApiDuplicatedetectionGivenAnExtensiveDatasetVerwachtdubbelsCsv); + + private static IReadOnlyCollection ReadSeedFile(string associationregistryTestAdminApiDuplicatedetectionGivenAnExtensiveDatasetVerwachtdubbelsCsv) + { + var resourceName = associationregistryTestAdminApiDuplicatedetectionGivenAnExtensiveDatasetVerwachtdubbelsCsv; + var assembly = typeof(Then_Some_Duplicates_Are_Expected).Assembly; + var stream = assembly.GetResource(resourceName); + + using var streamReader = new StreamReader(stream); + using var csvReader = new CsvReader(streamReader, new CsvConfiguration(CultureInfo.InvariantCulture) + { + Delimiter = ",", + HasHeaderRecord = true, + Quote = '"', + }); + + var records = csvReader.GetRecords() + .ToArray(); + + return new ReadOnlyCollection(records); + } + + public async Task InitializeAsync() + { + if(_elastic.Indices.ExistsAsync(_duplicateDetectionIndex).GetAwaiter().GetResult().Exists) + _elastic.Indices.DeleteAsync(_duplicateDetectionIndex).GetAwaiter().GetResult(); + + _elastic.Indices.CreateDuplicateDetectionIndex(_duplicateDetectionIndex); + + _duplicateVerenigingDetectionService = new SearchDuplicateVerenigingDetectionService(_elastic, NullLogger.Instance); + + VerwachteDubbels = ReadSeed("AssociationRegistry.Test.Admin.Api.DuplicateDetection.Given_An_Extensive_DataSet.Seed.verwachte_dubbels.csv"); + VerwachteUnieke = ReadSeed("AssociationRegistry.Test.Admin.Api.DuplicateDetection.Given_An_Extensive_DataSet.Seed.verwachte_unieke.csv"); + await InsertGeregistreerdeVerenigingen(VerwachteDubbels); + } + + public Task DisposeAsync() + => Task.CompletedTask; + + public async Task> GetDuplicatesFor(string teRegistrerenNaam) + => await _duplicateVerenigingDetectionService.GetDuplicates(VerenigingsNaam.Create(teRegistrerenNaam), + [ + _fixture.Create() with + { + Adres = _adres, + }, + ]); +} + +public class TestOutputLogger : ILogger +{ + private readonly ITestOutputHelper _outputHelper; + private readonly string _categoryName; + + public TestOutputLogger(ITestOutputHelper outputHelper, string categoryName) + { + _outputHelper = outputHelper ?? throw new ArgumentNullException(nameof(outputHelper)); + _categoryName = categoryName; + } + + public IDisposable BeginScope(TState state) + { + return null; // Scopes are not implemented + } + + public bool IsEnabled(Microsoft.Extensions.Logging.LogLevel logLevel) + => true; + + public void Log(Microsoft.Extensions.Logging.LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func formatter) + { + if (!IsEnabled(logLevel)) + { + return; + } + + if (formatter == null) + { + throw new ArgumentNullException(nameof(formatter)); + } + + var message = formatter(state, exception); + + if (!string.IsNullOrEmpty(message)) + { + var logEntry = $"[{logLevel}] {_categoryName}: {message}"; + _outputHelper.WriteLine(logEntry); + } + + if (exception != null) + { + _outputHelper.WriteLine(exception.ToString()); + } + if (!IsEnabled(logLevel)) + { + return; + } + + if (formatter == null) + { + throw new ArgumentNullException(nameof(formatter)); + } + + var msg = formatter(state, exception); + + if (!string.IsNullOrEmpty(msg)) + { + var logEntry = $"[{logLevel}] {_categoryName}: {msg}"; + _outputHelper.WriteLine(logEntry); + } + + if (exception != null) + { + _outputHelper.WriteLine(exception.ToString()); + } + } +} + +public class TestOutputLoggerProvider : ILoggerProvider +{ + private readonly ITestOutputHelper _outputHelper; + + public TestOutputLoggerProvider(ITestOutputHelper outputHelper) + { + _outputHelper = outputHelper; + } + + public ILogger CreateLogger(string categoryName) + { + return new TestOutputLogger(_outputHelper, categoryName); + } + + public void Dispose() + { + // No resources to dispose + } +} diff --git a/test/AssociationRegistry.Test.Admin.Api/DuplicateDetection/Given_An_Extensive_DataSet/Seed/verwachte_dubbels.csv b/test/AssociationRegistry.Test.Admin.Api/DuplicateDetection/Given_An_Extensive_DataSet/Seed/verwachte_dubbels.csv new file mode 100644 index 000000000..41b109553 --- /dev/null +++ b/test/AssociationRegistry.Test.Admin.Api/DuplicateDetection/Given_An_Extensive_DataSet/Seed/verwachte_dubbels.csv @@ -0,0 +1,21 @@ +Naam,TeRegistrerenNaam +Sint-jozefskoor,Sintjozefskoor +Sint - godelieve pius XI. 50/37 Scouts Berchem,50 Pius XI-37 Sint-Godelieve +VZW KSA Mopertingen,Mopertingen KSA +Chiro Mariakerke,ChiroMariakerke +Pollyanna chiromeisjes,Chiro Pollyanna +dorpsraad Muizenleeft,dorpsraad Muizen-leeft +Fanfare Sint-Cecilia,Koninklijke Fanfare Sinte-Cecilia Londerzeel centrum +Jongenschiro Balegem Sint Jozef,Jongenschiro Balegem +oostkapjes KSA,KSA OOSTKAPJES +Wrestling Oostende,Wrestling Lions Ostende +Vrienden van de omkeer,Vrienden van de ommekeer +missienaaikring bellegem,missiekring bellegem +Postzegelclub “De Leiestreek” Bissegem,Postzegelclub De leiestreek Bissegem +Rommelmarkt Meremstraat + BBQ,Rommelmarkt Merem(straat) +Helado,Helo +DE BERGPALLIETERS,Bierpallieters +Kriko,Scouts kriko +Plica,Plica vzw +Designregio Kortrijk,Designregio +Buurtcomité,buurtfeestcomité diff --git a/test/AssociationRegistry.Test.Admin.Api/DuplicateDetection/Given_An_Extensive_DataSet/Seed/verwachte_unieke.csv b/test/AssociationRegistry.Test.Admin.Api/DuplicateDetection/Given_An_Extensive_DataSet/Seed/verwachte_unieke.csv new file mode 100644 index 000000000..30bd7b2d9 --- /dev/null +++ b/test/AssociationRegistry.Test.Admin.Api/DuplicateDetection/Given_An_Extensive_DataSet/Seed/verwachte_unieke.csv @@ -0,0 +1,4 @@ +Naam,TeRegistrerenNaam +Basketbal Oostende,Wrestling Oostende +Roundnet Kortrijk,Designregio Kortrijk & Bruggenloop Kortrijk +Gezinsbadminton Heule,Koninklijke Schuttergilde Willem Tell Heule diff --git a/test/AssociationRegistry.Test.Admin.Api/DuplicateDetection/Given_An_Extensive_DataSet/Then_Some_Duplicates_Are_Expected.cs b/test/AssociationRegistry.Test.Admin.Api/DuplicateDetection/Given_An_Extensive_DataSet/Then_Some_Duplicates_Are_Expected.cs new file mode 100644 index 000000000..316fb813b --- /dev/null +++ b/test/AssociationRegistry.Test.Admin.Api/DuplicateDetection/Given_An_Extensive_DataSet/Then_Some_Duplicates_Are_Expected.cs @@ -0,0 +1,24 @@ +namespace AssociationRegistry.Test.Admin.Api.DuplicateDetection.Given_An_Extensive_DataSet; + +using FluentAssertions; +using Xunit; +using Xunit.Abstractions; + +public class Then_Some_Duplicates_Are_Expected: DuplicateDetectionTest +{ + + public Then_Some_Duplicates_Are_Expected(ITestOutputHelper helper) : base("duplicates", helper) + { + } + + [Fact] + public async Task With_Expected_Vereniging_In_Duplicate_List() + { + await Assert.AllAsync(VerwachteDubbels, async verwachteDubbel => + { + var duplicates = await GetDuplicatesFor(verwachteDubbel.TeRegistrerenNaam); + + duplicates.Select(x => x.Naam).Should().Contain(verwachteDubbel.Naam, because: $"Expected '{verwachteDubbel.Naam}' to be found in all duplicate names, but found mismatch."); + }); + } +} diff --git a/test/AssociationRegistry.Test.Admin.Api/DuplicateDetection/Given_An_Extensive_DataSet/Then_Some_Duplicates_Are_Not_Expected.cs b/test/AssociationRegistry.Test.Admin.Api/DuplicateDetection/Given_An_Extensive_DataSet/Then_Some_Duplicates_Are_Not_Expected.cs new file mode 100644 index 000000000..97101707e --- /dev/null +++ b/test/AssociationRegistry.Test.Admin.Api/DuplicateDetection/Given_An_Extensive_DataSet/Then_Some_Duplicates_Are_Not_Expected.cs @@ -0,0 +1,26 @@ +namespace AssociationRegistry.Test.Admin.Api.DuplicateDetection.Given_An_Extensive_DataSet; + +using FluentAssertions; +using Xunit; +using Xunit.Abstractions; + +public class Then_Some_Duplicates_Are_Not_Expected: DuplicateDetectionTest +{ + public Then_Some_Duplicates_Are_Not_Expected(ITestOutputHelper helper): base("uniques", helper) + { + } + + [Fact] + public async Task With_Expected_Vereniging_In_Duplicate_List() + { + await Assert.AllAsync(VerwachteUnieke, async verwachteDubbel => + { + var duplicates = await GetDuplicatesFor(verwachteDubbel.TeRegistrerenNaam); + + duplicates.Select(x => x.Naam).Should().NotContain(verwachteDubbel.Naam, + because: + $"Expected '{verwachteDubbel.Naam}' to be found in all duplicate names, but found mismatch."); + }); + } +} + diff --git a/test/AssociationRegistry.Test.E2E/Framework/ApiSetup/FullBlownApiSetup.cs b/test/AssociationRegistry.Test.E2E/Framework/ApiSetup/FullBlownApiSetup.cs index 696a59f2c..3e205eab7 100644 --- a/test/AssociationRegistry.Test.E2E/Framework/ApiSetup/FullBlownApiSetup.cs +++ b/test/AssociationRegistry.Test.E2E/Framework/ApiSetup/FullBlownApiSetup.cs @@ -1,11 +1,14 @@ namespace AssociationRegistry.Test.E2E.Framework.ApiSetup; using Admin.Api; +using Admin.Api.Infrastructure.Extensions; using Alba; using AlbaHost; using Amazon.SQS; using AssociationRegistry.Framework; using Common.Clients; +using Hosts.Configuration; +using Hosts.Configuration.ConfigurationBindings; using IdentityModel.AspNetCore.OAuth2Introspection; using Marten; using Marten.Events; @@ -13,6 +16,8 @@ using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; +using Nest; using NodaTime; using NodaTime.Text; using Oakton; @@ -46,6 +51,10 @@ public async Task InitializeAsync() var clients = new Clients(adminApiHost.Services.GetRequiredService(), createClientFunc: () => new HttpClient()); + var elasticSearchOptions = AdminApiConfiguration.GetElasticSearchOptionsSection(); + ElasticClient = ElasticSearchExtensions.CreateElasticClient(elasticSearchOptions, NullLogger.Instance); + ElasticClient.Indices.DeleteAsync(elasticSearchOptions.Indices.DuplicateDetection).GetAwaiter().GetResult(); + SuperAdminHttpClient = clients.SuperAdmin.HttpClient; AdminApiHost = adminApiHost.EnsureEachCallIsAuthenticated(clients.Authenticated.HttpClient); @@ -70,9 +79,14 @@ public async Task InitializeAsync() SqsClientWrapper = AdminApiHost.Services.GetRequiredService(); AmazonSqs = AdminApiHost.Services.GetRequiredService(); + ElasticClient = AdminApiHost.Services.GetRequiredService(); + + + await AdminApiHost.DocumentStore().Storage.ApplyAllConfiguredChangesToDatabaseAsync(); } + public IElasticClient ElasticClient { get; set; } public HttpClient SuperAdminHttpClient { get; private set; } private void SetUpAdminApiConfiguration() diff --git a/test/AssociationRegistry.Test.E2E/Scenarios/Givens/FeitelijkeVereniging/MultipleWerdenGeregistreerdWithGemeentenaamInVerenigingsnaamScenario.cs b/test/AssociationRegistry.Test.E2E/Scenarios/Givens/FeitelijkeVereniging/MultipleWerdenGeregistreerdWithGemeentenaamInVerenigingsnaamScenario.cs new file mode 100644 index 000000000..1d38eccd8 --- /dev/null +++ b/test/AssociationRegistry.Test.E2E/Scenarios/Givens/FeitelijkeVereniging/MultipleWerdenGeregistreerdWithGemeentenaamInVerenigingsnaamScenario.cs @@ -0,0 +1,61 @@ +namespace AssociationRegistry.Test.E2E.Scenarios.Givens.FeitelijkeVereniging; + +using Events; +using EventStore; +using AssociationRegistry.Framework; +using AssociationRegistry.Test.Common.AutoFixture; +using Vereniging; +using AutoFixture; + +public class MultipleWerdenGeregistreerdWithGemeentenaamInVerenigingsnaamScenario : Framework.TestClasses.IScenario +{ + private CommandMetadata Metadata; + + public MultipleWerdenGeregistreerdWithGemeentenaamInVerenigingsnaamScenario() + { + } + + public async Task[]> GivenEvents(IVCodeService service) + { + var fixture = new Fixture().CustomizeAdminApi(); + + + + Metadata = fixture.Create() with { ExpectedVersion = null }; + + var events = fixture.CreateMany(17).ToArray(); + + events[0] = events[0] with { Naam = "KORTRIJK SPURS" }; + events[1] = events[1] with { Naam = "JUDOSCHOOL KORTRIJK" }; + events[2] = events[2] with { Naam = "Lebad Kortrijk" }; + events[3] = events[3] with { Naam = "Reddersclub Kortrijk" }; + events[4] = events[4] with { Naam = "Koninklijke Turnvereniging Kortrijk" }; + events[5] = events[5] with { Naam = "JONG KORTRIJK VOETBALT" }; + events[6] = events[6] with { Naam = "Kortrijkse Zwemkring" }; + events[7] = events[7] with { Naam = "KORTRIJKS SYMFONISCH ORKEST" }; + events[8] = events[8] with { Naam = "KONINKLIJK KORTRIJK SPORT ATLETIEK" }; + events[9] = events[9] with { Naam = "Kortrijkse Ultimate Frisbee Club" }; + events[10] = events[10] with { Naam = "Ruygi KORTRIJK" }; + events[11] = events[11] with { Naam = "Ruygo Judoschool KORTRIJK" }; + events[12] = events[12] with { Naam = "Schaakclub Kortrijk" }; + events[13] = events[13] with { Naam = "Wielerclub FC De ratjes" }; + events[14] = events[14] with { Naam = "Club Kortrijk" }; + events[15] = events[15] with { Naam = "Kortrijkse C# fanclub" }; + events[16] = events[16] with { Naam = "Clubben met de vrienden" }; + + + events = events.Select( + @event => @event with + { + Locaties = [@event.Locaties.First() with { Adres = @event.Locaties.First().Adres with { Postcode = "8500" } }] + } + ).ToArray(); + + return events.Select(x => new KeyValuePair(x.VCode, [x])).ToArray(); + } + + public StreamActionResult Result { get; set; } = null!; + + public CommandMetadata GetCommandMetadata() + => Metadata; +} diff --git a/test/AssociationRegistry.Test.E2E/When_Registreer_FeitelijkeVereniging_With_Duplicates_With_Gemeentenaam_In_Verenigingsnaam/Beheer/Detail/Returns_Conflict.cs b/test/AssociationRegistry.Test.E2E/When_Registreer_FeitelijkeVereniging_With_Duplicates_With_Gemeentenaam_In_Verenigingsnaam/Beheer/Detail/Returns_Conflict.cs new file mode 100644 index 000000000..6e6df43a8 --- /dev/null +++ b/test/AssociationRegistry.Test.E2E/When_Registreer_FeitelijkeVereniging_With_Duplicates_With_Gemeentenaam_In_Verenigingsnaam/Beheer/Detail/Returns_Conflict.cs @@ -0,0 +1,122 @@ +namespace AssociationRegistry.Test.E2E.When_Registreer_FeitelijkeVereniging_With_Duplicates_With_Gemeentenaam_In_Verenigingsnaam.Beheer.Detail; + +using Admin.Api.Infrastructure; +using Alba; +using AssociationRegistry.Admin.Api.DecentraalBeheer.Verenigingen.Common; +using AssociationRegistry.Admin.Api.DecentraalBeheer.Verenigingen.Registreer.FeitelijkeVereniging.RequetsModels; +using AutoFixture; +using Common.AutoFixture; +using FluentAssertions; +using Newtonsoft.Json.Linq; +using System.Net; +using Xunit; + +[Collection(FullBlownApiCollection.Name)] +public class Returns_Conflict : IClassFixture, IAsyncLifetime +{ + private readonly RegistreerFeitelijkeVerenigingenWithGemeentenaamInVerenigingsnaamContext _context; + + + public Returns_Conflict(RegistreerFeitelijkeVerenigingenWithGemeentenaamInVerenigingsnaamContext context) + { + _context = context; + } + + [Theory] + [MemberData(nameof(Scenarios))] + public async Task WithDuplicateVerenigingen(RegistreerFeitelijkeVerenigingRequest request, string[] expectedDuplicateVerenigingen) + { + var response = await (await _context.ApiSetup.AdminApiHost.Scenario(s => + { + s.Post + .Json(request, JsonStyle.Mvc) + .ToUrl("/v1/verenigingen/feitelijkeverenigingen"); + + s.StatusCodeShouldBe(HttpStatusCode.Conflict); + + s.Header(WellknownHeaderNames.Sequence).ShouldNotBeWritten(); + })).ReadAsTextAsync(); + + ExtractDuplicateVerenigingsnamen(response).Should().BeEquivalentTo(expectedDuplicateVerenigingen, + because: $"'{request.Naam}' did not expect these duplicates"); + } + + + public static IEnumerable Scenarios() + { + var autoFixture = new Fixture().CustomizeAdminApi(); + + yield return + [ + RegistreerFeitelijkeVerenigingRequest(autoFixture, "Ultimate Frisbee club"), + new[] + { + "Kortrijkse Ultimate Frisbee Club", + }, + ]; + + yield return + [ + RegistreerFeitelijkeVerenigingRequest(autoFixture, "Ryugi Kortrijk"), + new[] + { + "Ruygi KORTRIJK", + "Ruygo Judoschool KORTRIJK" + }, + ]; + + yield return + [ + RegistreerFeitelijkeVerenigingRequest(autoFixture, "Judo School Kortrijk"), + new[] + { + "JUDOSCHOOL KORTRIJK", + }, + ]; + + yield return + [ + RegistreerFeitelijkeVerenigingRequest(autoFixture, "Ryugi"), + new[] + { + "Ruygi KORTRIJK", + "Ruygo Judoschool KORTRIJK" + }, + ]; + + yield return + [ + RegistreerFeitelijkeVerenigingRequest(autoFixture, "Osu Judoschool Kortrijk"), + new[] + { + "JUDOSCHOOL KORTRIJK", + }, + ]; + } + + private static RegistreerFeitelijkeVerenigingRequest RegistreerFeitelijkeVerenigingRequest(Fixture autoFixture, string verenigingsnaam) + { + var request = autoFixture.Create(); + request.Locaties = autoFixture.CreateMany().ToArray(); + request.Naam = verenigingsnaam; + request.Locaties[0].Adres.Postcode = "8500"; + request.Locaties[0].Adres.Gemeente = "FictieveGemeentenaam"; + + return request; + } + + private static IEnumerable ExtractDuplicateVerenigingsnamen(string responseContent) + { + var duplicates = JObject.Parse(responseContent) + .SelectTokens("$.mogelijkeDuplicateVerenigingen[*].naam") + .Select(x => x.ToString()); + + return duplicates; + } + + public async Task InitializeAsync() + => await Task.CompletedTask; + + public async Task DisposeAsync() + => await Task.CompletedTask; +} diff --git a/test/AssociationRegistry.Test.E2E/When_Registreer_FeitelijkeVereniging_With_Duplicates_With_Gemeentenaam_In_Verenigingsnaam/RegistreerFeitelijkeVerenigingenWithGemeentenaamInVerenigingsnaamContext.cs b/test/AssociationRegistry.Test.E2E/When_Registreer_FeitelijkeVereniging_With_Duplicates_With_Gemeentenaam_In_Verenigingsnaam/RegistreerFeitelijkeVerenigingenWithGemeentenaamInVerenigingsnaamContext.cs new file mode 100644 index 000000000..e9d33dc73 --- /dev/null +++ b/test/AssociationRegistry.Test.E2E/When_Registreer_FeitelijkeVereniging_With_Duplicates_With_Gemeentenaam_In_Verenigingsnaam/RegistreerFeitelijkeVerenigingenWithGemeentenaamInVerenigingsnaamContext.cs @@ -0,0 +1,93 @@ +namespace AssociationRegistry.Test.E2E.When_Registreer_FeitelijkeVereniging_With_Duplicates_With_Gemeentenaam_In_Verenigingsnaam; + +using Admin.Api.DecentraalBeheer.Verenigingen.Common; +using Admin.Api.Infrastructure; +using Alba; +using AssociationRegistry.Admin.Api.DecentraalBeheer.Verenigingen.Registreer.FeitelijkeVereniging.RequetsModels; +using AssociationRegistry.Test.E2E.Framework.ApiSetup; +using AssociationRegistry.Test.E2E.Framework.TestClasses; +using AssociationRegistry.Test.E2E.Scenarios.Givens.FeitelijkeVereniging; +using AssociationRegistry.Test.E2E.Scenarios.Requests.FeitelijkeVereniging; +using AssociationRegistry.Vereniging; +using AutoFixture; +using Common.AutoFixture; +using Events; +using Marten.Events; +using Microsoft.Extensions.DependencyInjection; +using Nest; +using Newtonsoft.Json.Linq; +using Scenarios.Requests; +using System.Net; + +public class RegistreerFeitelijkeVerenigingenWithGemeentenaamInVerenigingsnaamContext: TestContextBase +{ + public class TestData + { + public string KortrijkSpurs { get; set; } = $"{FictieveGemeentenaam.ToUpper()} SPURS"; + public string JudoschoolKortrijk { get; set; } = $"JUDOSCHOOL {FictieveGemeentenaam.ToUpper()}"; + public string LebadKortrijk { get; set; } = $"Lebad {FictieveGemeentenaam}"; + public string ReddersclubKortrijk { get; set; } = $"Reddersclub {FictieveGemeentenaam}"; + public string KoninklijkeTurnverenigingKortrijk { get; set; } = $"Koninklijke Turnvereniging {FictieveGemeentenaam}"; + public string JongKortrijkVoetbalt { get; set; } = $"JONG {FictieveGemeentenaam} VOETBALT"; + public string KortrijkseZwemkring { get; set; } = $"{FictieveGemeentenaam}se Zwemkring"; + public string KortrijksSymfonischOrkest { get; set; } = $"{FictieveGemeentenaam}se SYMFONISCH ORKEST"; + public string KoninklijkKortrijkSportAtletiek { get; set; } = $"KONINKLIJK {FictieveGemeentenaam} SPORT ATLETIEK"; + public string KortrijkseUltimateFrisbeeClub { get; set; } = $"{FictieveGemeentenaam}se Ultimate Frisbee Club"; + public string RuygiKortrijk { get; set; } = $"Ruygi {FictieveGemeentenaam}"; + public string RuygoJudoschoolKortrijk { get; set; } = $"Ruygo Judoschool {FictieveGemeentenaam}"; + + public List Events { get; set; } + public static string FictieveGemeentenaam = "FictieveGemeentenaam"; + public static string FictievePostcode = "8500"; + + public TestData() + { + var fixture = new Fixture().CustomizeAdminApi(); + + Events = new List + { + KortrijkSpurs, + JudoschoolKortrijk, + LebadKortrijk, + ReddersclubKortrijk, + KoninklijkeTurnverenigingKortrijk, + JongKortrijkVoetbalt, + KortrijkseZwemkring, + KortrijksSymfonischOrkest, + KoninklijkKortrijkSportAtletiek, + KortrijkseUltimateFrisbeeClub, + RuygiKortrijk, + RuygoJudoschoolKortrijk + }.Select((x, i) => fixture.Create() with + { + VCode = AssociationRegistry.Vereniging.VCode.Create(i + 32000), + Naam = x + }) + .Select( + @event => @event with + { + Locaties = [@event.Locaties.First() with { Adres = @event.Locaties.First().Adres with { Postcode = FictievePostcode } }] + } + ).ToList(); + } + + } + + private MultipleWerdenGeregistreerdWithGemeentenaamInVerenigingsnaamScenario _scenario; + + + public RegistreerFeitelijkeVerenigingenWithGemeentenaamInVerenigingsnaamContext(FullBlownApiSetup apiSetup) + { + ApiSetup = apiSetup; + _scenario = new(); + } + + public override async Task InitializeAsync() + { + await ApiSetup.ExecuteGiven(_scenario); + + await ApiSetup.AdminApiHost.WaitForNonStaleProjectionDataAsync(TimeSpan.FromSeconds(60)); + await ApiSetup.AdminProjectionHost.WaitForNonStaleProjectionDataAsync(TimeSpan.FromSeconds(10)); + await ApiSetup.AdminProjectionHost.Services.GetRequiredService().Indices.RefreshAsync(Indices.AllIndices); + } +}