diff --git a/Services/CO.CDP.Organisation.WebApi.Tests/UseCase/GetLatestMouUseCaseTest.cs b/Services/CO.CDP.Organisation.WebApi.Tests/UseCase/GetLatestMouUseCaseTest.cs new file mode 100644 index 000000000..010c06c08 --- /dev/null +++ b/Services/CO.CDP.Organisation.WebApi.Tests/UseCase/GetLatestMouUseCaseTest.cs @@ -0,0 +1,145 @@ +using AutoMapper; +using CO.CDP.Organisation.WebApi.Model; +using CO.CDP.Organisation.WebApi.Tests.AutoMapper; +using CO.CDP.Organisation.WebApi.UseCase; +using CO.CDP.OrganisationInformation; +using CO.CDP.OrganisationInformation.Persistence; +using FluentAssertions; +using Moq; +using Persistence = CO.CDP.OrganisationInformation.Persistence; +using Person = CO.CDP.OrganisationInformation.Persistence.Person; + +namespace CO.CDP.Organisation.WebApi.Tests.UseCase; + +public class GetLatestMouUseCaseTest(AutoMapperFixture mapperFixture) + : IClassFixture +{ + private readonly Mock _organisationRepository = new(); + private GetLatestMouUseCase _useCase => new GetLatestMouUseCase(_organisationRepository.Object, mapperFixture.Mapper); + + [Fact] + public async Task Execute_ShouldReturnMappedMou_WhenLatestMouExists() + { + var latestMouEntity = new Persistence.Mou + { + Id = 1, + Guid = Guid.NewGuid(), + FilePath = "/path/to/mou.pdf", + CreatedOn = DateTimeOffset.UtcNow, + UpdatedOn = DateTimeOffset.UtcNow + }; + + var mappedMou = new CO.CDP.Organisation.WebApi.Model.Mou + { + Id = latestMouEntity.Guid, + FilePath = latestMouEntity.FilePath, + CreatedOn = latestMouEntity.CreatedOn + }; + + _organisationRepository + .Setup(repo => repo.GetLatestMou()) + .ReturnsAsync(latestMouEntity); + + var result = await _useCase.Execute(); + + result.Should().BeEquivalentTo(mappedMou); + + _organisationRepository.Verify(repo => repo.GetLatestMou(), Times.Once); + } + + [Fact] + public async Task Execute_ShouldThrowUnknownMouException_WhenLatestMouIsNull() + { + _organisationRepository + .Setup(repo => repo.GetLatestMou()) + .ReturnsAsync((Persistence.Mou)null!); + + Func act = async () => await _useCase.Execute(); + + await act.Should().ThrowAsync() + .WithMessage("No MOU found."); + + _organisationRepository.Verify(repo => repo.GetLatestMou(), Times.Once); + } + + public static Person FakePerson( + Guid? guid = null, + string? userUrn = null, + string firstname = "Jon", + string lastname = "doe", + string? email = null, + string phone = "07925123123", + List? scopes = null, + Tenant? tenant = null, + List<(Persistence.Organisation, List)>? organisationsWithScope = null +) + { + scopes = scopes ?? []; + var personGuid = guid ?? Guid.NewGuid(); + var person = new Person + { + Guid = personGuid, + UserUrn = userUrn ?? $"urn:fdc:gov.uk:2022:{Guid.NewGuid()}", + FirstName = firstname, + LastName = lastname, + Email = email ?? $"jon{personGuid}@example.com", + Phone = phone, + Scopes = scopes + }; + if (tenant != null) + { + person.Tenants.Add(tenant); + } + + foreach (var organisationWithScope in organisationsWithScope ?? []) + { + person.PersonOrganisations.Add( + new OrganisationPerson + { + Person = person, + Organisation = organisationWithScope.Item1, + Scopes = organisationWithScope.Item2 + } + ); + } + + return person; + } + + private static Persistence.Organisation FakeOrganisation(bool? withBuyerInfo = true) + { + Persistence.Organisation org = new() + { + Id = 1, + Guid = Guid.NewGuid(), + Name = "FakeOrg", + Tenant = new Tenant + { + Guid = Guid.NewGuid(), + Name = "Tenant 101" + }, + ContactPoints = + [ + new Persistence.Organisation.ContactPoint + { + Email = "contact@test.org" + } + ], + Type = OrganisationType.Organisation + }; + + if (withBuyerInfo == true) + { + var devolvedRegulations = new List(); + devolvedRegulations.Add(DevolvedRegulation.NorthernIreland); + + org.BuyerInfo = new Persistence.Organisation.BuyerInformation + { + BuyerType = "FakeBuyerType", + DevolvedRegulations = devolvedRegulations, + }; + } + + return org; + } +} \ No newline at end of file diff --git a/Services/CO.CDP.Organisation.WebApi.Tests/UseCase/GetMouUseCaseTest.cs b/Services/CO.CDP.Organisation.WebApi.Tests/UseCase/GetMouUseCaseTest.cs new file mode 100644 index 000000000..3253355bb --- /dev/null +++ b/Services/CO.CDP.Organisation.WebApi.Tests/UseCase/GetMouUseCaseTest.cs @@ -0,0 +1,147 @@ +using AutoMapper; +using CO.CDP.Organisation.WebApi.Model; +using CO.CDP.Organisation.WebApi.Tests.AutoMapper; +using CO.CDP.Organisation.WebApi.UseCase; +using CO.CDP.OrganisationInformation; +using CO.CDP.OrganisationInformation.Persistence; +using FluentAssertions; +using Moq; +using Persistence = CO.CDP.OrganisationInformation.Persistence; +using Person = CO.CDP.OrganisationInformation.Persistence.Person; + +namespace CO.CDP.Organisation.WebApi.Tests.UseCase; + +public class GetMouUseCaseTest(AutoMapperFixture mapperFixture) + : IClassFixture +{ + private readonly Mock _organisationRepository = new(); + private GetMouUseCase _useCase => new GetMouUseCase(_organisationRepository.Object, mapperFixture.Mapper); + + [Fact] + public async Task Execute_ShouldReturnMappedMou_WhenLatestMouExists() + { + var mouId = Guid.NewGuid(); + var mouEntity = new Persistence.Mou + { + Id = 1, + Guid = mouId, + FilePath = "/path/to/mou.pdf", + CreatedOn = DateTimeOffset.UtcNow, + UpdatedOn = DateTimeOffset.UtcNow + }; + + var mappedMou = new Model.Mou + { + Id = mouId, + FilePath = mouEntity.FilePath, + CreatedOn = mouEntity.CreatedOn + }; + + _organisationRepository + .Setup(repo => repo.GetMou(mouId)) + .ReturnsAsync(mouEntity); + + var result = await _useCase.Execute(mouId); + + result.Should().BeEquivalentTo(mappedMou); + + _organisationRepository.Verify(repo => repo.GetMou(mouId), Times.Once); + } + + [Fact] + public async Task Execute_ShouldThrowUnknownMouException_WhenLatestMouIsNull() + { + var mouId = Guid.NewGuid(); + _organisationRepository + .Setup(repo => repo.GetMou(mouId)) + .ReturnsAsync((Persistence.Mou)null!); + + Func act = async () => await _useCase.Execute(mouId); + + await act.Should().ThrowAsync() + .WithMessage("No MOU found."); + + _organisationRepository.Verify(repo => repo.GetMou(mouId), Times.Once); + } + + public static Person FakePerson( + Guid? guid = null, + string? userUrn = null, + string firstname = "Jon", + string lastname = "doe", + string? email = null, + string phone = "07925123123", + List? scopes = null, + Tenant? tenant = null, + List<(Persistence.Organisation, List)>? organisationsWithScope = null +) + { + scopes = scopes ?? []; + var personGuid = guid ?? Guid.NewGuid(); + var person = new Person + { + Guid = personGuid, + UserUrn = userUrn ?? $"urn:fdc:gov.uk:2022:{Guid.NewGuid()}", + FirstName = firstname, + LastName = lastname, + Email = email ?? $"jon{personGuid}@example.com", + Phone = phone, + Scopes = scopes + }; + if (tenant != null) + { + person.Tenants.Add(tenant); + } + + foreach (var organisationWithScope in organisationsWithScope ?? []) + { + person.PersonOrganisations.Add( + new OrganisationPerson + { + Person = person, + Organisation = organisationWithScope.Item1, + Scopes = organisationWithScope.Item2 + } + ); + } + + return person; + } + + private static Persistence.Organisation FakeOrganisation(bool? withBuyerInfo = true) + { + Persistence.Organisation org = new() + { + Id = 1, + Guid = Guid.NewGuid(), + Name = "FakeOrg", + Tenant = new Tenant + { + Guid = Guid.NewGuid(), + Name = "Tenant 101" + }, + ContactPoints = + [ + new Persistence.Organisation.ContactPoint + { + Email = "contact@test.org" + } + ], + Type = OrganisationType.Organisation + }; + + if (withBuyerInfo == true) + { + var devolvedRegulations = new List(); + devolvedRegulations.Add(DevolvedRegulation.NorthernIreland); + + org.BuyerInfo = new Persistence.Organisation.BuyerInformation + { + BuyerType = "FakeBuyerType", + DevolvedRegulations = devolvedRegulations, + }; + } + + return org; + } +} \ No newline at end of file diff --git a/Services/CO.CDP.Organisation.WebApi.Tests/UseCase/GetOrganisationMouSignatureLatestUseCaseTest.cs b/Services/CO.CDP.Organisation.WebApi.Tests/UseCase/GetOrganisationMouSignatureLatestUseCaseTest.cs index 6f4ca6caf..1d0b85fe9 100644 --- a/Services/CO.CDP.Organisation.WebApi.Tests/UseCase/GetOrganisationMouSignatureLatestUseCaseTest.cs +++ b/Services/CO.CDP.Organisation.WebApi.Tests/UseCase/GetOrganisationMouSignatureLatestUseCaseTest.cs @@ -77,7 +77,7 @@ public async Task Execute_ShouldThrowInvalidOperationException_WhenNoMouSignatur Func act = async () => await _useCase.Execute(organisation.Guid); - await act.Should().ThrowAsync() + await act.Should().ThrowAsync() .WithMessage($"No MOU signatures found for organisation {organisation.Guid}."); } diff --git a/Services/CO.CDP.Organisation.WebApi/Api/Organisation.cs b/Services/CO.CDP.Organisation.WebApi/Api/Organisation.cs index b49e2b161..563e4c40c 100644 --- a/Services/CO.CDP.Organisation.WebApi/Api/Organisation.cs +++ b/Services/CO.CDP.Organisation.WebApi/Api/Organisation.cs @@ -928,7 +928,7 @@ await useCase.Execute((organisationId, keyName)) return app; } - public static RouteGroupBuilder UseMouEndpoints(this RouteGroupBuilder app) + public static RouteGroupBuilder UseOrganisationMouEndpoints(this RouteGroupBuilder app) { app.MapGet("/{organisationId}/mou", [OrganisationAuthorize( @@ -987,7 +987,7 @@ await useCase.Execute((organisationId, mouSignatureId)) [AuthenticationChannel.OneLogin], [Constants.OrganisationPersonScope.Admin], OrganisationIdLocation.Path)] - async (Guid organisationId, IUseCase useCase) => + async (Guid organisationId, IUseCase useCase) => await useCase.Execute(organisationId) .AndThen(mouSignatureLatest => mouSignatureLatest != null ? Results.Ok(mouSignatureLatest) : Results.NotFound())) .Produces(StatusCodes.Status200OK, "application/json") @@ -1040,6 +1040,65 @@ await useCase.Execute((organisationId, signMou)) return app; } + public static RouteGroupBuilder UseMouEndpoints(this RouteGroupBuilder app) + { + app.MapGet("/latest", + [OrganisationAuthorize( + [AuthenticationChannel.OneLogin] + , [Constants.OrganisationPersonScope.Admin], + OrganisationIdLocation.Path + )] + async (IUseCase useCase) => + await useCase.Execute() + .AndThen(mouLatest => mouLatest != null ? Results.Ok(mouLatest) : Results.NotFound())) + .Produces(StatusCodes.Status200OK, "application/json") + .Produces(StatusCodes.Status401Unauthorized) + .Produces(StatusCodes.Status404NotFound) + .ProducesProblem(StatusCodes.Status422UnprocessableEntity) + .Produces(StatusCodes.Status500InternalServerError) + .WithOpenApi(operation => + { + operation.OperationId = "GetLatestMou"; + operation.Description = "Get Latest MOU."; + operation.Summary = "Get Latest MOU to sign."; + operation.Responses["200"].Description = "Latest MOU."; + operation.Responses["401"].Description = "Valid authentication credentials are missing in the request."; + operation.Responses["404"].Description = "Latest Mou information not found."; + operation.Responses["422"].Description = "Unprocessable entity."; + operation.Responses["500"].Description = "Internal server error."; + return operation; + }); + + app.MapGet("/{mouId}", + [OrganisationAuthorize( + [AuthenticationChannel.OneLogin] + , [Constants.OrganisationPersonScope.Admin], + OrganisationIdLocation.Path + )] + async (Guid mouId, IUseCase useCase) => + await useCase.Execute(mouId) + .AndThen(mou => mou != null ? Results.Ok(mou) : Results.NotFound())) + .Produces(StatusCodes.Status200OK, "application/json") + .Produces(StatusCodes.Status401Unauthorized) + .Produces(StatusCodes.Status404NotFound) + .ProducesProblem(StatusCodes.Status422UnprocessableEntity) + .Produces(StatusCodes.Status500InternalServerError) + .WithOpenApi(operation => + { + operation.OperationId = "GetMou"; + operation.Description = "Get MOU byId."; + operation.Summary = "Get MOU by ID."; + operation.Responses["200"].Description = "MOU by Id."; + operation.Responses["401"].Description = "Valid authentication credentials are missing in the request."; + operation.Responses["404"].Description = "Mou information not found."; + operation.Responses["422"].Description = "Unprocessable entity."; + operation.Responses["500"].Description = "Internal server error."; + return operation; + }); + + return app; + } + public static RouteGroupBuilder UseOrganisationPartiesEndpoints(this RouteGroupBuilder app) { app.MapGet("/{organisationId}/parties", diff --git a/Services/CO.CDP.Organisation.WebApi/Program.cs b/Services/CO.CDP.Organisation.WebApi/Program.cs index f16ee9398..5267dc06f 100644 --- a/Services/CO.CDP.Organisation.WebApi/Program.cs +++ b/Services/CO.CDP.Organisation.WebApi/Program.cs @@ -109,6 +109,8 @@ builder.Services.AddScoped, GetOrganisationMouSignatureLatestUseCase>(); builder.Services.AddScoped, SignOrganisationMouUseCase>(); builder.Services.AddScoped, AddOrganisationPartyUseCase>(); +builder.Services.AddScoped, GetLatestMouUseCase>(); +builder.Services.AddScoped, GetMouUseCase>(); builder.Services.AddGovUKNotifyApiClient(builder.Configuration); builder.Services.AddProblemDetails(); @@ -186,8 +188,13 @@ .WithTags("Feedback - provide feedback"); app.MapGroup("/organisations") - .UseMouEndpoints() + .UseOrganisationMouEndpoints() .WithTags("Organisation - MOUs"); +app.MapGroup("/mou") + .UseMouEndpoints() + .WithTags("Mou"); + + app.Run(); public abstract partial class Program; \ No newline at end of file diff --git a/Services/CO.CDP.Organisation.WebApi/UseCase/GetLatestMouUseCase.cs b/Services/CO.CDP.Organisation.WebApi/UseCase/GetLatestMouUseCase.cs new file mode 100644 index 000000000..3672e9a41 --- /dev/null +++ b/Services/CO.CDP.Organisation.WebApi/UseCase/GetLatestMouUseCase.cs @@ -0,0 +1,18 @@ +using AutoMapper; +using CO.CDP.Functional; +using CO.CDP.Organisation.WebApi.Model; +using CO.CDP.OrganisationInformation.Persistence; + +namespace CO.CDP.Organisation.WebApi.UseCase; + +public class GetLatestMouUseCase(IOrganisationRepository organisationRepository, IMapper mapper) + : IUseCase +{ + public async Task Execute() + { + var latestMou = await organisationRepository.GetLatestMou() + ?? throw new UnknownMouException($"No MOU found."); + + return mapper.Map(latestMou); + } +} \ No newline at end of file diff --git a/Services/CO.CDP.Organisation.WebApi/UseCase/GetMouUseCase.cs b/Services/CO.CDP.Organisation.WebApi/UseCase/GetMouUseCase.cs new file mode 100644 index 000000000..f11c37147 --- /dev/null +++ b/Services/CO.CDP.Organisation.WebApi/UseCase/GetMouUseCase.cs @@ -0,0 +1,18 @@ +using AutoMapper; +using CO.CDP.Functional; +using CO.CDP.Organisation.WebApi.Model; +using CO.CDP.OrganisationInformation.Persistence; + +namespace CO.CDP.Organisation.WebApi.UseCase; + +public class GetMouUseCase(IOrganisationRepository organisationRepository, IMapper mapper) + : IUseCase +{ + public async Task Execute(Guid mouId) + { + var mou = await organisationRepository.GetMou(mouId) + ?? throw new UnknownMouException($"No MOU found."); + + return mapper.Map(mou); + } +} \ No newline at end of file diff --git a/Services/CO.CDP.Organisation.WebApi/UseCase/GetOrganisationMouSignatureLatestUseCase.cs b/Services/CO.CDP.Organisation.WebApi/UseCase/GetOrganisationMouSignatureLatestUseCase.cs index 9891c18f1..4f7a26b18 100644 --- a/Services/CO.CDP.Organisation.WebApi/UseCase/GetOrganisationMouSignatureLatestUseCase.cs +++ b/Services/CO.CDP.Organisation.WebApi/UseCase/GetOrganisationMouSignatureLatestUseCase.cs @@ -17,7 +17,7 @@ public class GetOrganisationMouSignatureLatestUseCase(IOrganisationRepository or if (mouSignatures == null || !mouSignatures.Any()) { - throw new InvalidOperationException($"No MOU signatures found for organisation {organisationId}."); + throw new UnknownMouException($"No MOU signatures found for organisation {organisationId}."); } var latestSignature = mouSignatures.OrderByDescending(m => m.CreatedOn).First();