diff --git a/src/FindAnApprenticeship/SFA.DAS.FindAnApprenticeship.UnitTests/Application/Commands/WhenHandlingCreateCandidateCommand.cs b/src/FindAnApprenticeship/SFA.DAS.FindAnApprenticeship.UnitTests/Application/Commands/WhenHandlingCreateCandidateCommand.cs index 811804f853..b5eedd97cf 100644 --- a/src/FindAnApprenticeship/SFA.DAS.FindAnApprenticeship.UnitTests/Application/Commands/WhenHandlingCreateCandidateCommand.cs +++ b/src/FindAnApprenticeship/SFA.DAS.FindAnApprenticeship.UnitTests/Application/Commands/WhenHandlingCreateCandidateCommand.cs @@ -19,8 +19,13 @@ namespace SFA.DAS.FindAnApprenticeship.UnitTests.Application.Commands; public class WhenHandlingPostCandidateCommand { - [Test, MoqAutoData] + [Test] + [MoqInlineAutoData(UserStatus.Completed)] + [MoqInlineAutoData(UserStatus.Deleted)] + [MoqInlineAutoData(UserStatus.InProgress)] + [MoqInlineAutoData(UserStatus.Incomplete)] public async Task Then_If_Candidate_Already_Exists_Then_Details_Are_Returned( + UserStatus status, CreateCandidateCommand command, string govUkId, GetCandidateApiResponse candidate, @@ -33,6 +38,8 @@ public async Task Then_If_Candidate_Already_Exists_Then_Details_Are_Returned( command.GovUkIdentifier = govUkId; command.Email = candidate.Email; + candidate.Status = status; + var expectedGetCandidateRequest = new GetCandidateApiRequest(govUkId); mockApiClient.Setup(x => x.GetWithResponseCode( It.Is(r => r.GetUrl == expectedGetCandidateRequest.GetUrl))) diff --git a/src/FindAnApprenticeship/SFA.DAS.FindAnApprenticeship/Application/Commands/Candidate/CreateCandidateCommandHandler.cs b/src/FindAnApprenticeship/SFA.DAS.FindAnApprenticeship/Application/Commands/Candidate/CreateCandidateCommandHandler.cs index ab77940acf..f5a4b58f57 100644 --- a/src/FindAnApprenticeship/SFA.DAS.FindAnApprenticeship/Application/Commands/Candidate/CreateCandidateCommandHandler.cs +++ b/src/FindAnApprenticeship/SFA.DAS.FindAnApprenticeship/Application/Commands/Candidate/CreateCandidateCommandHandler.cs @@ -29,11 +29,14 @@ await candidateApiClient.GetWithResponseCode( if (existingUser.StatusCode != HttpStatusCode.NotFound) { - if (existingUser.Body.Email != request.Email) + if (existingUser.Body.Email != request.Email || existingUser.Body.Status == UserStatus.Dormant) { var updateEmailRequest = new PutCandidateApiRequest(existingUser.Body.Id, new PutCandidateApiRequestData { - Email = request.Email + Email = request.Email, + Status = existingUser.Body.Status == UserStatus.Dormant + ? UserStatus.Completed + : existingUser.Body.Status }); await candidateApiClient.PutWithResponseCode(updateEmailRequest); diff --git a/src/FindAnApprenticeship/SFA.DAS.FindAnApprenticeship/Domain/Models/UserStatus.cs b/src/FindAnApprenticeship/SFA.DAS.FindAnApprenticeship/Domain/Models/UserStatus.cs index b7982e6d58..05d6688c2c 100644 --- a/src/FindAnApprenticeship/SFA.DAS.FindAnApprenticeship/Domain/Models/UserStatus.cs +++ b/src/FindAnApprenticeship/SFA.DAS.FindAnApprenticeship/Domain/Models/UserStatus.cs @@ -6,5 +6,6 @@ public enum UserStatus Completed = 1, InProgress = 2, Deleted = 3, + Dormant = 4, } } \ No newline at end of file diff --git a/src/FindApprenticeshipJobs/SFA.DAS.FindApprenticeshipJobs.Api.UnitTests/Controllers/WhenGettingCandidatesByActivity.cs b/src/FindApprenticeshipJobs/SFA.DAS.FindApprenticeshipJobs.Api.UnitTests/Controllers/WhenGettingCandidatesByActivity.cs new file mode 100644 index 0000000000..44c3e4a6e4 --- /dev/null +++ b/src/FindApprenticeshipJobs/SFA.DAS.FindApprenticeshipJobs.Api.UnitTests/Controllers/WhenGettingCandidatesByActivity.cs @@ -0,0 +1,56 @@ +using AutoFixture.NUnit3; +using FluentAssertions; +using FluentAssertions.Execution; +using MediatR; +using Microsoft.AspNetCore.Mvc; +using Moq; +using NUnit.Framework; +using SFA.DAS.FindApprenticeshipJobs.Api.Controllers; +using SFA.DAS.Testing.AutoFixture; +using System.Net; +using SFA.DAS.FindApprenticeshipJobs.Application.Queries.SavedSearch.GetInactiveCandidates; + +namespace SFA.DAS.FindApprenticeshipJobs.Api.UnitTests.Controllers +{ + [TestFixture] + public class WhenGettingCandidatesByActivity + { + [Test, MoqAutoData] + public async Task Then_Candidates_Returned_From_Mediator( + int pageNumber, + int pageSize, + DateTime cutOffDateTime, + GetInactiveCandidatesQueryResult mockQueryResult, + [Frozen] Mock mockMediator, + [Greedy] CandidatesController sut) + { + mockMediator.Setup(x => x.Send(It.IsAny(), It.IsAny())).ReturnsAsync(mockQueryResult); + + var actual = await sut.GetInactiveCandidates(cutOffDateTime, pageNumber, pageSize, It.IsAny()) as ObjectResult; + var actualValue = actual!.Value as GetInactiveCandidatesQueryResult; + + using (new AssertionScope()) + { + actual.StatusCode.Should().Be((int)HttpStatusCode.OK); + actual.Value.Should().BeOfType(); + actualValue!.Candidates.Should().BeEquivalentTo(mockQueryResult.Candidates); + } + } + + [Test, MoqAutoData] + public async Task And_Exception_Returned_Then_Returns_Internal_Server_Error( + int pageNumber, + int pageSize, + DateTime cutOffDateTime, + GetInactiveCandidatesQueryResult mockQueryResult, + [Frozen] Mock mockMediator, + [Greedy] CandidatesController sut) + { + mockMediator.Setup(x => x.Send(It.IsAny(), It.IsAny())).ThrowsAsync(new InvalidOperationException()); + + var actual = await sut.GetInactiveCandidates(cutOffDateTime, pageNumber, pageSize, It.IsAny()) as StatusCodeResult; + + actual!.StatusCode.Should().Be((int)HttpStatusCode.InternalServerError); + } + } +} diff --git a/src/FindApprenticeshipJobs/SFA.DAS.FindApprenticeshipJobs.Api.UnitTests/Controllers/WhenPostingCandidateStatus.cs b/src/FindApprenticeshipJobs/SFA.DAS.FindApprenticeshipJobs.Api.UnitTests/Controllers/WhenPostingCandidateStatus.cs new file mode 100644 index 0000000000..d4eae4af07 --- /dev/null +++ b/src/FindApprenticeshipJobs/SFA.DAS.FindApprenticeshipJobs.Api.UnitTests/Controllers/WhenPostingCandidateStatus.cs @@ -0,0 +1,60 @@ +using AutoFixture.NUnit3; +using FluentAssertions; +using FluentAssertions.Execution; +using MediatR; +using Microsoft.AspNetCore.Mvc; +using Moq; +using NUnit.Framework; +using SFA.DAS.FindApprenticeshipJobs.Api.Models; +using SFA.DAS.FindApprenticeshipJobs.Application.Commands.Candidates; +using SFA.DAS.Testing.AutoFixture; +using System.Net; + +namespace SFA.DAS.FindApprenticeshipJobs.Api.UnitTests.Controllers +{ + [TestFixture] + public class WhenPostingCandidateStatus + { + [Test, MoqAutoData] + public async Task Then_Returns_Post_Response( + string govIdentifier, + CandidateUpdateStatusRequest model, + [Frozen] Mock mediator, + [Greedy] Api.Controllers.CandidatesController controller) + { + var actual = await controller.UpdateStatus(govIdentifier, model); + + using (new AssertionScope()) + { + actual.Should().BeOfType(); + mediator.Verify(x => x.Send(It.Is(command => command.GovUkIdentifier == govIdentifier + && command.Email == model.Email + && command.Status == model.Status), + It.IsAny()), Times.Once); + } + } + + [Test, MoqAutoData] + public async Task Then_If_An_Exception_Is_Thrown_Then_Internal_Server_Error_Response_Returned( + string govIdentifier, + CandidateUpdateStatusRequest model, + [Frozen] Mock mediator, + [Greedy] Api.Controllers.CandidatesController controller) + { + mediator.Setup(x => x.Send(It.Is(x => x.GovUkIdentifier == govIdentifier + && (x.Email == model.Email) + && x.Status == model.Status), + It.IsAny())) + .ThrowsAsync(new Exception()); + + var actual = await controller.UpdateStatus(govIdentifier, model) as StatusCodeResult; + + actual.Should().NotBeNull(); + actual!.StatusCode.Should().Be((int)HttpStatusCode.InternalServerError); + mediator.Verify(x => x.Send(It.Is(command => command.GovUkIdentifier == govIdentifier + && command.Email == model.Email + && command.Status == model.Status), + It.IsAny()), Times.Once); + } + } +} diff --git a/src/FindApprenticeshipJobs/SFA.DAS.FindApprenticeshipJobs.Api/Controllers/CandidatesController.cs b/src/FindApprenticeshipJobs/SFA.DAS.FindApprenticeshipJobs.Api/Controllers/CandidatesController.cs new file mode 100644 index 0000000000..14155df493 --- /dev/null +++ b/src/FindApprenticeshipJobs/SFA.DAS.FindApprenticeshipJobs.Api/Controllers/CandidatesController.cs @@ -0,0 +1,66 @@ +using MediatR; +using Microsoft.AspNetCore.Mvc; +using System.Net; +using SFA.DAS.FindApprenticeshipJobs.Application.Commands.Candidates; +using SFA.DAS.FindApprenticeshipJobs.Domain.Models; +using SFA.DAS.FindApprenticeshipJobs.Api.Models; +using SFA.DAS.FindApprenticeshipJobs.Application.Queries.SavedSearch.GetInactiveCandidates; + +namespace SFA.DAS.FindApprenticeshipJobs.Api.Controllers +{ + [ApiController] + [Route("[controller]")] + public class CandidatesController( + IMediator mediator, + ILogger logger) : ControllerBase + { + [HttpGet] + [Route("GetInactiveCandidates")] + [ProducesResponseType((int)HttpStatusCode.OK)] + [ProducesResponseType((int)HttpStatusCode.InternalServerError)] + public async Task GetInactiveCandidates( + [FromQuery] DateTime cutOffDateTime, + [FromQuery] int pageNumber = 1, + [FromQuery] int pageSize = 10, + CancellationToken cancellationToken = default) + { + logger.LogInformation("Get Candidates by activity invoked"); + + try + { + var result = await mediator.Send(new GetInactiveCandidatesQuery(cutOffDateTime, pageNumber, pageSize), + cancellationToken); + return Ok(result); + } + catch (Exception ex) + { + logger.LogError(ex, "Error invoking Get Inactive Candidates"); + return new StatusCodeResult((int)HttpStatusCode.InternalServerError); + } + } + + [HttpPost] + [Route("{govIdentifier}/status")] + public async Task UpdateStatus( + [FromRoute] string govIdentifier, + [FromBody] CandidateUpdateStatusRequest request, CancellationToken cancellationToken = default) + { + try + { + await mediator.Send(new UpdateCandidateStatusCommand + { + GovUkIdentifier = govIdentifier, + Email = request.Email, + Status = request.Status + }, cancellationToken); + + return NoContent(); + } + catch (Exception e) + { + logger.LogError(e, "Error attempting to update candidate status"); + return StatusCode((int)HttpStatusCode.InternalServerError); + } + } + } +} \ No newline at end of file diff --git a/src/FindApprenticeshipJobs/SFA.DAS.FindApprenticeshipJobs.Api/Models/CandidateUpdateStatusRequest.cs b/src/FindApprenticeshipJobs/SFA.DAS.FindApprenticeshipJobs.Api/Models/CandidateUpdateStatusRequest.cs new file mode 100644 index 0000000000..c4a3431c3a --- /dev/null +++ b/src/FindApprenticeshipJobs/SFA.DAS.FindApprenticeshipJobs.Api/Models/CandidateUpdateStatusRequest.cs @@ -0,0 +1,10 @@ +using SFA.DAS.FindApprenticeshipJobs.Domain.Models; + +namespace SFA.DAS.FindApprenticeshipJobs.Api.Models +{ + public class CandidateUpdateStatusRequest + { + public required string Email { get; set; } + public UserStatus Status { get; set; } + } +} diff --git a/src/FindApprenticeshipJobs/SFA.DAS.FindApprenticeshipJobs.UnitTests/Candidates/WhenHandlingGetCandidatesByActivityQuery.cs b/src/FindApprenticeshipJobs/SFA.DAS.FindApprenticeshipJobs.UnitTests/Candidates/WhenHandlingGetCandidatesByActivityQuery.cs new file mode 100644 index 0000000000..9f5b18d1a7 --- /dev/null +++ b/src/FindApprenticeshipJobs/SFA.DAS.FindApprenticeshipJobs.UnitTests/Candidates/WhenHandlingGetCandidatesByActivityQuery.cs @@ -0,0 +1,45 @@ +using AutoFixture.NUnit3; +using FluentAssertions; +using Moq; +using NUnit.Framework; +using SFA.DAS.FindApprenticeshipJobs.Application.Queries.SavedSearch.GetInactiveCandidates; +using SFA.DAS.FindApprenticeshipJobs.InnerApi.Requests; +using SFA.DAS.FindApprenticeshipJobs.InnerApi.Responses; +using SFA.DAS.SharedOuterApi.Configuration; +using SFA.DAS.SharedOuterApi.Interfaces; +using SFA.DAS.Testing.AutoFixture; + +namespace SFA.DAS.FindApprenticeshipJobs.UnitTests.Candidates +{ + [TestFixture] + public class WhenHandlingGetCandidatesByActivityQuery + { + [Test] + [MoqAutoData] + public async Task Then_The_Candidates_Are_Returned( + GetInactiveCandidatesQuery query, + GetInactiveCandidatesApiResponse mockCandidatesByActivityApiResponse, + [Frozen] Mock> mockCandidateApiClient, + GetInactiveCandidatesQueryHandler handler) + { + var expectedGetCandidatesByActivityApiRequest = + new GetInactiveCandidatesApiRequest(query.CutOffDateTime.ToString("O"), query.PageNumber, query.PageSize); + + mockCandidateApiClient + .Setup(client => client.Get( + It.Is(c => + c.GetUrl == expectedGetCandidatesByActivityApiRequest.GetUrl))) + .ReturnsAsync(mockCandidatesByActivityApiResponse); + + + var actual = await handler.Handle(query, CancellationToken.None); + + actual.Should().NotBeNull(); + actual.Candidates.Should().BeEquivalentTo(mockCandidatesByActivityApiResponse.Candidates); + actual.PageSize.Should().Be(mockCandidatesByActivityApiResponse.PageSize); + actual.PageIndex.Should().Be(mockCandidatesByActivityApiResponse.PageIndex); + actual.TotalPages.Should().Be(mockCandidatesByActivityApiResponse.TotalPages); + actual.TotalCount.Should().Be(mockCandidatesByActivityApiResponse.TotalCount); + } + } +} diff --git a/src/FindApprenticeshipJobs/SFA.DAS.FindApprenticeshipJobs.UnitTests/Candidates/WhenHandlingUpdateCandidateStatusCommand.cs b/src/FindApprenticeshipJobs/SFA.DAS.FindApprenticeshipJobs.UnitTests/Candidates/WhenHandlingUpdateCandidateStatusCommand.cs new file mode 100644 index 0000000000..6db771979f --- /dev/null +++ b/src/FindApprenticeshipJobs/SFA.DAS.FindApprenticeshipJobs.UnitTests/Candidates/WhenHandlingUpdateCandidateStatusCommand.cs @@ -0,0 +1,123 @@ +using AutoFixture.NUnit3; +using Moq; +using NUnit.Framework; +using SFA.DAS.FindApprenticeshipJobs.Application.Commands.Candidates; +using SFA.DAS.FindApprenticeshipJobs.InnerApi.Requests; +using SFA.DAS.FindApprenticeshipJobs.InnerApi.Responses; +using SFA.DAS.SharedOuterApi.Configuration; +using SFA.DAS.SharedOuterApi.Interfaces; +using SFA.DAS.SharedOuterApi.Models; +using SFA.DAS.Testing.AutoFixture; +using System.Net; + +namespace SFA.DAS.FindApprenticeshipJobs.UnitTests.Candidates +{ + public class WhenHandlingUpdateCandidateStatusCommand + { + [Test, MoqAutoData] + public async Task Then_The_Put_Is_Sent_And_Data_Returned( + string govUkId, + UpdateCandidateStatusCommand command, + GetCandidateApiResponse getCandidateApiResponse, + PutCandidateApiResponse putCandidateApiResponse, + [Frozen] Mock> mockApiClient, + UpdateCandidateStatusCommandHandler handler) + { + command.GovUkIdentifier = govUkId; + command.Email = getCandidateApiResponse.Email; + var expectedGetCandidateRequest = new GetCandidateApiRequest(govUkId); + mockApiClient.Setup(x => x.GetWithResponseCode( + It.Is(r => r.GetUrl == expectedGetCandidateRequest.GetUrl))) + .ReturnsAsync(new ApiResponse(getCandidateApiResponse, HttpStatusCode.OK, string.Empty)); + + + var expectedPutData = new PutCandidateApiRequestData + { + Email = command.Email, + Status = command.Status + }; + + var expectedRequest = new PutCandidateApiRequest(getCandidateApiResponse.Id, expectedPutData); + + mockApiClient + .Setup(client => client.PutWithResponseCode( + It.Is(c => + c.PutUrl == expectedRequest.PutUrl + && ((PutCandidateApiRequestData)c.Data).Email == command.Email))) + .ReturnsAsync(new ApiResponse(putCandidateApiResponse, HttpStatusCode.OK, string.Empty)); + + + await handler.Handle(command, CancellationToken.None); + + mockApiClient.Verify(client => client.PutWithResponseCode( + It.Is(c => + c.PutUrl == expectedRequest.PutUrl + && ((PutCandidateApiRequestData)c.Data).Email == command.Email)), Times.Once()); + } + + [Test, MoqAutoData] + public async Task And_Api_Returns_Null_Then_Put_Never_Called( + string govUkId, + UpdateCandidateStatusCommand command, + GetCandidateApiResponse getCandidateApiResponse, + PutCandidateApiResponse putCandidateApiResponse, + [Frozen] Mock> mockApiClient, + UpdateCandidateStatusCommandHandler handler) + { + command.GovUkIdentifier = govUkId; + command.Email = getCandidateApiResponse.Email; + var expectedGetCandidateRequest = new GetCandidateApiRequest(govUkId); + mockApiClient.Setup(x => x.GetWithResponseCode( + It.Is(r => r.GetUrl == expectedGetCandidateRequest.GetUrl))) + .ReturnsAsync(new ApiResponse(null!, HttpStatusCode.NotFound, string.Empty)); + + + var expectedPutData = new PutCandidateApiRequestData + { + Email = command.Email, + Status = command.Status + }; + + var expectedRequest = new PutCandidateApiRequest(getCandidateApiResponse.Id, expectedPutData); + + await handler.Handle(command, CancellationToken.None); + + mockApiClient.Verify(client => client.PutWithResponseCode( + It.Is(c => + c.PutUrl == expectedRequest.PutUrl + && ((PutCandidateApiRequestData)c.Data).Email == command.Email)), Times.Never()); + } + + [Test, MoqAutoData] + public async Task And_Api_Returns_Different_Email_Then_Put_Never_Called( + string govUkId, + UpdateCandidateStatusCommand command, + GetCandidateApiResponse getCandidateApiResponse, + PutCandidateApiResponse putCandidateApiResponse, + [Frozen] Mock> mockApiClient, + UpdateCandidateStatusCommandHandler handler) + { + command.GovUkIdentifier = govUkId; + var expectedGetCandidateRequest = new GetCandidateApiRequest(govUkId); + mockApiClient.Setup(x => x.GetWithResponseCode( + It.Is(r => r.GetUrl == expectedGetCandidateRequest.GetUrl))) + .ReturnsAsync(new ApiResponse(null!, HttpStatusCode.NotFound, string.Empty)); + + + var expectedPutData = new PutCandidateApiRequestData + { + Email = command.Email, + Status = command.Status + }; + + var expectedRequest = new PutCandidateApiRequest(getCandidateApiResponse.Id, expectedPutData); + + await handler.Handle(command, CancellationToken.None); + + mockApiClient.Verify(client => client.PutWithResponseCode( + It.Is(c => + c.PutUrl == expectedRequest.PutUrl + && ((PutCandidateApiRequestData)c.Data).Email == command.Email)), Times.Never()); + } + } +} diff --git a/src/FindApprenticeshipJobs/SFA.DAS.FindApprenticeshipJobs.UnitTests/InnerApi/WhenBuildingGetInactiveCandidatesApiRequest.cs b/src/FindApprenticeshipJobs/SFA.DAS.FindApprenticeshipJobs.UnitTests/InnerApi/WhenBuildingGetInactiveCandidatesApiRequest.cs new file mode 100644 index 0000000000..fdec1fef7a --- /dev/null +++ b/src/FindApprenticeshipJobs/SFA.DAS.FindApprenticeshipJobs.UnitTests/InnerApi/WhenBuildingGetInactiveCandidatesApiRequest.cs @@ -0,0 +1,19 @@ +using AutoFixture.NUnit3; +using FluentAssertions; +using NUnit.Framework; +using SFA.DAS.FindApprenticeshipJobs.InnerApi.Requests; + +namespace SFA.DAS.FindApprenticeshipJobs.UnitTests.InnerApi +{ + [TestFixture] + public class WhenBuildingGetInactiveCandidatesApiRequest + { + [Test, AutoData] + public void Then_The_Url_Is_Correctly_Built(string cutOffDateTime, int pageNumber, int pageSize) + { + var actual = new GetInactiveCandidatesApiRequest(cutOffDateTime, pageNumber, pageSize); + + actual.GetUrl.Should().Be($"api/candidates/GetInactiveCandidates?cutOffDateTime={cutOffDateTime}&pageNumber={pageNumber}&pageSize={pageSize}"); + } + } +} \ No newline at end of file diff --git a/src/FindApprenticeshipJobs/SFA.DAS.FindApprenticeshipJobs.UnitTests/InnerApi/WhenBuildingPutCandidateRequest.cs b/src/FindApprenticeshipJobs/SFA.DAS.FindApprenticeshipJobs.UnitTests/InnerApi/WhenBuildingPutCandidateRequest.cs new file mode 100644 index 0000000000..1bafa5a589 --- /dev/null +++ b/src/FindApprenticeshipJobs/SFA.DAS.FindApprenticeshipJobs.UnitTests/InnerApi/WhenBuildingPutCandidateRequest.cs @@ -0,0 +1,16 @@ +using AutoFixture.NUnit3; +using FluentAssertions; +using NUnit.Framework; +using SFA.DAS.FindApprenticeshipJobs.InnerApi.Requests; + +namespace SFA.DAS.FindApprenticeshipJobs.UnitTests.InnerApi; +public class WhenBuildingPutCandidateRequest +{ + [Test, AutoData] + public void Then_The_Request_Url_Is_Correctly_Built(Guid candidateId, PutCandidateApiRequestData data) + { + var actual = new PutCandidateApiRequest(candidateId, data); + + actual.PutUrl.Should().Be($"/api/candidates/{candidateId}"); + } +} diff --git a/src/FindApprenticeshipJobs/SFA.DAS.FindApprenticeshipJobs/Application/Commands/Candidates/UpdateCandidateStatusCommand.cs b/src/FindApprenticeshipJobs/SFA.DAS.FindApprenticeshipJobs/Application/Commands/Candidates/UpdateCandidateStatusCommand.cs new file mode 100644 index 0000000000..45e8d73a4e --- /dev/null +++ b/src/FindApprenticeshipJobs/SFA.DAS.FindApprenticeshipJobs/Application/Commands/Candidates/UpdateCandidateStatusCommand.cs @@ -0,0 +1,12 @@ +using MediatR; +using SFA.DAS.FindApprenticeshipJobs.Domain.Models; + +namespace SFA.DAS.FindApprenticeshipJobs.Application.Commands.Candidates +{ + public record UpdateCandidateStatusCommand : IRequest + { + public required string GovUkIdentifier { get; set; } + public required string Email { get; set; } + public UserStatus Status { get; set; } + } +} \ No newline at end of file diff --git a/src/FindApprenticeshipJobs/SFA.DAS.FindApprenticeshipJobs/Application/Commands/Candidates/UpdateCandidateStatusCommandHandler.cs b/src/FindApprenticeshipJobs/SFA.DAS.FindApprenticeshipJobs/Application/Commands/Candidates/UpdateCandidateStatusCommandHandler.cs new file mode 100644 index 0000000000..70953d8f37 --- /dev/null +++ b/src/FindApprenticeshipJobs/SFA.DAS.FindApprenticeshipJobs/Application/Commands/Candidates/UpdateCandidateStatusCommandHandler.cs @@ -0,0 +1,36 @@ +using System.Net; +using MediatR; +using SFA.DAS.FindApprenticeshipJobs.InnerApi.Requests; +using SFA.DAS.FindApprenticeshipJobs.InnerApi.Responses; +using SFA.DAS.SharedOuterApi.Configuration; +using SFA.DAS.SharedOuterApi.Interfaces; + +namespace SFA.DAS.FindApprenticeshipJobs.Application.Commands.Candidates +{ + public class UpdateCandidateStatusCommandHandler( + ICandidateApiClient candidateApiClient) + : IRequestHandler + { + public async Task Handle(UpdateCandidateStatusCommand request, + CancellationToken cancellationToken) + { + var existingUser = + await candidateApiClient.GetWithResponseCode( + new GetCandidateApiRequest(request.GovUkIdentifier)); + + if (existingUser.StatusCode != HttpStatusCode.NotFound) + { + if (existingUser.Body.Email == request.Email) + { + var updateEmailRequest = new PutCandidateApiRequest(existingUser.Body.Id, new PutCandidateApiRequestData + { + Email = request.Email, + Status = request.Status + }); + + await candidateApiClient.PutWithResponseCode(updateEmailRequest); + } + } + } + } +} \ No newline at end of file diff --git a/src/FindApprenticeshipJobs/SFA.DAS.FindApprenticeshipJobs/Application/Queries/SavedSearch/GetInactiveCandidates/GetInactiveCandidatesQuery.cs b/src/FindApprenticeshipJobs/SFA.DAS.FindApprenticeshipJobs/Application/Queries/SavedSearch/GetInactiveCandidates/GetInactiveCandidatesQuery.cs new file mode 100644 index 0000000000..8ed049e5af --- /dev/null +++ b/src/FindApprenticeshipJobs/SFA.DAS.FindApprenticeshipJobs/Application/Queries/SavedSearch/GetInactiveCandidates/GetInactiveCandidatesQuery.cs @@ -0,0 +1,9 @@ +using MediatR; + +namespace SFA.DAS.FindApprenticeshipJobs.Application.Queries.SavedSearch.GetInactiveCandidates +{ + public record GetInactiveCandidatesQuery( + DateTime CutOffDateTime, + int PageNumber = 1, + int PageSize = 10) : IRequest; +} \ No newline at end of file diff --git a/src/FindApprenticeshipJobs/SFA.DAS.FindApprenticeshipJobs/Application/Queries/SavedSearch/GetInactiveCandidates/GetInactiveCandidatesQueryHandler.cs b/src/FindApprenticeshipJobs/SFA.DAS.FindApprenticeshipJobs/Application/Queries/SavedSearch/GetInactiveCandidates/GetInactiveCandidatesQueryHandler.cs new file mode 100644 index 0000000000..8986516655 --- /dev/null +++ b/src/FindApprenticeshipJobs/SFA.DAS.FindApprenticeshipJobs/Application/Queries/SavedSearch/GetInactiveCandidates/GetInactiveCandidatesQueryHandler.cs @@ -0,0 +1,41 @@ +using MediatR; +using SFA.DAS.FindApprenticeshipJobs.InnerApi.Requests; +using SFA.DAS.FindApprenticeshipJobs.InnerApi.Responses; +using SFA.DAS.SharedOuterApi.Configuration; +using SFA.DAS.SharedOuterApi.Interfaces; + +namespace SFA.DAS.FindApprenticeshipJobs.Application.Queries.SavedSearch.GetInactiveCandidates +{ + public class GetInactiveCandidatesQueryHandler( + ICandidateApiClient candidateApiClient) + : IRequestHandler + { + public async Task Handle(GetInactiveCandidatesQuery request, CancellationToken cancellationToken) + { + var candidatesResponse = await candidateApiClient.Get( + new GetInactiveCandidatesApiRequest( + request.CutOffDateTime.ToString("O"), + request.PageNumber, + request.PageSize)); + + if (candidatesResponse is not { Candidates.Count: > 0 }) + return new GetInactiveCandidatesQueryResult + { + PageSize = candidatesResponse.PageSize, + PageIndex = candidatesResponse.PageIndex, + TotalPages = candidatesResponse.TotalPages, + TotalCount = candidatesResponse.TotalCount, + Candidates = [] + }; + + return new GetInactiveCandidatesQueryResult + { + Candidates = candidatesResponse.Candidates.Select(candidate => (GetInactiveCandidatesQueryResult.Candidate)candidate).ToList(), + PageSize = candidatesResponse.PageSize, + PageIndex = candidatesResponse.PageIndex, + TotalPages = candidatesResponse.TotalPages, + TotalCount = candidatesResponse.TotalCount + }; + } + } +} \ No newline at end of file diff --git a/src/FindApprenticeshipJobs/SFA.DAS.FindApprenticeshipJobs/Application/Queries/SavedSearch/GetInactiveCandidates/GetInactiveCandidatesQueryResult.cs b/src/FindApprenticeshipJobs/SFA.DAS.FindApprenticeshipJobs/Application/Queries/SavedSearch/GetInactiveCandidates/GetInactiveCandidatesQueryResult.cs new file mode 100644 index 0000000000..6f512fb50e --- /dev/null +++ b/src/FindApprenticeshipJobs/SFA.DAS.FindApprenticeshipJobs/Application/Queries/SavedSearch/GetInactiveCandidates/GetInactiveCandidatesQueryResult.cs @@ -0,0 +1,94 @@ +using SFA.DAS.FindApprenticeshipJobs.Domain.Models; +using SFA.DAS.FindApprenticeshipJobs.InnerApi.Responses; + +namespace SFA.DAS.FindApprenticeshipJobs.Application.Queries.SavedSearch.GetInactiveCandidates +{ + public record GetInactiveCandidatesQueryResult + { + public List Candidates { get; set; } = []; + public int TotalCount { get; set; } + public int PageIndex { get; set; } + public int PageSize { get; set; } + public int TotalPages { get; set; } + + public static implicit operator GetInactiveCandidatesQueryResult(GetInactiveCandidatesApiResponse source) + { + return new GetInactiveCandidatesQueryResult + { + Candidates = source.Candidates.Select(candidate => (Candidate)candidate).ToList() + }; + } + + public class Candidate + { + public Address Address { get; set; } + public string? GovUkIdentifier { get; set; } + public string? LastName { get; set; } + public string? FirstName { get; set; } + public string? MiddleNames { get; set; } + public string? PhoneNumber { get; set; } + public DateTime DateOfBirth { get; set; } + public DateTime CreatedOn { get; set; } + public DateTime UpdatedOn { get; set; } + public DateTime? TermsOfUseAcceptedOn { get; set; } + public UserStatus Status { get; set; } + public string? MigratedEmail { get; set; } + public string? Email { get; set; } + public Guid Id { get; set; } + public Guid? MigratedCandidateId { get; set; } + + public static implicit operator Candidate(GetInactiveCandidatesApiResponse.Candidate source) + { + return new Candidate + { + Address = source.Address, + GovUkIdentifier = source.GovUkIdentifier, + LastName = source.LastName, + FirstName = source.FirstName, + MiddleNames = source.MiddleNames, + PhoneNumber = source.PhoneNumber, + DateOfBirth = source.DateOfBirth, + CreatedOn = source.CreatedOn, + UpdatedOn = source.UpdatedOn, + TermsOfUseAcceptedOn = source.TermsOfUseAcceptedOn, + Status = source.Status, + MigratedEmail = source.MigratedEmail, + Email = source.Email, + Id = source.Id, + MigratedCandidateId = source.MigratedCandidateId + }; + } + } + + public class Address + { + public Guid Id { get; set; } + public string? Uprn { get; set; } + public string? AddressLine1 { get; set; } + public string? AddressLine2 { get; set; } + public string? Town { get; set; } + public string? County { get; set; } + public string? Postcode { get; set; } + public double Latitude { get; set; } + public double Longitude { get; set; } + public Guid CandidateId { get; set; } + + public static implicit operator Address(GetInactiveCandidatesApiResponse.Address source) + { + return new Address + { + Id = source.Id, + Uprn = source.Uprn, + AddressLine1 = source.AddressLine1, + AddressLine2 = source.AddressLine2, + Town = source.Town, + County = source.County, + Postcode = source.Postcode, + Latitude = source.Latitude, + Longitude = source.Longitude, + CandidateId = source.CandidateId + }; + } + } + } +} diff --git a/src/FindApprenticeshipJobs/SFA.DAS.FindApprenticeshipJobs/Application/Queries/SavedSearch/GetSavedSearches/GetSavedSearchesQueryHandler.cs b/src/FindApprenticeshipJobs/SFA.DAS.FindApprenticeshipJobs/Application/Queries/SavedSearch/GetSavedSearches/GetSavedSearchesQueryHandler.cs index 6427976bd1..86e7d0b057 100644 --- a/src/FindApprenticeshipJobs/SFA.DAS.FindApprenticeshipJobs/Application/Queries/SavedSearch/GetSavedSearches/GetSavedSearchesQueryHandler.cs +++ b/src/FindApprenticeshipJobs/SFA.DAS.FindApprenticeshipJobs/Application/Queries/SavedSearch/GetSavedSearches/GetSavedSearchesQueryHandler.cs @@ -15,7 +15,8 @@ public record GetSavedSearchesQueryHandler( ICandidateApiClient CandidateApiClient) : IRequestHandler { - public async Task Handle(GetSavedSearchesQuery request, CancellationToken cancellationToken) + public async Task Handle(GetSavedSearchesQuery request, + CancellationToken cancellationToken) { var searchResultList = new ConcurrentBag(); @@ -24,7 +25,7 @@ public async Task Handle(GetSavedSearchesQuery requ request.PageNumber, request.PageSize)); - if (savedSearchResponse is not { SavedSearches.Count: > 0 }) + if (savedSearchResponse is not {SavedSearches.Count: > 0}) return new GetSavedSearchesQueryResult { PageSize = savedSearchResponse.PageSize, @@ -33,15 +34,35 @@ public async Task Handle(GetSavedSearchesQuery requ TotalCount = savedSearchResponse.TotalCount, SavedSearchResults = [] }; - - var routesTask = CourseService.GetRoutes(); - var levelsTask = CourseService.GetLevels(); - await Task.WhenAll(routesTask, levelsTask); - var routesList = routesTask.Result; - var levelsList = levelsTask.Result; - var taskList = savedSearchResponse.SavedSearches.Select(savedSearch => GetSavedSearchResults(request, savedSearch, routesList, levelsList, searchResultList)).ToList(); - await Task.WhenAll(taskList); + foreach (var savedSearch in savedSearchResponse.SavedSearches) + { + var candidate = + await CandidateApiClient.Get( + new GetCandidateApiRequest(savedSearch.UserReference.ToString())); + + if (candidate == null || candidate.Status == UserStatus.Deleted || + candidate.Status == UserStatus.Dormant) continue; + + var routesTask = CourseService.GetRoutes(); + var levelsTask = CourseService.GetLevels(); + + await Task.WhenAll(routesTask, levelsTask); + var routesList = routesTask.Result; + var levelsList = levelsTask.Result; + var taskList = savedSearchResponse.SavedSearches.Select(savedSearch => + GetSavedSearchResults(request, savedSearch, routesList, levelsList, searchResultList)).ToList(); + await Task.WhenAll(taskList); + return new GetSavedSearchesQueryResult + { + PageSize = savedSearchResponse.PageSize, + PageIndex = savedSearchResponse.PageIndex, + TotalPages = savedSearchResponse.TotalPages, + TotalCount = savedSearchResponse.TotalCount, + SavedSearchResults = searchResultList.ToList() + }; + } + return new GetSavedSearchesQueryResult { PageSize = savedSearchResponse.PageSize, @@ -52,8 +73,10 @@ public async Task Handle(GetSavedSearchesQuery requ }; } - private async Task GetSavedSearchResults(GetSavedSearchesQuery request, GetSavedSearchesApiResponse.SavedSearch savedSearch, - GetRoutesListResponse routesList, GetCourseLevelsListResponse levelsList, ConcurrentBag searchResultList) + private async Task GetSavedSearchResults(GetSavedSearchesQuery request, + GetSavedSearchesApiResponse.SavedSearch savedSearch, + GetRoutesListResponse routesList, GetCourseLevelsListResponse levelsList, + ConcurrentBag searchResultList) { var candidate = await CandidateApiClient.Get( @@ -83,11 +106,15 @@ await CandidateApiClient.Get( var vacanciesResponse = await FindApprenticeshipApiClient.Get( new GetVacanciesRequest( - !string.IsNullOrEmpty(savedSearch.SearchParameters.Latitude) ? Convert.ToDouble(savedSearch.SearchParameters.Latitude) : null, - !string.IsNullOrEmpty(savedSearch.SearchParameters.Longitude) ? Convert.ToDouble(savedSearch.SearchParameters.Longitude) : null, + !string.IsNullOrEmpty(savedSearch.SearchParameters.Latitude) + ? Convert.ToDouble(savedSearch.SearchParameters.Latitude) + : null, + !string.IsNullOrEmpty(savedSearch.SearchParameters.Longitude) + ? Convert.ToDouble(savedSearch.SearchParameters.Longitude) + : null, savedSearch.SearchParameters.Distance, savedSearch.SearchParameters.SearchTerm, - 1, // Defaulting to top results. + 1, // Defaulting to top results. request.MaxApprenticeshipSearchResultsCount, // Default page size set to 5. categories.Select(cat => cat.Id.ToString()).ToList(), savedSearch.SearchParameters.SelectedLevelIds, @@ -118,5 +145,6 @@ await CandidateApiClient.Get( searchResultList.Add(searchResult); } } + } -} +} \ No newline at end of file diff --git a/src/FindApprenticeshipJobs/SFA.DAS.FindApprenticeshipJobs/Domain/Models/UserStatus.cs b/src/FindApprenticeshipJobs/SFA.DAS.FindApprenticeshipJobs/Domain/Models/UserStatus.cs index e50045867d..70ca7b9679 100644 --- a/src/FindApprenticeshipJobs/SFA.DAS.FindApprenticeshipJobs/Domain/Models/UserStatus.cs +++ b/src/FindApprenticeshipJobs/SFA.DAS.FindApprenticeshipJobs/Domain/Models/UserStatus.cs @@ -5,6 +5,7 @@ public enum UserStatus Incomplete = 0, Completed = 1, InProgress = 2, - Deleted = 3 + Deleted = 3, + Dormant = 4 } } \ No newline at end of file diff --git a/src/FindApprenticeshipJobs/SFA.DAS.FindApprenticeshipJobs/InnerApi/Requests/GetInactiveCandidatesApiRequest.cs b/src/FindApprenticeshipJobs/SFA.DAS.FindApprenticeshipJobs/InnerApi/Requests/GetInactiveCandidatesApiRequest.cs new file mode 100644 index 0000000000..dd023d915f --- /dev/null +++ b/src/FindApprenticeshipJobs/SFA.DAS.FindApprenticeshipJobs/InnerApi/Requests/GetInactiveCandidatesApiRequest.cs @@ -0,0 +1,9 @@ +using SFA.DAS.SharedOuterApi.Interfaces; + +namespace SFA.DAS.FindApprenticeshipJobs.InnerApi.Requests +{ + public class GetInactiveCandidatesApiRequest(string cutOffDateTime, int pageNumber, int pageSize) : IGetApiRequest + { + public string GetUrl => $"api/candidates/GetInactiveCandidates?cutOffDateTime={cutOffDateTime}&pageNumber={pageNumber}&pageSize={pageSize}"; + } +} \ No newline at end of file diff --git a/src/FindApprenticeshipJobs/SFA.DAS.FindApprenticeshipJobs/InnerApi/Requests/PutCandidateApiRequest.cs b/src/FindApprenticeshipJobs/SFA.DAS.FindApprenticeshipJobs/InnerApi/Requests/PutCandidateApiRequest.cs new file mode 100644 index 0000000000..01552b93e3 --- /dev/null +++ b/src/FindApprenticeshipJobs/SFA.DAS.FindApprenticeshipJobs/InnerApi/Requests/PutCandidateApiRequest.cs @@ -0,0 +1,17 @@ +using SFA.DAS.FindApprenticeshipJobs.Domain.Models; +using SFA.DAS.SharedOuterApi.Interfaces; + +namespace SFA.DAS.FindApprenticeshipJobs.InnerApi.Requests +{ + public class PutCandidateApiRequest(Guid candidateId, PutCandidateApiRequestData data) : IPutApiRequest + { + public object Data { get; set; } = data; + + public string PutUrl => $"/api/candidates/{candidateId}"; + } + public class PutCandidateApiRequestData + { + public string Email { get; set; } + public UserStatus? Status { get; set; } + } +} diff --git a/src/FindApprenticeshipJobs/SFA.DAS.FindApprenticeshipJobs/InnerApi/Responses/GetInactiveCandidatesApiResponse.cs b/src/FindApprenticeshipJobs/SFA.DAS.FindApprenticeshipJobs/InnerApi/Responses/GetInactiveCandidatesApiResponse.cs new file mode 100644 index 0000000000..5a731e249d --- /dev/null +++ b/src/FindApprenticeshipJobs/SFA.DAS.FindApprenticeshipJobs/InnerApi/Responses/GetInactiveCandidatesApiResponse.cs @@ -0,0 +1,81 @@ +using Newtonsoft.Json; +using SFA.DAS.FindApprenticeshipJobs.Domain.Models; + +namespace SFA.DAS.FindApprenticeshipJobs.InnerApi.Responses +{ + public class GetInactiveCandidatesApiResponse + { + [JsonProperty("candidates")] + public List Candidates { get; set; } = []; + + [JsonProperty("totalCount")] + public int TotalCount { get; set; } + + [JsonProperty("pageIndex")] + public int PageIndex { get; set; } + + [JsonProperty("pageSize")] + public int PageSize { get; set; } + + [JsonProperty("totalPages")] + public int TotalPages { get; set; } + + public class Candidate + { + [JsonProperty("address")] + public Address Address { get; set; } + [JsonProperty("govUkIdentifier")] + public string? GovUkIdentifier { get; set; } + [JsonProperty("lastName")] + public string? LastName { get; set; } + [JsonProperty("firstName")] + public string? FirstName { get; set; } + [JsonProperty("middleNames")] + public string? MiddleNames { get; set; } + [JsonProperty("phoneNumber")] + public string? PhoneNumber { get; set; } + [JsonProperty("dateOfBirth")] + public DateTime DateOfBirth { get; set; } + [JsonProperty("createdOn")] + public DateTime CreatedOn { get; set; } + [JsonProperty("updatedOn")] + public DateTime UpdatedOn { get; set; } + [JsonProperty("termsOfUseAcceptedOn")] + public DateTime? TermsOfUseAcceptedOn { get; set; } + [JsonProperty("status")] + public UserStatus Status { get; set; } + [JsonProperty("migratedEmail")] + public string? MigratedEmail { get; set; } + [JsonProperty("email")] + public string? Email { get; set; } + [JsonProperty("id")] + public Guid Id { get; set; } + [JsonProperty("migratedCandidateId")] + public Guid? MigratedCandidateId { get; set; } + } + + public class Address + { + [JsonProperty("id")] + public Guid Id { get; set; } + [JsonProperty("uprn")] + public string? Uprn { get; set; } + [JsonProperty("addressLine1")] + public string? AddressLine1 { get; set; } + [JsonProperty("addressLine2")] + public string? AddressLine2 { get; set; } + [JsonProperty("town")] + public string? Town { get; set; } + [JsonProperty("county")] + public string? County { get; set; } + [JsonProperty("postcode")] + public string? Postcode { get; set; } + [JsonProperty("latitude")] + public double Latitude { get; set; } + [JsonProperty("longitude")] + public double Longitude { get; set; } + [JsonProperty("source")] + public Guid CandidateId { get; set; } + } + } +} \ No newline at end of file diff --git a/src/FindApprenticeshipJobs/SFA.DAS.FindApprenticeshipJobs/InnerApi/Responses/PutCandidateApiResponse.cs b/src/FindApprenticeshipJobs/SFA.DAS.FindApprenticeshipJobs/InnerApi/Responses/PutCandidateApiResponse.cs new file mode 100644 index 0000000000..3fbe02f606 --- /dev/null +++ b/src/FindApprenticeshipJobs/SFA.DAS.FindApprenticeshipJobs/InnerApi/Responses/PutCandidateApiResponse.cs @@ -0,0 +1,12 @@ +namespace SFA.DAS.FindApprenticeshipJobs.InnerApi.Responses +{ + public class PutCandidateApiResponse + { + public Guid Id { get; set; } + public string GovUkIdentifier { get; set; } = null!; + public string Email { get; set; } = null!; + public string? FirstName { get; set; } + public string? LastName { get; set; } + public string? PhoneNumber { get; set; } + } +} \ No newline at end of file