From 90113bd5c3619407816d86518b796eaec1f8d5dd Mon Sep 17 00:00:00 2001 From: skumardfe Date: Tue, 23 Jun 2026 17:14:53 +0100 Subject: [PATCH 1/5] Implement oltd start date for learner withdrawn ilr --- ...ppingApprenticeshipDetailsQueryResponse.cs | 1 + .../ValidateUlnOverlapOnStartDateTests.cs | 34 +++++++++++++---- ...verlappingTrainingDateRequestController.cs | 3 +- ...ngTrainingDateRequestDomainServiceTests.cs | 37 +++++++++++++++++-- ...rlappingTrainingDateRequestServiceTests.cs | 4 +- ...lidateUlnOverlapOnStartDateQueryHandler.cs | 7 +++- ...alidateUlnOverlapOnStartDateQueryResult.cs | 1 + .../Entities/OverlapCheckResultOnStartDate.cs | 4 +- .../Services/OverlapCheckService.cs | 13 ++++++- ...lappingTrainingDateRequestDomainService.cs | 12 ++++-- 10 files changed, 96 insertions(+), 20 deletions(-) diff --git a/src/CommitmentsV2/SFA.DAS.CommitmentsV2.Api.Types/Responses/GetOverlappingApprenticeshipDetailsQueryResponse.cs b/src/CommitmentsV2/SFA.DAS.CommitmentsV2.Api.Types/Responses/GetOverlappingApprenticeshipDetailsQueryResponse.cs index 7e05e2e58a..84c2343c4f 100644 --- a/src/CommitmentsV2/SFA.DAS.CommitmentsV2.Api.Types/Responses/GetOverlappingApprenticeshipDetailsQueryResponse.cs +++ b/src/CommitmentsV2/SFA.DAS.CommitmentsV2.Api.Types/Responses/GetOverlappingApprenticeshipDetailsQueryResponse.cs @@ -4,4 +4,5 @@ public class ValidateUlnOverlapOnStartDateResponse { public long? HasOverlapWithApprenticeshipId { get; set; } public bool HasStartDateOverlap { get; set; } + public bool HasOverlapWithIlrWithdrawnApprenticeship { get; set; } } \ No newline at end of file diff --git a/src/CommitmentsV2/SFA.DAS.CommitmentsV2.Api.UnitTests/Controllers/OverlappingTrainingDateRequestControllerTests/ValidateUlnOverlapOnStartDateTests.cs b/src/CommitmentsV2/SFA.DAS.CommitmentsV2.Api.UnitTests/Controllers/OverlappingTrainingDateRequestControllerTests/ValidateUlnOverlapOnStartDateTests.cs index 06c799bf9c..dac5743e8f 100644 --- a/src/CommitmentsV2/SFA.DAS.CommitmentsV2.Api.UnitTests/Controllers/OverlappingTrainingDateRequestControllerTests/ValidateUlnOverlapOnStartDateTests.cs +++ b/src/CommitmentsV2/SFA.DAS.CommitmentsV2.Api.UnitTests/Controllers/OverlappingTrainingDateRequestControllerTests/ValidateUlnOverlapOnStartDateTests.cs @@ -1,6 +1,6 @@ -using SFA.DAS.CommitmentsV2.Api.Controllers; -using SFA.DAS.CommitmentsV2.Api.Types.Requests; -using SFA.DAS.CommitmentsV2.Application.Commands.ValidateDraftApprenticeshipDetails; +using Microsoft.AspNetCore.Mvc; +using SFA.DAS.CommitmentsV2.Api.Controllers; +using SFA.DAS.CommitmentsV2.Api.Types.Responses; using SFA.DAS.CommitmentsV2.Application.Queries.GetOverlappingApprenticeshipDetails; using SFA.DAS.CommitmentsV2.Shared.Interfaces; @@ -19,10 +19,22 @@ public void Arrange() [Test] public async Task ValidateUlnOverlapOnStartDate_VerifyQuerySent() { - await _fixture.ValidateUlnOverlapOnStartDate(); + var response = await _fixture.ValidateUlnOverlapOnStartDate(); _fixture.VerifyQuerySent(); } + [Test] + [TestCase(true)] + [TestCase(false)] + public async Task ValidateUlnOverlapOnStartDate_HasOverlapWithIlrWithdrawnApprenticeship(bool HasOverlapWithIlrWithdrawnApprenticeship) + { + var response = await _fixture.withHasOverlapWithIlrWithdrawnApprenticeship(HasOverlapWithIlrWithdrawnApprenticeship).ValidateUlnOverlapOnStartDate(); + var result = response.Value as ValidateUlnOverlapOnStartDateResponse; + + result.Should().NotBeNull(); + result.HasOverlapWithIlrWithdrawnApprenticeship.Value.Should().Be(HasOverlapWithIlrWithdrawnApprenticeship); + } + private class ValidateUlnOverlapOnStartDateFixture { private readonly Mock _mediator; @@ -33,6 +45,7 @@ private class ValidateUlnOverlapOnStartDateFixture public string Uln; public string StartDate; public string EndDate; + private ValidateUlnOverlapOnStartDateQueryResult queryResult; public ValidateUlnOverlapOnStartDateFixture() { @@ -44,7 +57,7 @@ public ValidateUlnOverlapOnStartDateFixture() StartDate = "Jan 2022"; EndDate = "Dec 2022"; - var queryResult = _autoFixture.Create(); + queryResult = _autoFixture.Create(); _mediator .Setup(x => x.Send(It.IsAny(), It.IsAny())) .ReturnsAsync(queryResult); @@ -52,9 +65,16 @@ public ValidateUlnOverlapOnStartDateFixture() _controller = new OverlappingTrainingDateRequestController(_mediator.Object, _mapper.Object); } - public async Task ValidateUlnOverlapOnStartDate() + public async Task ValidateUlnOverlapOnStartDate() + { + var response = await _controller.ValidateUlnOverlapOnStartDate(ProviderId, Uln, StartDate, EndDate) as OkObjectResult; + return response; + } + + public ValidateUlnOverlapOnStartDateFixture withHasOverlapWithIlrWithdrawnApprenticeship(bool value) { - await _controller.ValidateUlnOverlapOnStartDate(ProviderId, Uln, StartDate, EndDate); + queryResult.HasOverlapWithIlrWithdrawnApprenticeship = value; + return this; } public void VerifyQuerySent() diff --git a/src/CommitmentsV2/SFA.DAS.CommitmentsV2.Api/Controllers/OverlappingTrainingDateRequestController.cs b/src/CommitmentsV2/SFA.DAS.CommitmentsV2.Api/Controllers/OverlappingTrainingDateRequestController.cs index 9f0395f113..500133c7fa 100644 --- a/src/CommitmentsV2/SFA.DAS.CommitmentsV2.Api/Controllers/OverlappingTrainingDateRequestController.cs +++ b/src/CommitmentsV2/SFA.DAS.CommitmentsV2.Api/Controllers/OverlappingTrainingDateRequestController.cs @@ -60,7 +60,8 @@ public async Task ValidateUlnOverlapOnStartDate(long providerId, return Ok(new ValidateUlnOverlapOnStartDateResponse { HasOverlapWithApprenticeshipId = result.HasOverlapWithApprenticeshipId, - HasStartDateOverlap = result.HasStartDateOverlap + HasStartDateOverlap = result.HasStartDateOverlap, + HasOverlapWithIlrWithdrawnApprenticeship = result.HasOverlapWithIlrWithdrawnApprenticeship }); } diff --git a/src/CommitmentsV2/SFA.DAS.CommitmentsV2.UnitTests/Services/OverlappingTrainingDateRequestDomainServiceTests.cs b/src/CommitmentsV2/SFA.DAS.CommitmentsV2.UnitTests/Services/OverlappingTrainingDateRequestDomainServiceTests.cs index ec0503758c..d9afe9f63a 100644 --- a/src/CommitmentsV2/SFA.DAS.CommitmentsV2.UnitTests/Services/OverlappingTrainingDateRequestDomainServiceTests.cs +++ b/src/CommitmentsV2/SFA.DAS.CommitmentsV2.UnitTests/Services/OverlappingTrainingDateRequestDomainServiceTests.cs @@ -143,7 +143,7 @@ string uln _overlapCheckServiceMock .Setup(m => m.CheckForOverlapsOnStartDate(uln, It.IsAny(), apprenticeshipId, It.IsAny())) - .ReturnsAsync(new OverlapCheckResultOnStartDate(false, null)); + .ReturnsAsync(new OverlapCheckResultOnStartDate(false, null,false)); // assert Assert.ThrowsAsync(async () => @@ -166,13 +166,14 @@ string uln var draftApprenticeships = GetDraftApprenticeshipTestData(apprenticeshipId, false); draftApprenticeships[0].Id = apprenticeshipId; draftApprenticeships[0].Uln = uln; + draftApprenticeships[0].PreviousApprenticeship.WithdrawnReasonCode = null; _dbContext.Setup(db => db.DraftApprenticeships).ReturnsDbSet(draftApprenticeships); _overlapCheckServiceMock .Setup(m => m.CheckForOverlapsOnStartDate(uln, It.IsAny(), apprenticeshipId, It.IsAny())) - .ReturnsAsync(new OverlapCheckResultOnStartDate(true, previousApprenticeshipId)); + .ReturnsAsync(new OverlapCheckResultOnStartDate(true, previousApprenticeshipId,false)); // act var result = await _sut.CreateOverlappingTrainingDateRequest(apprenticeshipId, Party.Provider, null, @@ -197,13 +198,14 @@ string uln var draftApprenticeships = GetDraftApprenticeshipTestData(apprenticeshipId, false); draftApprenticeships[0].Id = apprenticeshipId; draftApprenticeships[0].Uln = uln; + draftApprenticeships[0].PreviousApprenticeship.WithdrawnReasonCode = null; _dbContext.Setup(db => db.DraftApprenticeships).ReturnsDbSet(draftApprenticeships); _overlapCheckServiceMock .Setup(m => m.CheckForOverlapsOnStartDate(uln, It.IsAny(), apprenticeshipId, It.IsAny())) - .ReturnsAsync(new OverlapCheckResultOnStartDate(true, previousApprenticeshipId)); + .ReturnsAsync(new OverlapCheckResultOnStartDate(true, previousApprenticeshipId,false)); // act var result = await _sut.CreateOverlappingTrainingDateRequest(apprenticeshipId, Party.Provider, null, @@ -213,6 +215,35 @@ string uln _dbContext.Verify(db => db.SaveChangesAsync(It.IsAny()), Times.Once); } + [Test, MoqAutoData] + public async Task WhenOverlapExists_OverlapRequestNotCreated_IfWithdrawnFromIlr( + UserInfo userInfo, + long apprenticeshipId, + long previousApprenticeshipId, + string uln + ) + { + // arrange + var now = DateTime.UtcNow; + _currentDateTimeMock.Setup(m => m.UtcNow).Returns(now); + + var draftApprenticeships = GetDraftApprenticeshipTestData(apprenticeshipId, false); + draftApprenticeships[0].Id = apprenticeshipId; + draftApprenticeships[0].Uln = uln; + + _dbContext.Setup(db => db.DraftApprenticeships).ReturnsDbSet(draftApprenticeships); + + _overlapCheckServiceMock + .Setup(m => m.CheckForOverlapsOnStartDate(uln, It.IsAny(), + apprenticeshipId, It.IsAny())) + .ReturnsAsync(new OverlapCheckResultOnStartDate(true, previousApprenticeshipId, true)); + + // assert + Assert.ThrowsAsync(async () => + await _sut.CreateOverlappingTrainingDateRequest(apprenticeshipId, Party.Provider, null, userInfo, + new CancellationToken())); + } + private List GetDraftApprenticeshipTestData(long testApprenticeshipId, bool isCohortApproved) { diff --git a/src/CommitmentsV2/SFA.DAS.CommitmentsV2.UnitTests/Services/ResolveOverlappingTrainingDateRequestServiceTests.cs b/src/CommitmentsV2/SFA.DAS.CommitmentsV2.UnitTests/Services/ResolveOverlappingTrainingDateRequestServiceTests.cs index 3d83a183e7..a2cdbb0075 100644 --- a/src/CommitmentsV2/SFA.DAS.CommitmentsV2.UnitTests/Services/ResolveOverlappingTrainingDateRequestServiceTests.cs +++ b/src/CommitmentsV2/SFA.DAS.CommitmentsV2.UnitTests/Services/ResolveOverlappingTrainingDateRequestServiceTests.cs @@ -254,7 +254,7 @@ public ResolveOverlappingTrainingDateRequestServiceTestsFixture() _fixture = new Fixture(); UnitOfWorkContext = new UnitOfWorkContext(); _overlapCheckService = new Mock(); - _overlapCheckResultOnStartDate = new OverlapCheckResultOnStartDate(false, null); + _overlapCheckResultOnStartDate = new OverlapCheckResultOnStartDate(false, null,false); _overlapCheckService.Setup(x => x.CheckForOverlapsOnStartDate(It.IsAny(), It.IsAny(), null, It.IsAny())).ReturnsAsync(() => _overlapCheckResultOnStartDate); @@ -271,7 +271,7 @@ public ResolveOverlappingTrainingDateRequestServiceTestsFixture() public ResolveOverlappingTrainingDateRequestServiceTestsFixture SetupOverlapCheckService(bool hasStartDateOverlap, long? apprneticeshipId) { - _overlapCheckResultOnStartDate = new OverlapCheckResultOnStartDate(hasStartDateOverlap, apprneticeshipId); + _overlapCheckResultOnStartDate = new OverlapCheckResultOnStartDate(hasStartDateOverlap, apprneticeshipId,false); return this; } diff --git a/src/CommitmentsV2/SFA.DAS.CommitmentsV2/Application/Queries/GetOverlappingApprenticeshipDetails/ValidateUlnOverlapOnStartDateQueryHandler.cs b/src/CommitmentsV2/SFA.DAS.CommitmentsV2/Application/Queries/GetOverlappingApprenticeshipDetails/ValidateUlnOverlapOnStartDateQueryHandler.cs index 4bc7a73cf8..618d955a99 100644 --- a/src/CommitmentsV2/SFA.DAS.CommitmentsV2/Application/Queries/GetOverlappingApprenticeshipDetails/ValidateUlnOverlapOnStartDateQueryHandler.cs +++ b/src/CommitmentsV2/SFA.DAS.CommitmentsV2/Application/Queries/GetOverlappingApprenticeshipDetails/ValidateUlnOverlapOnStartDateQueryHandler.cs @@ -11,7 +11,12 @@ public async Task Handle(ValidateUlnOv var apprenticeshipWithOverlap = await overlapCheckService.CheckForOverlapsOnStartDate(request.Uln, new Domain.Entities.DateRange(startDate, endDate), null, cancellationToken); - var result = new ValidateUlnOverlapOnStartDateQueryResult { HasStartDateOverlap = apprenticeshipWithOverlap.HasOverlappingStartDate, HasOverlapWithApprenticeshipId = apprenticeshipWithOverlap.ApprenticeshipId }; + var result = new ValidateUlnOverlapOnStartDateQueryResult + { + HasStartDateOverlap = apprenticeshipWithOverlap.HasOverlappingStartDate, + HasOverlapWithApprenticeshipId = apprenticeshipWithOverlap.ApprenticeshipId, + HasOverlapWithIlrWithdrawnApprenticeship = apprenticeshipWithOverlap.HasOverlapWithIlrWithdrawnApprenticeship + }; return await Task.FromResult(result); } diff --git a/src/CommitmentsV2/SFA.DAS.CommitmentsV2/Application/Queries/GetOverlappingApprenticeshipDetails/ValidateUlnOverlapOnStartDateQueryResult.cs b/src/CommitmentsV2/SFA.DAS.CommitmentsV2/Application/Queries/GetOverlappingApprenticeshipDetails/ValidateUlnOverlapOnStartDateQueryResult.cs index f1c6d5e7b4..7f6464bf5a 100644 --- a/src/CommitmentsV2/SFA.DAS.CommitmentsV2/Application/Queries/GetOverlappingApprenticeshipDetails/ValidateUlnOverlapOnStartDateQueryResult.cs +++ b/src/CommitmentsV2/SFA.DAS.CommitmentsV2/Application/Queries/GetOverlappingApprenticeshipDetails/ValidateUlnOverlapOnStartDateQueryResult.cs @@ -4,4 +4,5 @@ public class ValidateUlnOverlapOnStartDateQueryResult { public long? HasOverlapWithApprenticeshipId { get; set; } public bool HasStartDateOverlap { get; set; } + public bool HasOverlapWithIlrWithdrawnApprenticeship { get; set; } } \ No newline at end of file diff --git a/src/CommitmentsV2/SFA.DAS.CommitmentsV2/Domain/Entities/OverlapCheckResultOnStartDate.cs b/src/CommitmentsV2/SFA.DAS.CommitmentsV2/Domain/Entities/OverlapCheckResultOnStartDate.cs index 36e4f77196..d34c7530b1 100644 --- a/src/CommitmentsV2/SFA.DAS.CommitmentsV2/Domain/Entities/OverlapCheckResultOnStartDate.cs +++ b/src/CommitmentsV2/SFA.DAS.CommitmentsV2/Domain/Entities/OverlapCheckResultOnStartDate.cs @@ -2,12 +2,14 @@ public class OverlapCheckResultOnStartDate { - public OverlapCheckResultOnStartDate(bool hasOverlappingStartDate, long? apprenticeshipId) + public OverlapCheckResultOnStartDate(bool hasOverlappingStartDate, long? apprenticeshipId, bool hasOverlapWithIlrWithdrawnApprenticeship) { HasOverlappingStartDate = hasOverlappingStartDate; ApprenticeshipId = apprenticeshipId; + HasOverlapWithIlrWithdrawnApprenticeship = hasOverlapWithIlrWithdrawnApprenticeship; } public bool HasOverlappingStartDate { get; } public long? ApprenticeshipId { get; } + public bool HasOverlapWithIlrWithdrawnApprenticeship { get; } } \ No newline at end of file diff --git a/src/CommitmentsV2/SFA.DAS.CommitmentsV2/Services/OverlapCheckService.cs b/src/CommitmentsV2/SFA.DAS.CommitmentsV2/Services/OverlapCheckService.cs index ede930b215..8c2340d054 100644 --- a/src/CommitmentsV2/SFA.DAS.CommitmentsV2/Services/OverlapCheckService.cs +++ b/src/CommitmentsV2/SFA.DAS.CommitmentsV2/Services/OverlapCheckService.cs @@ -64,9 +64,10 @@ public async Task CheckForOverlapsOnStartDate(str { break; } - } + } + - return new OverlapCheckResultOnStartDate(overlapStartDate, apprenticeshipId); + return new OverlapCheckResultOnStartDate(overlapStartDate, apprenticeshipId, await IsOverlapWithIlrWithdrawnApprenticeship(apprenticeshipId)); } private async Task> GetCandidateUlnUtilisations(string uln, long? existingApprenticeshipId, CancellationToken cancellationToken) @@ -125,4 +126,12 @@ public async Task> CheckForOverlaps(long cohortId, Canc return overlapCheckResult.ToList(); } + + private async Task IsOverlapWithIlrWithdrawnApprenticeship(long? apprenticeshipId) + { + return apprenticeshipId.HasValue && await dbContext.Value.Apprenticeships + .Where(x => x.Id == apprenticeshipId) + .AnyAsync(x => x.WithdrawnReasonCode.HasValue); + } + } \ No newline at end of file diff --git a/src/CommitmentsV2/SFA.DAS.CommitmentsV2/Services/OverlappingTrainingDateRequestDomainService.cs b/src/CommitmentsV2/SFA.DAS.CommitmentsV2/Services/OverlappingTrainingDateRequestDomainService.cs index 0853869d41..f95f7d7189 100644 --- a/src/CommitmentsV2/SFA.DAS.CommitmentsV2/Services/OverlappingTrainingDateRequestDomainService.cs +++ b/src/CommitmentsV2/SFA.DAS.CommitmentsV2/Services/OverlappingTrainingDateRequestDomainService.cs @@ -29,6 +29,12 @@ public async Task CreateOverlappingTrainingDateR throw new BadRequestException($"Draft Apprenticeship {apprenticeshipId}"); } + if (draftApprenticeship.PreviousApprenticeship.WithdrawnReasonCode.HasValue) + { + throw new InvalidOperationException( + $"Can't create Overlapping Training Date Request. Draft apprenticeship {draftApprenticeship.Id} has overlap with a withdrawn apprenticeship."); + } + if (draftApprenticeship.Cohort.IsApprovedByAllParties) { throw new InvalidOperationException($"Cohort {draftApprenticeship.Cohort.Id} is approved by all parties and can't be modified"); @@ -49,14 +55,14 @@ public async Task CreateOverlappingTrainingDateR { throw new InvalidOperationException( $"Can't create Overlapping Training Date Request. Draft apprenticeship {draftApprenticeship.Id} doesn't have overlap with another apprenticeship."); - } + } var result = draftApprenticeship.CreateOverlappingTrainingDateRequest(originatingParty, changeOfEmployerOriginalApprenticeId ?? overlapResult.ApprenticeshipId.Value, userInfo, currentDateTime.UtcNow); - + await dbContext.Value.SaveChangesAsync(cancellationToken); - + return result; } From 0eb050d117cce8cdb451a7f7fc66ac2e64179e82 Mon Sep 17 00:00:00 2001 From: skumardfe Date: Wed, 24 Jun 2026 22:22:28 +0100 Subject: [PATCH 2/5] refactor code --- .../ValidateUlnOverlapOnStartDateTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/CommitmentsV2/SFA.DAS.CommitmentsV2.Api.UnitTests/Controllers/OverlappingTrainingDateRequestControllerTests/ValidateUlnOverlapOnStartDateTests.cs b/src/CommitmentsV2/SFA.DAS.CommitmentsV2.Api.UnitTests/Controllers/OverlappingTrainingDateRequestControllerTests/ValidateUlnOverlapOnStartDateTests.cs index dac5743e8f..979d0195ca 100644 --- a/src/CommitmentsV2/SFA.DAS.CommitmentsV2.Api.UnitTests/Controllers/OverlappingTrainingDateRequestControllerTests/ValidateUlnOverlapOnStartDateTests.cs +++ b/src/CommitmentsV2/SFA.DAS.CommitmentsV2.Api.UnitTests/Controllers/OverlappingTrainingDateRequestControllerTests/ValidateUlnOverlapOnStartDateTests.cs @@ -32,7 +32,7 @@ public async Task ValidateUlnOverlapOnStartDate_HasOverlapWithIlrWithdrawnAppren var result = response.Value as ValidateUlnOverlapOnStartDateResponse; result.Should().NotBeNull(); - result.HasOverlapWithIlrWithdrawnApprenticeship.Value.Should().Be(HasOverlapWithIlrWithdrawnApprenticeship); + result.HasOverlapWithIlrWithdrawnApprenticeship.Should().Be(HasOverlapWithIlrWithdrawnApprenticeship); } private class ValidateUlnOverlapOnStartDateFixture From 2e886f0af49f847858e103db4fa9efd24d358d47 Mon Sep 17 00:00:00 2001 From: skumardfe Date: Tue, 30 Jun 2026 19:11:33 +0100 Subject: [PATCH 3/5] refactor code --- ...ngTrainingDateRequestDomainServiceTests.cs | 11 ++++---- .../Services/OverlapCheckService.cs | 25 +++++++++++-------- ...lappingTrainingDateRequestDomainService.cs | 16 ++++++------ 3 files changed, 27 insertions(+), 25 deletions(-) diff --git a/src/CommitmentsV2/SFA.DAS.CommitmentsV2.UnitTests/Services/OverlappingTrainingDateRequestDomainServiceTests.cs b/src/CommitmentsV2/SFA.DAS.CommitmentsV2.UnitTests/Services/OverlappingTrainingDateRequestDomainServiceTests.cs index d9afe9f63a..ce0616b79b 100644 --- a/src/CommitmentsV2/SFA.DAS.CommitmentsV2.UnitTests/Services/OverlappingTrainingDateRequestDomainServiceTests.cs +++ b/src/CommitmentsV2/SFA.DAS.CommitmentsV2.UnitTests/Services/OverlappingTrainingDateRequestDomainServiceTests.cs @@ -70,7 +70,7 @@ public void WhenPartyIsProvider_AndApprenticeshipIdInvalid_ThrowException( long apprenticeshipId ) { - // arrange + // arrange var draftApprenticeships = Fixture .Build() .With(app => app.Id, Fixture.Create>().Where(l => l != apprenticeshipId).First()) @@ -143,7 +143,7 @@ string uln _overlapCheckServiceMock .Setup(m => m.CheckForOverlapsOnStartDate(uln, It.IsAny(), apprenticeshipId, It.IsAny())) - .ReturnsAsync(new OverlapCheckResultOnStartDate(false, null,false)); + .ReturnsAsync(new OverlapCheckResultOnStartDate(false, null, false)); // assert Assert.ThrowsAsync(async () => @@ -173,7 +173,7 @@ string uln _overlapCheckServiceMock .Setup(m => m.CheckForOverlapsOnStartDate(uln, It.IsAny(), apprenticeshipId, It.IsAny())) - .ReturnsAsync(new OverlapCheckResultOnStartDate(true, previousApprenticeshipId,false)); + .ReturnsAsync(new OverlapCheckResultOnStartDate(true, previousApprenticeshipId, false)); // act var result = await _sut.CreateOverlappingTrainingDateRequest(apprenticeshipId, Party.Provider, null, @@ -205,7 +205,7 @@ string uln _overlapCheckServiceMock .Setup(m => m.CheckForOverlapsOnStartDate(uln, It.IsAny(), apprenticeshipId, It.IsAny())) - .ReturnsAsync(new OverlapCheckResultOnStartDate(true, previousApprenticeshipId,false)); + .ReturnsAsync(new OverlapCheckResultOnStartDate(true, previousApprenticeshipId, false)); // act var result = await _sut.CreateOverlappingTrainingDateRequest(apprenticeshipId, Party.Provider, null, @@ -219,7 +219,6 @@ string uln public async Task WhenOverlapExists_OverlapRequestNotCreated_IfWithdrawnFromIlr( UserInfo userInfo, long apprenticeshipId, - long previousApprenticeshipId, string uln ) { @@ -236,7 +235,7 @@ string uln _overlapCheckServiceMock .Setup(m => m.CheckForOverlapsOnStartDate(uln, It.IsAny(), apprenticeshipId, It.IsAny())) - .ReturnsAsync(new OverlapCheckResultOnStartDate(true, previousApprenticeshipId, true)); + .ReturnsAsync(new OverlapCheckResultOnStartDate(true, null, true)); // assert Assert.ThrowsAsync(async () => diff --git a/src/CommitmentsV2/SFA.DAS.CommitmentsV2/Services/OverlapCheckService.cs b/src/CommitmentsV2/SFA.DAS.CommitmentsV2/Services/OverlapCheckService.cs index 8c2340d054..f7a68501c7 100644 --- a/src/CommitmentsV2/SFA.DAS.CommitmentsV2/Services/OverlapCheckService.cs +++ b/src/CommitmentsV2/SFA.DAS.CommitmentsV2/Services/OverlapCheckService.cs @@ -21,9 +21,11 @@ public async Task CheckForOverlaps(string uln, DateRange ran case OverlapStatus.OverlappingStartDate: overlapStartDate = true; break; + case OverlapStatus.OverlappingEndDate: overlapEndDate = true; break; + case OverlapStatus.DateWithin: case OverlapStatus.DateEmbrace: overlapStartDate = true; @@ -45,7 +47,9 @@ public async Task CheckForOverlapsOnStartDate(str var overlapStartDate = false; long? apprenticeshipId = null; - foreach (var utilisation in await GetCandidateUlnUtilisations(uln, existingApprenticeshipId, cancellationToken)) + var utilisations = await GetCandidateUlnUtilisations(uln, existingApprenticeshipId, cancellationToken); + + foreach (var utilisation in utilisations) { var overlapStatus = utilisation.DateRange.DetermineOverlap(range); @@ -64,10 +68,9 @@ public async Task CheckForOverlapsOnStartDate(str { break; } - } - + } - return new OverlapCheckResultOnStartDate(overlapStartDate, apprenticeshipId, await IsOverlapWithIlrWithdrawnApprenticeship(apprenticeshipId)); + return new OverlapCheckResultOnStartDate(overlapStartDate, apprenticeshipId, await IsOverlapWithIlrWithdrawnApprenticeship(utilisations, apprenticeshipId)); } private async Task> GetCandidateUlnUtilisations(string uln, long? existingApprenticeshipId, CancellationToken cancellationToken) @@ -95,9 +98,9 @@ public async Task> CheckForEmailOverlaps(long coho var overlappingEmails = await emailOverlapService.GetOverlappingEmails(cohortId, cancellationToken); var singleEmails = from overlap in overlappingEmails - group overlap by overlap.RowId + group overlap by overlap.RowId into groups - select groups.OrderBy(e => e.RowId).First(); + select groups.OrderBy(e => e.RowId).First(); var summary = singleEmails.Select(x => new EmailOverlapCheckResult(x.RowId, x.OverlapStatus, x.IsApproved)); @@ -127,11 +130,11 @@ public async Task> CheckForOverlaps(long cohortId, Canc return overlapCheckResult.ToList(); } - private async Task IsOverlapWithIlrWithdrawnApprenticeship(long? apprenticeshipId) + private async Task IsOverlapWithIlrWithdrawnApprenticeship(IEnumerable utilisations, long? excludeId) { - return apprenticeshipId.HasValue && await dbContext.Value.Apprenticeships - .Where(x => x.Id == apprenticeshipId) - .AnyAsync(x => x.WithdrawnReasonCode.HasValue); - } + var apprenticehipIds = utilisations.Where(t => excludeId == null || t.ApprenticeshipId != excludeId).Select(x => x.ApprenticeshipId).ToList(); + return await dbContext.Value.Apprenticeships.AsNoTracking() + .AnyAsync(x => apprenticehipIds.Contains(x.Id) && x.WithdrawnReasonCode.HasValue); + } } \ No newline at end of file diff --git a/src/CommitmentsV2/SFA.DAS.CommitmentsV2/Services/OverlappingTrainingDateRequestDomainService.cs b/src/CommitmentsV2/SFA.DAS.CommitmentsV2/Services/OverlappingTrainingDateRequestDomainService.cs index f95f7d7189..6fb3abea11 100644 --- a/src/CommitmentsV2/SFA.DAS.CommitmentsV2/Services/OverlappingTrainingDateRequestDomainService.cs +++ b/src/CommitmentsV2/SFA.DAS.CommitmentsV2/Services/OverlappingTrainingDateRequestDomainService.cs @@ -22,18 +22,12 @@ public async Task CreateOverlappingTrainingDateR var draftApprenticeship = await dbContext.Value.DraftApprenticeships .Include(a => a.Cohort) - .SingleOrDefaultAsync(a => a.Id == apprenticeshipId, cancellationToken); + .SingleOrDefaultAsync(a => a.Id == apprenticeshipId, cancellationToken); if (draftApprenticeship == null) { throw new BadRequestException($"Draft Apprenticeship {apprenticeshipId}"); - } - - if (draftApprenticeship.PreviousApprenticeship.WithdrawnReasonCode.HasValue) - { - throw new InvalidOperationException( - $"Can't create Overlapping Training Date Request. Draft apprenticeship {draftApprenticeship.Id} has overlap with a withdrawn apprenticeship."); - } + } if (draftApprenticeship.Cohort.IsApprovedByAllParties) { @@ -50,6 +44,12 @@ public async Task CreateOverlappingTrainingDateR new Domain.Entities.DateRange(draftApprenticeship.StartDate.Value, draftApprenticeship.EndDate.Value), draftApprenticeship.Id, cancellationToken); + if (overlapResult.HasOverlapWithIlrWithdrawnApprenticeship) + { + throw new InvalidOperationException( + $"Can't create Overlapping Training Date Request. Draft apprenticeship {draftApprenticeship.Id} has overlap with a withdrawn apprenticeship."); + } + if (changeOfEmployerOriginalApprenticeId == null && (!overlapResult.HasOverlappingStartDate || overlapResult.ApprenticeshipId == null)) { From c236a2fa991a98b1a0754788b996964c6db28c1b Mon Sep 17 00:00:00 2001 From: Corey Faulconbridge Date: Mon, 13 Jul 2026 16:11:13 +0100 Subject: [PATCH 4/5] fix --- .../Services/OverlapCheckServiceTests.cs | 58 +++++++++++++++++++ .../Services/OverlapCheckService.cs | 11 ++-- 2 files changed, 65 insertions(+), 4 deletions(-) diff --git a/src/CommitmentsV2/SFA.DAS.CommitmentsV2.UnitTests/Services/OverlapCheckServiceTests.cs b/src/CommitmentsV2/SFA.DAS.CommitmentsV2.UnitTests/Services/OverlapCheckServiceTests.cs index 123a0e2be2..3848fd7c9c 100644 --- a/src/CommitmentsV2/SFA.DAS.CommitmentsV2.UnitTests/Services/OverlapCheckServiceTests.cs +++ b/src/CommitmentsV2/SFA.DAS.CommitmentsV2.UnitTests/Services/OverlapCheckServiceTests.cs @@ -77,6 +77,43 @@ public async Task ThenIfStartDateFallsWithinRangeOfExistingApprenticeshipThenIsO Assert.That(result.HasOverlappingStartDate, Is.True); } + [Test] + public async Task WhenOverlappingApprenticeshipHasIlrWithdrawal_HasOverlapWithIlrWithdrawnApprenticeshipIsTrue() + { + var result = await _fixture + .WithDateRange(new DateTime(2018, 03, 15), new DateTime(2018, 05, 15)) + .WithApprenticeshipWithdrawnReasonCode(1, 40) + .CheckForOverlapsOnStartDate(); + + result.HasOverlappingStartDate.Should().BeTrue(); + result.ApprenticeshipId.Should().Be(1); + result.HasOverlapWithIlrWithdrawnApprenticeship.Should().BeTrue(); + } + + [Test] + public async Task WhenOverlappingApprenticeshipHasNoIlrWithdrawal_HasOverlapWithIlrWithdrawnApprenticeshipIsFalse() + { + var result = await _fixture + .WithDateRange(new DateTime(2018, 03, 15), new DateTime(2018, 05, 15)) + .WithApprenticeshipWithdrawnReasonCode(1, null) + .CheckForOverlapsOnStartDate(); + + result.HasOverlapWithIlrWithdrawnApprenticeship.Should().BeFalse(); + } + + [Test] + public async Task WhenDifferentApprenticeshipOnUlnHasIlrWithdrawal_OverlappingRecordIsNotFlagged() + { + var result = await _fixture + .WithDateRange(new DateTime(2018, 03, 15), new DateTime(2018, 05, 15)) + .WithApprenticeshipWithdrawnReasonCode(1, null) + .WithApprenticeshipWithdrawnReasonCode(2, 40) + .CheckForOverlapsOnStartDate(); + + result.ApprenticeshipId.Should().Be(1); + result.HasOverlapWithIlrWithdrawnApprenticeship.Should().BeFalse(); + } + [Test] public async Task ThenIfEndDateFallsWithinRangeOfExistingApprenticeshipThenIsOverlapping() { @@ -325,6 +362,22 @@ public OverlapCheckServiceTestFixture WithExistingApprenticeship() return this; } + public OverlapCheckServiceTestFixture WithApprenticeshipWithdrawnReasonCode(long id, int? withdrawnReasonCode) + { + var existing = _db.Apprenticeships.Local.FirstOrDefault(a => a.Id == id); + if (existing != null) + { + existing.WithdrawnReasonCode = withdrawnReasonCode; + } + else + { + _db.Apprenticeships.Add(new Apprenticeship { Id = id, WithdrawnReasonCode = withdrawnReasonCode }); + } + + _db.SaveChanges(); + return this; + } + public OverlapCheckServiceTestFixture WithNoMatchingUlnUtilisations() { _ulnUtilisationService.Setup(x => x.GetUlnUtilisations(It.IsAny(), It.IsAny())) @@ -403,6 +456,11 @@ public async Task CheckForOverlaps() return await _overlapCheckService.CheckForOverlaps("", _startDate.To(_endDate), _apprenticeshipId, new CancellationToken()); } + public async Task CheckForOverlapsOnStartDate() + { + return await _overlapCheckService.CheckForOverlapsOnStartDate("", new SFA.DAS.CommitmentsV2.Domain.Entities.DateRange(_startDate, _endDate), _apprenticeshipId, CancellationToken.None); + } + public async Task CheckForEmailOverlaps() { return await _overlapCheckService.CheckForEmailOverlaps(_email, _startDate.To(_endDate), _apprenticeshipId, _cohortId, new CancellationToken()); diff --git a/src/CommitmentsV2/SFA.DAS.CommitmentsV2/Services/OverlapCheckService.cs b/src/CommitmentsV2/SFA.DAS.CommitmentsV2/Services/OverlapCheckService.cs index f7a68501c7..33aa062de7 100644 --- a/src/CommitmentsV2/SFA.DAS.CommitmentsV2/Services/OverlapCheckService.cs +++ b/src/CommitmentsV2/SFA.DAS.CommitmentsV2/Services/OverlapCheckService.cs @@ -70,7 +70,7 @@ public async Task CheckForOverlapsOnStartDate(str } } - return new OverlapCheckResultOnStartDate(overlapStartDate, apprenticeshipId, await IsOverlapWithIlrWithdrawnApprenticeship(utilisations, apprenticeshipId)); + return new OverlapCheckResultOnStartDate(overlapStartDate, apprenticeshipId, await IsOverlapWithIlrWithdrawnApprenticeship(apprenticeshipId)); } private async Task> GetCandidateUlnUtilisations(string uln, long? existingApprenticeshipId, CancellationToken cancellationToken) @@ -130,11 +130,14 @@ public async Task> CheckForOverlaps(long cohortId, Canc return overlapCheckResult.ToList(); } - private async Task IsOverlapWithIlrWithdrawnApprenticeship(IEnumerable utilisations, long? excludeId) + private async Task IsOverlapWithIlrWithdrawnApprenticeship(long? apprenticeshipId) { - var apprenticehipIds = utilisations.Where(t => excludeId == null || t.ApprenticeshipId != excludeId).Select(x => x.ApprenticeshipId).ToList(); + if (!apprenticeshipId.HasValue) + { + return false; + } return await dbContext.Value.Apprenticeships.AsNoTracking() - .AnyAsync(x => apprenticehipIds.Contains(x.Id) && x.WithdrawnReasonCode.HasValue); + .AnyAsync(x => x.Id == apprenticeshipId.Value && x.WithdrawnReasonCode.HasValue); } } \ No newline at end of file From 3558636f561cc5992b32038bcf2c50070b8be2d7 Mon Sep 17 00:00:00 2001 From: skumardfe Date: Tue, 4 Aug 2026 15:47:49 +0100 Subject: [PATCH 5/5] update the references --- .../Services/OverlapCheckServiceTests.cs | 2 +- .../OverlappingTrainingDateRequestDomainServiceTests.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/CommitmentsV2/SFA.DAS.CommitmentsV2.UnitTests/Services/OverlapCheckServiceTests.cs b/src/CommitmentsV2/SFA.DAS.CommitmentsV2.UnitTests/Services/OverlapCheckServiceTests.cs index 7484415682..0a50b8d4b1 100644 --- a/src/CommitmentsV2/SFA.DAS.CommitmentsV2.UnitTests/Services/OverlapCheckServiceTests.cs +++ b/src/CommitmentsV2/SFA.DAS.CommitmentsV2.UnitTests/Services/OverlapCheckServiceTests.cs @@ -489,7 +489,7 @@ public async Task CheckForOverlaps() public async Task CheckForOverlapsOnStartDate() { - return await _overlapCheckService.CheckForOverlapsOnStartDate("", new SFA.DAS.CommitmentsV2.Domain.Entities.DateRange(_startDate, _endDate), _apprenticeshipId, CancellationToken.None); + return await _overlapCheckService.CheckForOverlapsOnStartDate("", new SFA.DAS.CommitmentsV2.Domain.Entities.CourseDateRange(_startDate, _endDate), _apprenticeshipId, CancellationToken.None); } public async Task CheckForEmailOverlaps() diff --git a/src/CommitmentsV2/SFA.DAS.CommitmentsV2.UnitTests/Services/OverlappingTrainingDateRequestDomainServiceTests.cs b/src/CommitmentsV2/SFA.DAS.CommitmentsV2.UnitTests/Services/OverlappingTrainingDateRequestDomainServiceTests.cs index b9d2e39fdf..f8aa1206be 100644 --- a/src/CommitmentsV2/SFA.DAS.CommitmentsV2.UnitTests/Services/OverlappingTrainingDateRequestDomainServiceTests.cs +++ b/src/CommitmentsV2/SFA.DAS.CommitmentsV2.UnitTests/Services/OverlappingTrainingDateRequestDomainServiceTests.cs @@ -233,7 +233,7 @@ string uln _dbContext.Setup(db => db.DraftApprenticeships).ReturnsDbSet(draftApprenticeships); _overlapCheckServiceMock - .Setup(m => m.CheckForOverlapsOnStartDate(uln, It.IsAny(), + .Setup(m => m.CheckForOverlapsOnStartDate(uln, It.IsAny(), apprenticeshipId, It.IsAny())) .ReturnsAsync(new OverlapCheckResultOnStartDate(true, null, true));