Skip to content

Commit

Permalink
feat: or-2006 prevent regsitreer kbo from being instered twice
Browse files Browse the repository at this point in the history
  • Loading branch information
QuintenGreenstack committed Dec 12, 2023
1 parent c644cfc commit cda36d1
Show file tree
Hide file tree
Showing 23 changed files with 567 additions and 34 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
using AssociationRegistry.Magda.Models;
using ConfigurationBindings;
using Constants;
using EventStore.Locks;
using JasperFx.CodeGeneration;
using Json;
using Marten;
Expand Down Expand Up @@ -38,6 +39,8 @@ public static IServiceCollection AddMarten(
opts.RegisterDocumentType<BeheerVerenigingDetailDocument>();
opts.RegisterDocumentType<BeheerVerenigingHistoriekDocument>();

opts.RegisterDocumentType<KboLockDocument>();

opts.RegisterDocumentType<VerenigingState>();

opts.Schema.For<MagdaCallReference>().Identity(x => x.Reference);
Expand Down
1 change: 1 addition & 0 deletions src/AssociationRegistry.Admin.Api/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -299,6 +299,7 @@ private static void ConfigureServices(WebApplicationBuilder builder)
.AddScoped<ICommandMetadataProvider, CommandMetadataProvider>()
.AddSingleton<IClock, Clock>()
.AddTransient<IEventStore, EventStore>()
.AddTransient<ILockStore, LockStore>()
.AddTransient<IVerenigingsRepository, VerenigingsRepository>()
.AddTransient<IDuplicateVerenigingDetectionService, SearchDuplicateVerenigingDetectionService>()
.AddTransient<IMagdaGeefVerenigingService, MagdaGeefVerenigingService>()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,19 +23,45 @@ public RegistreerVerenigingUitKboCommandHandler(
_magdaGeefVerenigingService = magdaGeefVerenigingService;
}

public async Task<Result> Handle(CommandEnvelope<RegistreerVerenigingUitKboCommand> message, CancellationToken cancellationToken = default)
public async Task<Result> Handle(
CommandEnvelope<RegistreerVerenigingUitKboCommand> message,
CancellationToken cancellationToken = default)
{
var command = message.Command;

var duplicateResult = await CheckForDuplicate(command.KboNummer);
for (var i = 0; i <= 60; i += 3)
{
var duplicateResult = await CheckForDuplicate(command.KboNummer);

if (duplicateResult.IsFailure()) return duplicateResult;
if (duplicateResult.IsFailure()) return duplicateResult;

var kboLockDocument = await _verenigingsRepository.GetKboNummerLock(command.KboNummer);
var hasLock = kboLockDocument is not null;

// Lock door andere instance dus even wachten
if (hasLock)
{
await Task.Delay(3 * 1000, cancellationToken);

continue;
}

try
{
await _verenigingsRepository.SetKboNummerLock(command.KboNummer);
var vereniging = await _magdaGeefVerenigingService.GeefVereniging(command.KboNummer, message.Metadata, cancellationToken);

var vereniging = await _magdaGeefVerenigingService.GeefVereniging(command.KboNummer, message.Metadata, cancellationToken);
if (vereniging.IsFailure()) throw new GeenGeldigeVerenigingInKbo();

if (vereniging.IsFailure()) throw new GeenGeldigeVerenigingInKbo();
return await RegistreerVereniging(vereniging, message.Metadata, cancellationToken);
}
finally
{
await _verenigingsRepository.DeleteKboNummerLock(command.KboNummer);
}
}

return await RegistreerVereniging(vereniging, message.Metadata, cancellationToken);
throw new ApplicationException($"Kan niet langer wachten op lock voor KBO nummer {command.KboNummer}");
}

private async Task<Result> CheckForDuplicate(KboNummer kboNummer)
Expand All @@ -45,7 +71,10 @@ private async Task<Result> CheckForDuplicate(KboNummer kboNummer)
return duplicateKbo is not null ? DuplicateKboFound.WithVcode(duplicateKbo.VCode!) : Result.Success();
}

private async Task<Result> RegistreerVereniging(VerenigingVolgensKbo verenigingVolgensKbo, CommandMetadata messageMetadata, CancellationToken cancellationToken)
private async Task<Result> RegistreerVereniging(
VerenigingVolgensKbo verenigingVolgensKbo,
CommandMetadata messageMetadata,
CancellationToken cancellationToken)
{
var vCode = await _vCodeService.GetNext();

Expand Down
12 changes: 12 additions & 0 deletions src/AssociationRegistry/EventStore/ILockStore.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
namespace AssociationRegistry.EventStore;

using Locks;
using Vereniging;

public interface ILockStore
{
Task<KboLockDocument?> GetKboNummerLock(KboNummer kboNummer);
Task SetKboNummerLock(KboNummer kboNummer);
Task DeleteKboNummerLock(KboNummer kboNummer);
Task CleanKboNummerLocks();
}
58 changes: 58 additions & 0 deletions src/AssociationRegistry/EventStore/LockStore.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
namespace AssociationRegistry.EventStore;

using Locks;
using Marten;
using Vereniging;

public class LockStore : ILockStore
{
private readonly IDocumentStore _documentStore;

public LockStore(IDocumentStore documentStore)
{
_documentStore = documentStore;
}

public async Task CleanKboNummerLocks()
{
await using var session = _documentStore.LightweightSession();
session.DeleteWhere<KboLockDocument>(doc => doc.CreatedAt <= DateTimeOffset.UtcNow.AddMinutes(-1));
await session.SaveChangesAsync();
}

public async Task<KboLockDocument?> GetKboNummerLock(KboNummer kboNummer)
{
try
{
await using var session = _documentStore.QuerySession();

return await session.LoadAsync<KboLockDocument>(kboNummer);
}
catch
{
return await Task.FromResult<KboLockDocument?>(null);
}
}

public async Task SetKboNummerLock(KboNummer kboNummer)
{
await using var session = _documentStore.LightweightSession();

session.Store(new KboLockDocument
{
KboNummer = kboNummer,
CreatedAt = DateTimeOffset.UtcNow,
});

await session.SaveChangesAsync();
}

public async Task DeleteKboNummerLock(KboNummer kboNummer)
{
await using var session = _documentStore.LightweightSession();

session.Delete<KboLockDocument>(kboNummer);

await session.SaveChangesAsync();
}
}
11 changes: 11 additions & 0 deletions src/AssociationRegistry/EventStore/Locks/KboLockDocument.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
namespace AssociationRegistry.EventStore.Locks;

using Marten.Schema;

public class KboLockDocument
{
[Identity]
public string KboNummer { get; set; }

Check warning on line 8 in src/AssociationRegistry/EventStore/Locks/KboLockDocument.cs

View workflow job for this annotation

GitHub Actions / analyze-code

Non-nullable property 'KboNummer' must contain a non-null value when exiting constructor. Consider declaring the property as nullable.

Check warning on line 8 in src/AssociationRegistry/EventStore/Locks/KboLockDocument.cs

View workflow job for this annotation

GitHub Actions / Run Tests (test/AssociationRegistry.Test)

Non-nullable property 'KboNummer' must contain a non-null value when exiting constructor. Consider declaring the property as nullable.

Check warning on line 8 in src/AssociationRegistry/EventStore/Locks/KboLockDocument.cs

View workflow job for this annotation

GitHub Actions / Run Tests (test/AssociationRegistry.Test.Public.Api)

Non-nullable property 'KboNummer' must contain a non-null value when exiting constructor. Consider declaring the property as nullable.

Check warning on line 8 in src/AssociationRegistry/EventStore/Locks/KboLockDocument.cs

View workflow job for this annotation

GitHub Actions / Run Tests (test/AssociationRegistry.Test.Acm.Api)

Non-nullable property 'KboNummer' must contain a non-null value when exiting constructor. Consider declaring the property as nullable.

Check warning on line 8 in src/AssociationRegistry/EventStore/Locks/KboLockDocument.cs

View workflow job for this annotation

GitHub Actions / Run Tests (test/AssociationRegistry.Test.Admin.Api)

Non-nullable property 'KboNummer' must contain a non-null value when exiting constructor. Consider declaring the property as nullable.

public DateTimeOffset CreatedAt { get; set; }
}
19 changes: 18 additions & 1 deletion src/AssociationRegistry/EventStore/VerenigingsRepository.cs
Original file line number Diff line number Diff line change
@@ -1,15 +1,19 @@
namespace AssociationRegistry.EventStore;

using Framework;
using Locks;
using Marten;
using Vereniging;

public class VerenigingsRepository : IVerenigingsRepository
{
private readonly IEventStore _eventStore;
private readonly ILockStore _lockStore;

public VerenigingsRepository(IEventStore eventStore)
public VerenigingsRepository(IEventStore eventStore, ILockStore lockStore)
{
_eventStore = eventStore;
_lockStore = lockStore;
}

public async Task<StreamActionResult> Save(
Expand Down Expand Up @@ -49,6 +53,19 @@ public async Task<TVereniging> Load<TVereniging>(VCode vCode, long? expectedVers
return new VCodeAndNaam(verenigingState.VCode, verenigingState.Naam);
}

public async Task<KboLockDocument?> GetKboNummerLock(KboNummer kboNummer)
{
await _lockStore.CleanKboNummerLocks();

return await _lockStore.GetKboNummerLock(kboNummer);
}

public async Task SetKboNummerLock(KboNummer kboNummer)
=> await _lockStore.SetKboNummerLock(kboNummer);

public async Task DeleteKboNummerLock(KboNummer kboNummer)
=> await _lockStore.DeleteKboNummerLock(kboNummer);

public record VCodeAndNaam(VCode? VCode, VerenigingsNaam VerenigingsNaam)
{
public static VCodeAndNaam Fallback(KboNummer kboNummer)
Expand Down
2 changes: 1 addition & 1 deletion src/AssociationRegistry/Kbo/VerenigingVolgensKbo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

using Vereniging;

public class VerenigingVolgensKbo
public record VerenigingVolgensKbo
{
public KboNummer KboNummer { get; init; } = null!;
public Verenigingstype Type { get; set; } = null!;
Expand Down
4 changes: 4 additions & 0 deletions src/AssociationRegistry/Vereniging/IVerenigingsRepository.cs
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
namespace AssociationRegistry.Vereniging;

using EventStore;
using EventStore.Locks;
using Framework;

public interface IVerenigingsRepository
{
Task<StreamActionResult> Save(VerenigingsBase vereniging, CommandMetadata metadata, CancellationToken cancellationToken);
Task<TVereniging> Load<TVereniging>(VCode vCode, long? expectedVersion) where TVereniging : IHydrate<VerenigingState>, new();
Task<VerenigingsRepository.VCodeAndNaam?> GetVCodeAndNaam(KboNummer kboNummer);
Task<KboLockDocument?> GetKboNummerLock(KboNummer kboNummer);
Task SetKboNummerLock(KboNummer kboNummer);
Task DeleteKboNummerLock(KboNummer kboNummer);
}
Original file line number Diff line number Diff line change
@@ -1,19 +1,35 @@
namespace AssociationRegistry.Test.Admin.Api.Fakes;

using AssociationRegistry.Framework;
using AutoFixture;
using Framework;
using Kbo;
using ResultNet;
using Vereniging;

public class MagdaGeefVerenigingNumberFoundMagdaGeefVerenigingService : IMagdaGeefVerenigingService
{
private readonly VerenigingVolgensKbo _verenigingVolgensKbo;
private readonly VerenigingVolgensKbo? _verenigingVolgensKbo;

public MagdaGeefVerenigingNumberFoundMagdaGeefVerenigingService(VerenigingVolgensKbo verenigingVolgensKbo)
public MagdaGeefVerenigingNumberFoundMagdaGeefVerenigingService(VerenigingVolgensKbo? verenigingVolgensKbo = null)
{
_verenigingVolgensKbo = verenigingVolgensKbo;
}

public Task<Result<VerenigingVolgensKbo>> GeefVereniging(KboNummer kboNummer, CommandMetadata metadata, CancellationToken cancellationToken)
=> Task.FromResult(VerenigingVolgensKboResult.GeldigeVereniging(_verenigingVolgensKbo));
public Task<Result<VerenigingVolgensKbo>> GeefVereniging(
KboNummer kboNummer,
CommandMetadata metadata,
CancellationToken cancellationToken)
=> Task.FromResult(VerenigingVolgensKboResult.GeldigeVereniging(_verenigingVolgensKbo ?? VerenigingVolgensKbo(kboNummer)));

private static VerenigingVolgensKbo VerenigingVolgensKbo(KboNummer kboNummer)
{
var v = new Fixture().CustomizeAdminApi().Create<VerenigingVolgensKbo>() with
{
KboNummer = kboNummer,
Type = Verenigingstype.VZW,
};

return v;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,14 @@

using AssociationRegistry.Framework;
using EventStore;
using EventStore.Locks;
using FluentAssertions;
using Test.Framework;
using Vereniging;

public class VerenigingRepositoryMock : IVerenigingsRepository
{
private readonly ILockStore _lockStore;
private VerenigingState? _verenigingToLoad;
private readonly VerenigingsRepository.VCodeAndNaam _moederVCodeAndNaam;
public record SaveInvocation(VerenigingsBase Vereniging);
Expand All @@ -17,8 +20,12 @@ private record InvocationLoad(VCode VCode, Type Type);
public List<SaveInvocation> SaveInvocations { get; } = new();
private readonly List<InvocationLoad> _invocationsLoad = new();

public VerenigingRepositoryMock(VerenigingState? verenigingToLoad = null, VerenigingsRepository.VCodeAndNaam moederVCodeAndNaam = null!)
public VerenigingRepositoryMock(
VerenigingState? verenigingToLoad = null,
VerenigingsRepository.VCodeAndNaam moederVCodeAndNaam = null!,
ILockStore? lockStore = null)
{
_lockStore = lockStore ?? new LockStoreMock();
_verenigingToLoad = verenigingToLoad;
_moederVCodeAndNaam = moederVCodeAndNaam;
}
Expand Down Expand Up @@ -53,6 +60,15 @@ public async Task<TVereniging> Load<TVereniging>(VCode vCode, long? expectedVers
public Task<VerenigingsRepository.VCodeAndNaam?> GetVCodeAndNaam(KboNummer kboNummer)
=> Task.FromResult(_moederVCodeAndNaam)!;

public async Task<KboLockDocument?> GetKboNummerLock(KboNummer kboNummer)
=> await _lockStore.GetKboNummerLock(kboNummer);

public async Task SetKboNummerLock(KboNummer kboNummer)
=> await _lockStore.SetKboNummerLock(kboNummer);

public async Task DeleteKboNummerLock(KboNummer kboNummer)
=> await _lockStore.DeleteKboNummerLock(kboNummer);

public void ShouldHaveLoaded<TVereniging>(params string[] vCodes) where TVereniging : IHydrate<VerenigingState>, new()
{
_invocationsLoad.Should().BeEquivalentTo(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
namespace AssociationRegistry.Test.Admin.Api.VerenigingMetRechtspersoonlijkheid.When_RegistreerVerenigingMetRechtspersoonlijkheid.
CommandHandling.
When_Duplicate_KboNummer;

using Acties.RegistreerVerenigingUitKbo;
using AssociationRegistry.Framework;
using AutoFixture;
using EventStore;
using Fakes;
using FluentAssertions;
using Framework;
using Kbo;
using Moq;
using Test.Framework;
using Xunit;
using Xunit.Categories;

[UnitTest]
public class WithLock_And_LockNotRemovedWhileWaiting
{
private readonly RegistreerVerenigingUitKboCommandHandler _commandHandler;
private readonly CommandEnvelope<RegistreerVerenigingUitKboCommand> _envelope;

public WithLock_And_LockNotRemovedWhileWaiting()
{
var fixture = new Fixture().CustomizeAdminApi();

var moederVCodeAndNaam = fixture.Create<VerenigingsRepository.VCodeAndNaam>();

_envelope = new CommandEnvelope<RegistreerVerenigingUitKboCommand>(fixture.Create<RegistreerVerenigingUitKboCommand>(),
fixture.Create<CommandMetadata>());

ILockStore lockStoreMock = new AlwaysLockStoreMock();
var repositoryMock = new VerenigingRepositoryMock(moederVCodeAndNaam: moederVCodeAndNaam, lockStore: lockStoreMock);

_commandHandler = new RegistreerVerenigingUitKboCommandHandler(
repositoryMock,
new InMemorySequentialVCodeService(),
Mock.Of<IMagdaGeefVerenigingService>());
}

[Fact]
public void Then_It_Throws_An_Exception()
{
var handle = async () => await _commandHandler
.Handle(_envelope, CancellationToken.None);

handle.Should().ThrowAsync<ApplicationException>()
.WithMessage($"Kan niet langer wachten op lock voor KBO nummer {_envelope.Command.KboNummer}");
}
}
Loading

0 comments on commit cda36d1

Please sign in to comment.