Skip to content

Commit

Permalink
feat: add merge endpoint GAWR-6520
Browse files Browse the repository at this point in the history
  • Loading branch information
rikdepeuter authored Jul 9, 2024
1 parent 886ae40 commit 3c958be
Show file tree
Hide file tree
Showing 16 changed files with 429 additions and 149 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
namespace MunicipalityRegistry.Api.Import.Merger
{
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Autofac;
using Be.Vlaanderen.Basisregisters.CommandHandling.Idempotency;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Municipality.Commands;

public partial class MergerController
{
[HttpPost("{fusieJaar}")]
public async Task<IActionResult> Merge(
[FromRoute(Name = "fusieJaar")] int mergerYear,
CancellationToken cancellationToken = default)
{
var municipalityMergers = await _importContext.MunicipalityMergers
.Where(x => x.Year == mergerYear)
.ToListAsync(cancellationToken: cancellationToken);

if (!municipalityMergers.Any())
{
return BadRequest($"No municipality mergers found for year {mergerYear}");
}

var municipalityIdToNisCodeMapping = await _legacyContext.MunicipalityDetail
.ToDictionaryAsync(x => x.MunicipalityId!.Value, x => new NisCode(x.NisCode!), cancellationToken: cancellationToken);

foreach (var mergersPerNewMunicipality in municipalityMergers.GroupBy(x => x.NewMunicipalityId))
{
var newMunicipalityId = new MunicipalityId(mergersPerNewMunicipality.Key);
var newNisCode = municipalityIdToNisCodeMapping[newMunicipalityId];

{
var activateMunicipalityCommand = new ActivateMunicipality(newMunicipalityId, CreateProvenance($"Fusie {mergerYear}"));

try
{
await using var scopedContainer = _container.BeginLifetimeScope();
var idempotentCommandHandler = scopedContainer.Resolve<IIdempotentCommandHandler>();
await idempotentCommandHandler.Dispatch(
activateMunicipalityCommand.CreateCommandId(),
activateMunicipalityCommand,
new Dictionary<string, object>(),
cancellationToken);
}
catch (IdempotencyException)
{
// Do nothing
}
}

foreach (var mergeMunicipality in mergersPerNewMunicipality)
{
var mergeMunicipalityCommand = new MergeMunicipality(
new MunicipalityId(mergeMunicipality.MunicipalityId),
mergeMunicipality.MunicipalityIdsToMergeWith.Select(x => new MunicipalityId(x)).ToList(),
mergeMunicipality.MunicipalityIdsToMergeWith.Select(x => municipalityIdToNisCodeMapping[x]).ToList(),
newMunicipalityId,
newNisCode,
CreateProvenance($"Fusie {mergerYear}")
);

try
{
await using var scopedContainer = _container.BeginLifetimeScope();
var idempotentCommandHandler = scopedContainer.Resolve<IIdempotentCommandHandler>();
await idempotentCommandHandler.Dispatch(
mergeMunicipalityCommand.CreateCommandId(),
mergeMunicipalityCommand,
new Dictionary<string, object>(),
cancellationToken);
}
catch (IdempotencyException)
{
// Do nothing
}
}
}

return Ok();
}
}
}
130 changes: 130 additions & 0 deletions src/MunicipalityRegistry.Api.Import/Merger/MergerController-Propose.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
namespace MunicipalityRegistry.Api.Import.Merger
{
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Autofac;
using Be.Vlaanderen.Basisregisters.CommandHandling.Idempotency;
using Be.Vlaanderen.Basisregisters.GrAr.Legacy;
using Be.Vlaanderen.Basisregisters.GrAr.Provenance;
using Exceptions;
using FluentValidation;
using Infrastructure.Vrbg;
using Microsoft.AspNetCore.Mvc;
using Municipality.Commands;
using NetTopologySuite.Geometries;
using NodaTime;
using Propose;

public partial class MergerController
{
[HttpPost("propose")]
public async Task<IActionResult> Propose(
[FromBody] ProposeMergersRequest request,
[FromServices] IValidator<ProposeMergersRequest> validator,
[FromServices] IMunicipalityGeometryReader municipalityGeometryReader,
CancellationToken cancellationToken = default)
{
await validator.ValidateAndThrowAsync(request, cancellationToken: cancellationToken);

foreach (var municipality in request.Municipalities)
{
var futureMunicipalityId = await EnsureMunicipalityExistsAndReturnMunicipalityId(
request.MergerYear,
municipality,
municipalityGeometryReader,
cancellationToken);

var municipalitiesToMerge = municipality.MergerOf
.Select(niscode => _legacyContext.MunicipalityDetail.Single(x => x.NisCode == niscode))
.ToList();

foreach (var municipalityToMerge in municipalitiesToMerge)
{
await _importContext.MunicipalityMergers.AddAsync(new MunicipalityMerger(
request.MergerYear,
municipalityToMerge.MunicipalityId!.Value,
municipalitiesToMerge.Where(x => x.NisCode != municipalityToMerge.NisCode).Select(x => x.MunicipalityId!.Value),
futureMunicipalityId
), cancellationToken);
}

await _importContext.SaveChangesAsync(cancellationToken);
}

return Ok();
}

private async Task<Guid> EnsureMunicipalityExistsAndReturnMunicipalityId(
int mergerYear,
ProposeMergerRequest municipality,
IMunicipalityGeometryReader municipalityGeometryReader,
CancellationToken cancellationToken)
{
var existingMunicipality = _legacyContext.MunicipalityDetail.SingleOrDefault(x => x.NisCode == municipality.NisCode);
if (existingMunicipality is not null)
{
return existingMunicipality.MunicipalityId!.Value;
}

var newMunicipalityGeometry = await BuildMunicipalityGeometry(municipality, municipalityGeometryReader);

var registerMunicipalityCommand = new RegisterMunicipality(
new MunicipalityId(Guid.NewGuid()),
new NisCode(municipality.NisCode),
municipality.ProposeMunicipality!.OfficialLanguages.Select(ToLanguage).ToList(),
municipality.ProposeMunicipality.FacilitiesLanguages.Select(ToLanguage).ToList(),
municipality.ProposeMunicipality.Names.Select(n => new MunicipalityName(n.Value, ToLanguage(n.Key))).ToList(),
ExtendedWkbGeometry.CreateEWkb(newMunicipalityGeometry.ToBinary())!,
CreateProvenance($"Fusie {mergerYear}")
);

await using var scopedContainer = _container.BeginLifetimeScope();
var idempotentCommandHandler = scopedContainer.Resolve<IIdempotentCommandHandler>();
await idempotentCommandHandler.Dispatch(
registerMunicipalityCommand.CreateCommandId(),
registerMunicipalityCommand,
new Dictionary<string, object>(),
cancellationToken);

return registerMunicipalityCommand.MunicipalityId;
}

private static async Task<MultiPolygon> BuildMunicipalityGeometry(ProposeMergerRequest municipality, IMunicipalityGeometryReader municipalityGeometryReader)
{
var geometryFactory = GeometryConfiguration.CreateGeometryFactory();

var municipalityGeometriesToMerge = await Task.WhenAll(municipality.MergerOf.Select(municipalityGeometryReader.GetGeometry));
var newMunicipalityGeometry = new MultiPolygon(
municipalityGeometriesToMerge.SelectMany(geometry =>
{
return geometry switch
{
MultiPolygon multiPolygon => multiPolygon.Geometries.Cast<Polygon>(),
Polygon polygon => new[] { polygon },
_ => throw new InvalidPolygonException()
};
})
.ToArray(),
geometryFactory)
{
SRID = geometryFactory.SRID
};
return newMunicipalityGeometry;
}

private static Language ToLanguage(Taal taal)
{
return taal switch
{
Taal.NL => Language.Dutch,
Taal.FR => Language.French,
Taal.DE => Language.German,
Taal.EN => Language.English,
_ => throw new ArgumentOutOfRangeException(nameof(taal), taal, $"Non existing language '{taal}'.")
};
}
}
}
135 changes: 9 additions & 126 deletions src/MunicipalityRegistry.Api.Import/Merger/MergerController.cs
Original file line number Diff line number Diff line change
@@ -1,33 +1,18 @@
namespace MunicipalityRegistry.Api.Import.Merger
{
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Asp.Versioning;
using Autofac;
using Be.Vlaanderen.Basisregisters.Api;
using Be.Vlaanderen.Basisregisters.CommandHandling;
using Be.Vlaanderen.Basisregisters.CommandHandling.Idempotency;
using Be.Vlaanderen.Basisregisters.GrAr.Legacy;
using Be.Vlaanderen.Basisregisters.GrAr.Provenance;
using Exceptions;
using FluentValidation;
using Infrastructure;
using Infrastructure.Vrbg;
using Microsoft.AspNetCore.Mvc;
using Municipality.Commands;
using NetTopologySuite.Geometries;
using NodaTime;
using Projections.Legacy;
using Projections.Legacy.MunicipalityDetail;

[ApiVersion("1.0")]
[AdvertiseApiVersions("1.0")]
[ApiRoute("merger")]
[ApiExplorerSettings(GroupName = "Merger")]
public class MergerController : ApiController
public partial class MergerController : ApiController
{
private readonly LegacyContext _legacyContext;
private readonly ImportContext _importContext;
Expand All @@ -43,117 +28,15 @@ public MergerController(
_container = container;
}

[HttpPost]
public async Task<IActionResult> Propose(
[FromBody] ProposeMergersRequest request,
[FromServices] IValidator<ProposeMergersRequest> validator,
[FromServices] IMunicipalityGeometryReader municipalityGeometryReader,
CancellationToken cancellationToken = default)
private Provenance CreateProvenance(string reason)
{
await validator.ValidateAndThrowAsync(request, cancellationToken: cancellationToken);

foreach (var municipality in request.Municipalities)
{
var futureMunicipalityId = await EnsureMunicipalityExistsAndReturnMunicipalityId(
request.MergerYear,
municipality,
municipalityGeometryReader,
cancellationToken);

var municipalitiesToMerge = municipality.MergerOf
.Select(niscode => _legacyContext.MunicipalityDetail.Single(x => x.NisCode == niscode))
.ToList();

foreach (var municipalityToMerge in municipalitiesToMerge)
{
await _importContext.MunicipalityMergers.AddAsync(new MunicipalityMerger(
request.MergerYear,
municipalityToMerge.MunicipalityId!.Value,
municipalitiesToMerge.Where(x => x.NisCode != municipalityToMerge.NisCode).Select(x => x.MunicipalityId!.Value),
futureMunicipalityId
), cancellationToken);
}

await _importContext.SaveChangesAsync(cancellationToken);
}

return Ok();
}

private async Task<Guid> EnsureMunicipalityExistsAndReturnMunicipalityId(
int mergerYear,
ProposeMergerRequest municipality,
IMunicipalityGeometryReader municipalityGeometryReader,
CancellationToken cancellationToken)
{
var existingMunicipality = _legacyContext.MunicipalityDetail.SingleOrDefault(x => x.NisCode == municipality.NisCode);
if (existingMunicipality is not null)
{
return existingMunicipality.MunicipalityId!.Value;
}

var newMunicipalityGeometry = await BuildMunicipalityGeometry(municipality, municipalityGeometryReader);

var registerMunicipalityCommand = new RegisterMunicipality(
new MunicipalityId(Guid.NewGuid()),
new NisCode(municipality.NisCode),
municipality.ProposeMunicipality!.OfficialLanguages.Select(ToLanguage).ToList(),
municipality.ProposeMunicipality.FacilitiesLanguages.Select(ToLanguage).ToList(),
municipality.ProposeMunicipality.Names.Select(n => new MunicipalityName(n.Value, ToLanguage(n.Key))).ToList(),
ExtendedWkbGeometry.CreateEWkb(newMunicipalityGeometry.ToBinary())!,
new Provenance(
SystemClock.Instance.GetCurrentInstant(),
Application.MunicipalityRegistry,
new Reason($"Fusie {mergerYear}"),
new Operator("OVO002949"),
Modification.Insert,
Organisation.DigitaalVlaanderen)
);

await using var scopedContainer = _container.BeginLifetimeScope();
var idempotentCommandHandler = scopedContainer.Resolve<IIdempotentCommandHandler>();
await idempotentCommandHandler.Dispatch(
registerMunicipalityCommand.CreateCommandId(),
registerMunicipalityCommand,
new Dictionary<string, object>(),
cancellationToken);

return registerMunicipalityCommand.MunicipalityId;
}

private static async Task<MultiPolygon> BuildMunicipalityGeometry(ProposeMergerRequest municipality, IMunicipalityGeometryReader municipalityGeometryReader)
{
var geometryFactory = GeometryConfiguration.CreateGeometryFactory();

var municipalityGeometriesToMerge = await Task.WhenAll(municipality.MergerOf.Select(municipalityGeometryReader.GetGeometry));
var newMunicipalityGeometry = new MultiPolygon(
municipalityGeometriesToMerge.SelectMany(geometry =>
{
return geometry switch
{
MultiPolygon multiPolygon => multiPolygon.Geometries.Cast<Polygon>(),
Polygon polygon => new[] { polygon },
_ => throw new InvalidPolygonException()
};
})
.ToArray(),
geometryFactory)
{
SRID = geometryFactory.SRID
};
return newMunicipalityGeometry;
}

private static Language ToLanguage(Taal taal)
{
return taal switch
{
Taal.NL => Language.Dutch,
Taal.FR => Language.French,
Taal.DE => Language.German,
Taal.EN => Language.English,
_ => throw new ArgumentOutOfRangeException(nameof(taal), taal, $"Non existing language '{taal}'.")
};
return new Provenance(
SystemClock.Instance.GetCurrentInstant(),
Application.MunicipalityRegistry,
new Reason(reason),
new Operator("OVO002949"),
Modification.Insert,
Organisation.DigitaalVlaanderen);
}
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
namespace MunicipalityRegistry.Api.Import.Merger
namespace MunicipalityRegistry.Api.Import.Merger.Propose
{
using System.Collections.Generic;
using Be.Vlaanderen.Basisregisters.GrAr.Legacy;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
namespace MunicipalityRegistry.Api.Import.Merger
namespace MunicipalityRegistry.Api.Import.Merger.Propose
{
using System.Collections.Generic;
using Newtonsoft.Json;
Expand Down
Loading

0 comments on commit 3c958be

Please sign in to comment.