From 8cfb0c6079f977676bb6d1ea8b739e02f6e2bbed Mon Sep 17 00:00:00 2001 From: skumardfe Date: Fri, 31 Jul 2026 16:39:33 +0100 Subject: [PATCH 1/3] Implement Learning Resumed event --- .../LearningResumedEventHandler.cs | 99 ++++++ .../LearningEvents/LearningResumedEvent.cs | 13 + .../LearningResumedEventHandlerTests.cs | 291 ++++++++++++++++++ .../Events/ApprenticeshipResumedEvent.cs | 1 + .../Models/Apprenticeship.cs | 23 +- 5 files changed, 425 insertions(+), 2 deletions(-) create mode 100644 src/CommitmentsV2/SFA.DAS.CommitmentsV2.ExternalHandlers/EventHandlers/LearningResumedEventHandler.cs create mode 100644 src/CommitmentsV2/SFA.DAS.CommitmentsV2.ExternalHandlers/LearningEvents/LearningResumedEvent.cs create mode 100644 src/CommitmentsV2/SFA.DAS.CommitmentsV2.ExternalMessageHandlers.UnitTests/EventHandlers/LearningResumedEventHandlerTests.cs diff --git a/src/CommitmentsV2/SFA.DAS.CommitmentsV2.ExternalHandlers/EventHandlers/LearningResumedEventHandler.cs b/src/CommitmentsV2/SFA.DAS.CommitmentsV2.ExternalHandlers/EventHandlers/LearningResumedEventHandler.cs new file mode 100644 index 0000000000..caeadf80c8 --- /dev/null +++ b/src/CommitmentsV2/SFA.DAS.CommitmentsV2.ExternalHandlers/EventHandlers/LearningResumedEventHandler.cs @@ -0,0 +1,99 @@ +using System; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; +using Microsoft.IdentityModel.Tokens; +using NServiceBus; +using SFA.DAS.CommitmentsV2.Data; +using SFA.DAS.CommitmentsV2.Domain.Exceptions; +using SFA.DAS.CommitmentsV2.Messages.Commands; +using SFA.DAS.CommitmentsV2.Models; +using SFA.DAS.CommitmentsV2.Shared.Extensions; +using SFA.DAS.CommitmentsV2.Types; +using SFA.DAS.Learning.Types; + +namespace SFA.DAS.CommitmentsV2.ExternalHandlers.EventHandlers; + +public class LearningResumedEventHandler( + Lazy dbContext, + ILogger logger) + : IHandleMessages +{ + public async Task Handle(LearningResumedEvent message, IMessageHandlerContext context) + { + try + { + logger.LogInformation(" Started executing {Event}", nameof(LearningResumedEvent)); + + if (message is null) + { + logger.LogInformation(" {Event} received null message : {Event}", nameof(LearningResumedEvent), message == null); + return; + } + + logger.LogInformation("LearningResumedEvent for ApprenticeshipId {ApprenticeshipId} with ResumeDate {ResumeDate}", + message.ApprenticeshipId, message.ResumeDate); + var db = dbContext.Value; + var apprentice = await db.Apprenticeships + .Include(a => a.Cohort) + .ThenInclude(c => c.Provider) + .SingleOrDefaultAsync(t => t.Id == message.ApprenticeshipId); + + if (apprentice == null) + { + throw new DomainException(nameof(apprentice), $"Apprenticeship with Id {message.ApprenticeshipId} not found."); + } + + ValidateResumeDate(message.ResumeDate, apprentice); + + apprentice.SetIlrResumed(message.ResumeDate); + + var historyCommand = new StoreLearningHistoryCommand + { + ApprenticeshipId = message.ApprenticeshipId, + Source = LearningSourceType.ILRStatusChange, + ChangeType = LearningChangeType.AutoApproved, + LearningKey = message.LearningKey, + AppliedDate = message.Created, + Description = $"Learning has been resumed on {message.ResumeDate.ToGdsFormat()}" + }; + await context.Send(historyCommand); + + logger.LogInformation(" Executing {Event} completed", nameof(LearningResumedEvent)); + } + catch (Exception e) + { + logger.LogError(e, "Error processing LearningResumedEventHandler for ApprenticeshipId {0}", message.ApprenticeshipId); + throw; + } + } + + private void ValidateResumeDate(DateTime resumeDate, Apprenticeship apprenticeship) + { + if (apprenticeship.PaymentStatus == PaymentStatus.Completed || apprenticeship.PaymentStatus == PaymentStatus.Withdrawn) + { + throw new DomainException(nameof(resumeDate), "Learning cannot be Resumed if Payment Status is Completed or Withdrawn. Unable to resume apprenticeship"); + } + + if (apprenticeship.StartDate.HasValue && apprenticeship.StartDate.Value.Date >= resumeDate.Date) + { + throw new DomainException(nameof(resumeDate), "Invalid resume date. Learner not started."); + } + + if (apprenticeship.EndDate.HasValue && resumeDate.Date >= apprenticeship.EndDate.Value.Date) + { + throw new DomainException(nameof(resumeDate), "Invalid resume date. Resume date cannot be on or after the end date."); + } + + if (apprenticeship.PauseDate.HasValue && resumeDate.Date < apprenticeship.PauseDate.Value.Date) + { + throw new DomainException(nameof(resumeDate), "Invalid resume date. Resume date cannot be before the pausedate."); + } + + if(!apprenticeship.PauseDate.HasValue && resumeDate.Date != DateTime.MinValue) + { + logger.LogInformation("apprenticeship paused date is missing for apprenticeship {ApprenticeshipId}.", apprenticeship.Id); + } + } +} \ No newline at end of file diff --git a/src/CommitmentsV2/SFA.DAS.CommitmentsV2.ExternalHandlers/LearningEvents/LearningResumedEvent.cs b/src/CommitmentsV2/SFA.DAS.CommitmentsV2.ExternalHandlers/LearningEvents/LearningResumedEvent.cs new file mode 100644 index 0000000000..04fa58adce --- /dev/null +++ b/src/CommitmentsV2/SFA.DAS.CommitmentsV2.ExternalHandlers/LearningEvents/LearningResumedEvent.cs @@ -0,0 +1,13 @@ +using System; + +namespace SFA.DAS.Learning.Types; + +// Replace with SFA.DAS.Learning.Types NuGet package when LearningResumedEvent is published. + +public class LearningResumedEvent +{ + public Guid LearningKey { get; set; } + public long ApprenticeshipId { get; set; } + public DateTime Created { get; set; } + public DateTime ResumeDate { get; set; } +} \ No newline at end of file diff --git a/src/CommitmentsV2/SFA.DAS.CommitmentsV2.ExternalMessageHandlers.UnitTests/EventHandlers/LearningResumedEventHandlerTests.cs b/src/CommitmentsV2/SFA.DAS.CommitmentsV2.ExternalMessageHandlers.UnitTests/EventHandlers/LearningResumedEventHandlerTests.cs new file mode 100644 index 0000000000..791e7d169b --- /dev/null +++ b/src/CommitmentsV2/SFA.DAS.CommitmentsV2.ExternalMessageHandlers.UnitTests/EventHandlers/LearningResumedEventHandlerTests.cs @@ -0,0 +1,291 @@ +using System; +using System.Linq; +using System.Threading.Tasks; +using AutoFixture; +using FluentAssertions; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; +using Moq; +using NServiceBus; +using NUnit.Framework; +using SFA.DAS.CommitmentsV2.Configuration; +using SFA.DAS.CommitmentsV2.Data; +using SFA.DAS.CommitmentsV2.Domain.Exceptions; +using SFA.DAS.CommitmentsV2.ExternalHandlers.EventHandlers; +using SFA.DAS.CommitmentsV2.Messages.Commands; +using SFA.DAS.CommitmentsV2.Messages.Events; +using SFA.DAS.CommitmentsV2.Models; +using SFA.DAS.CommitmentsV2.Shared.Extensions; +using SFA.DAS.CommitmentsV2.Types; +using SFA.DAS.Encoding; +using SFA.DAS.Learning.Types; +using SFA.DAS.UnitOfWork.Context; + +namespace SFA.DAS.CommitmentsV2.ExternalMessageHandlers.UnitTests.EventHandlers; + +public class LearningResumedEventHandlerTests +{ + public LearningResumedEventHandlerTestsFixture _fixture; + + [SetUp] + public void Arrange() + { + _fixture = new LearningResumedEventHandlerTestsFixture(); + } + + [TearDown] + public void TearDown() => _fixture.Dispose(); + + [Test] + public async Task Handle_WhenLearningResumedEventReceived() + { + await _fixture.SetPauseDate(DateTime.UtcNow.AddMonths(1)).SetResumeDate(DateTime.UtcNow.AddMonths(2)).Handle(); + _fixture.VerifyLearnerResumed(); + _fixture.VerifyStoreLearnerHistoryCommandIsSent(); + _fixture.VerifyLearningResumedEventIsPublished(); + } + + [Test] + public async Task ThenThrowsDomainException_WhenResumeDateIsEarlierThanLearningStartDate() + { + var act = async () => await _fixture.SetStartDate(DateTime.UtcNow.AddMonths(4)).SetResumeDate(DateTime.UtcNow.AddMonths(3)).Handle(); + await act.Should().ThrowAsync().Where(ex => ex.DomainErrors.First().ErrorMessage.Contains("Learner not started")); + _fixture.VerifyLearningResumedEventIsNotPublished(); + } + + [Test] + public async Task ThenThrowsDomainException_WhenResumeDateIsOnLearningStartDate() + { + var act = async () => await _fixture.SetStartDate(DateTime.UtcNow.AddMonths(3)).SetResumeDate(DateTime.UtcNow.AddMonths(3)).Handle(); + await act.Should().ThrowAsync().Where(ex => ex.DomainErrors.First().ErrorMessage.Contains("Learner not started")); + _fixture.VerifyLearningResumedEventIsNotPublished(); + } + + [Test] + public async Task ThenThrowsDomainException_WhenApprenticeshipNotfound() + { + var act = async () => await _fixture.SetEventApprenticeshipId(_fixture.fixture.Create()).Handle(); + await act.Should().ThrowAsync().Where(ex => ex.DomainErrors.First().ErrorMessage.Contains("not found")); + _fixture.VerifyLearningResumedEventIsNotPublished(); + } + + [Test] + public async Task ThenThrowsDomainException_WhenResumeDateIsAfterLearningEndDate() + { + var act = async () => await _fixture.SetEndDate(DateTime.UtcNow.AddMonths(5)).SetResumeDate(DateTime.UtcNow.AddMonths(6)).Handle(); + await act.Should().ThrowAsync().Where(ex => ex.DomainErrors.First().ErrorMessage.Contains("Resume date cannot be on or after the end date")); + _fixture.VerifyLearningResumedEventIsNotPublished(); + } + + [Test] + public async Task ThenThrowsDomainException_WhenResumeDateIsOnLearningEndDate() + { + var act = async () => await _fixture.SetEndDate(DateTime.UtcNow.AddMonths(3)).SetResumeDate(DateTime.UtcNow.AddMonths(3)).Handle(); + await act.Should().ThrowAsync().Where(ex => ex.DomainErrors.First().ErrorMessage.Contains("Resume date cannot be on or after the end date")); + _fixture.VerifyLearningResumedEventIsNotPublished(); + } + + [Test] + public async Task ThenThrowsDomainException_WhenResumeDateIsAfterPauseDate() + { + var act = async () => await _fixture.SetPauseDate(DateTime.UtcNow.AddMonths(5)).SetResumeDate(DateTime.UtcNow.AddMonths(4)).Handle(); + await act.Should().ThrowAsync().Where(ex => ex.DomainErrors.First().ErrorMessage.Contains("Resume date cannot be before the pausedate")); + _fixture.VerifyLearningResumedEventIsNotPublished(); + } + + [Test] + public async Task Handle_WhenLearningResumedEventReceivedOnPauseDate() + { + await _fixture.SetPauseDate(DateTime.UtcNow.AddMonths(2)).SetResumeDate(DateTime.UtcNow.AddMonths(2)).Handle(); + _fixture.VerifyLearnerResumed(); + _fixture.VerifyStoreLearnerHistoryCommandIsSent(); + _fixture.VerifyLearningResumedEventIsPublished(); + } + + [Test] + public async Task Handle_WhenLearningResumedEventReceivedAndNoPauseDate() + { + await _fixture.SetResumeDate(DateTime.UtcNow.AddMonths(2)).Handle(); + _fixture.VerifyLearnerResumed(); + _fixture.VerifyStoreLearnerHistoryCommandIsSent(); + _fixture.VerifyLearningResumedEventIsPublished(); + _fixture.VerifyLoggerLoggedInformation($"apprenticeship paused date is missing for apprenticeship"); + } + + [Test] + [TestCase(PaymentStatus.Withdrawn)] + [TestCase(PaymentStatus.Completed)] + public async Task ThenThrowsDomainException_WhenStatusIsWithdrawn(PaymentStatus status) + { + var act = async () => await _fixture.SetPaymentStatus(status).Handle(); + await act.Should().ThrowAsync().Where(ex => ex.DomainErrors.First().ErrorMessage.Contains("Learning cannot be Resumed if Payment Status is Completed or Withdrawn")); + _fixture.VerifyLearningResumedEventIsNotPublished(); + } +} + +public class LearningResumedEventHandlerTestsFixture +{ + public Fixture fixture { get; set; } + private ProviderCommitmentsDbContext _dbContext; + private Mock> _mockLogger; + private Mock _mockContext; + private LearningResumedEventHandler _handler; + private LearningResumedEvent _event; + private Mock _mockEncodingService; + private CommitmentsV2Configuration _commitmentsV2Configuration; + public UnitOfWorkContext UnitOfWorkContext { get; set; } + public long apprenticeshipId { get; set; } + + public LearningResumedEventHandlerTestsFixture() + { + fixture = new Fixture(); + _mockLogger = new Mock>(); + _mockContext = new Mock(); + _mockEncodingService = new Mock(); + _mockEncodingService.Setup(x => x.Encode(It.IsAny(), EncodingType.ApprenticeshipId)).Returns("APP123"); + _mockEncodingService.Setup(x => x.Encode(It.IsAny(), EncodingType.AccountId)).Returns("ACC123"); + + UnitOfWorkContext = new UnitOfWorkContext(); + + _event = fixture.Create(); + + _dbContext = new ProviderCommitmentsDbContext(new DbContextOptionsBuilder() + .UseInMemoryDatabase(Guid.NewGuid().ToString(), b => b.EnableNullChecks(false)) + .Options); + + apprenticeshipId = fixture.Create(); + _event.ApprenticeshipId = apprenticeshipId; + + var provider = new Provider() + { + UkPrn = 12345, + Name = "Test Provider" + }; + + var cohort = new Cohort + { + Id = fixture.Create(), + WithParty = Party.Provider, + Reference = fixture.Create(), + Provider = provider, + EmployerAccountId = 101 + }; + + var Apprenticeship = new Apprenticeship + { + Id = apprenticeshipId, + HasLearnerDataChanges = false, + FirstName = "Test", + LastName = "User", + DateOfBirth = DateTime.UtcNow.AddYears(-20), + Uln = fixture.Create().ToString(), + Cohort = cohort, + StartDate = DateTime.UtcNow.AddMonths(1), + EndDate = DateTime.UtcNow.AddMonths(13) + }; + + _dbContext.Cohorts.Add(cohort); + _dbContext.Apprenticeships.Add(Apprenticeship); + _dbContext.SaveChanges(); + + _handler = new LearningResumedEventHandler(new Lazy(() => _dbContext), + _mockLogger.Object); + } + + public LearningResumedEventHandlerTestsFixture SetStartDate(DateTime startDate) + { + var apprenticeship = _dbContext.Apprenticeships.Find(apprenticeshipId); + apprenticeship.StartDate = startDate; + _dbContext.SaveChanges(); + return this; + } + + public LearningResumedEventHandlerTestsFixture SetEndDate(DateTime endDate) + { + var apprenticeship = _dbContext.Apprenticeships.Find(apprenticeshipId); + apprenticeship.EndDate = endDate; + _dbContext.SaveChanges(); + return this; + } + + public LearningResumedEventHandlerTestsFixture SetPauseDate(DateTime pauseDate) + { + var apprenticeship = _dbContext.Apprenticeships.Find(apprenticeshipId); + apprenticeship.PauseDate = pauseDate; + _dbContext.SaveChanges(); + return this; + } + + public LearningResumedEventHandlerTestsFixture SetResumeDate(DateTime resumeDate) + { + _event.ResumeDate = resumeDate; + return this; + } + + public LearningResumedEventHandlerTestsFixture SetPaymentStatus(PaymentStatus status) + { + var apprenticeship = _dbContext.Apprenticeships.Find(apprenticeshipId); + apprenticeship.PaymentStatus = status; + _dbContext.SaveChanges(); + return this; + } + + public LearningResumedEventHandlerTestsFixture SetEventApprenticeshipId(long id) + { + _event.ApprenticeshipId = id; + return this; + } + + public async Task Handle() + { + await _handler.Handle(_event, _mockContext.Object); + } + + public void VerifyStoreLearnerHistoryCommandIsSent() + { + _mockContext.Verify(x => x.Send(It.Is(c => + c.ApprenticeshipId == _event.ApprenticeshipId && + c.Source == Types.LearningSourceType.ILRStatusChange && + c.ChangeType == Types.LearningChangeType.AutoApproved && + c.LearningKey == _event.LearningKey && + c.AppliedDate == _event.Created && + c.Description == $"Learning has been resumed on {_event.ResumeDate.ToGdsFormat()}" + ), It.IsAny()), Times.Once); + } + + public void VerifyLearningResumedEventIsPublished() + { + _ = _dbContext.Apprenticeships.Find(_event.ApprenticeshipId); + var resumedEvent = UnitOfWorkContext.GetEvents().OfType().First(); + resumedEvent.Should().NotBeNull(); + resumedEvent.ResumedOn.Date.Should().Be(_event.ResumeDate.Date); + resumedEvent.ApprenticeshipId.Should().Be(_event.ApprenticeshipId); + resumedEvent.ResumedViaILR.Should().BeTrue(); + } + + public void VerifyLearningResumedEventIsNotPublished() + { + var resumedEvent = UnitOfWorkContext.GetEvents().OfType().FirstOrDefault(); + resumedEvent.Should().BeNull(); + } + + public void VerifyLearnerResumed() + { + var updatedApprenticeship = _dbContext.Apprenticeships.Find(apprenticeshipId); + updatedApprenticeship.Should().NotBeNull(); + updatedApprenticeship.PaymentStatus.Should().Be(PaymentStatus.Active); + updatedApprenticeship.PauseDate.Should().BeNull(); + } + + public void VerifyLoggerLoggedInformation(string message) + { + _mockLogger.Verify(x => x.Log( + LogLevel.Information, + It.IsAny(), + It.Is((v, t) => v.ToString().Contains(message)), + null, + It.IsAny>()), Times.AtLeastOnce); + } + + public void Dispose() => _dbContext?.Dispose(); +} \ No newline at end of file diff --git a/src/CommitmentsV2/SFA.DAS.CommitmentsV2.Messages/Events/ApprenticeshipResumedEvent.cs b/src/CommitmentsV2/SFA.DAS.CommitmentsV2.Messages/Events/ApprenticeshipResumedEvent.cs index 29d31128b2..cb3f568acc 100644 --- a/src/CommitmentsV2/SFA.DAS.CommitmentsV2.Messages/Events/ApprenticeshipResumedEvent.cs +++ b/src/CommitmentsV2/SFA.DAS.CommitmentsV2.Messages/Events/ApprenticeshipResumedEvent.cs @@ -6,5 +6,6 @@ public class ApprenticeshipResumedEvent { public long ApprenticeshipId { get; set; } public DateTime ResumedOn { get; set; } + public bool ResumedViaILR { get; set; } = false; } } diff --git a/src/CommitmentsV2/SFA.DAS.CommitmentsV2/Models/Apprenticeship.cs b/src/CommitmentsV2/SFA.DAS.CommitmentsV2/Models/Apprenticeship.cs index 8228c1e21e..f6d87e9871 100644 --- a/src/CommitmentsV2/SFA.DAS.CommitmentsV2/Models/Apprenticeship.cs +++ b/src/CommitmentsV2/SFA.DAS.CommitmentsV2/Models/Apprenticeship.cs @@ -730,7 +730,6 @@ public void ResumeApprenticeship(ICurrentDateTime currentDateTime, Party party, }); } - public void FreezePayments(ICurrentDateTime currentDateTime, Party party, UserInfo userInfo, FreezePaymentsReason freezePaymentsReason) { var frozenOn = currentDateTime.UtcNow; @@ -1036,7 +1035,7 @@ private void ResolveDatalocks(DateTime stopDate) foreach (var dataLock in dataLocks) { - if(ChangeTrackingSession != null) + if (ChangeTrackingSession != null) { ChangeTrackingSession.TrackUpdate(dataLock); } @@ -1082,4 +1081,24 @@ public void SetIlrPaused(DateTime pausedDate) PausedViaILR = true }); } + + public void SetIlrResumed(DateTime resumedDate) + { + StartTrackingSession(UserAction.ResumeApprenticeship, Party.None, Cohort.EmployerAccountId, Cohort.ProviderId, null); + + ChangeTrackingSession.TrackUpdate(this); + + PaymentStatus = PaymentStatus.Active; + PauseDate = null; + ApprenticeshipStatus = ApprenticeshipStatus.Live; + + ChangeTrackingSession.CompleteTrackingSession(); + + Publish(() => new ApprenticeshipResumedEvent + { + ApprenticeshipId = Id, + ResumedOn = resumedDate, + ResumedViaILR = true + }); + } } \ No newline at end of file From bf8eccc21afb380eb241216fee076a3249b53a9e Mon Sep 17 00:00:00 2001 From: skumardfe Date: Wed, 5 Aug 2026 12:05:19 +0100 Subject: [PATCH 2/3] Update logic for active apprenticeships --- .../LearningResumedEventHandler.cs | 38 +++++++++++-------- .../LearningResumedEventHandlerTests.cs | 17 ++++++++- 2 files changed, 38 insertions(+), 17 deletions(-) diff --git a/src/CommitmentsV2/SFA.DAS.CommitmentsV2.ExternalHandlers/EventHandlers/LearningResumedEventHandler.cs b/src/CommitmentsV2/SFA.DAS.CommitmentsV2.ExternalHandlers/EventHandlers/LearningResumedEventHandler.cs index caeadf80c8..8959949083 100644 --- a/src/CommitmentsV2/SFA.DAS.CommitmentsV2.ExternalHandlers/EventHandlers/LearningResumedEventHandler.cs +++ b/src/CommitmentsV2/SFA.DAS.CommitmentsV2.ExternalHandlers/EventHandlers/LearningResumedEventHandler.cs @@ -3,7 +3,6 @@ using System.Threading.Tasks; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; -using Microsoft.IdentityModel.Tokens; using NServiceBus; using SFA.DAS.CommitmentsV2.Data; using SFA.DAS.CommitmentsV2.Domain.Exceptions; @@ -45,22 +44,29 @@ public async Task Handle(LearningResumedEvent message, IMessageHandlerContext co throw new DomainException(nameof(apprentice), $"Apprenticeship with Id {message.ApprenticeshipId} not found."); } - ValidateResumeDate(message.ResumeDate, apprentice); + if (apprentice.PaymentStatus == PaymentStatus.Active && !apprentice.PauseDate.HasValue) + { + logger.LogInformation("Apprenticeship {ApprenticeshipId} is already active and resumed.", apprentice.Id); + } + else + { + ValidateResumeDate(message.ResumeDate, apprentice); - apprentice.SetIlrResumed(message.ResumeDate); + apprentice.SetIlrResumed(message.ResumeDate); - var historyCommand = new StoreLearningHistoryCommand - { - ApprenticeshipId = message.ApprenticeshipId, - Source = LearningSourceType.ILRStatusChange, - ChangeType = LearningChangeType.AutoApproved, - LearningKey = message.LearningKey, - AppliedDate = message.Created, - Description = $"Learning has been resumed on {message.ResumeDate.ToGdsFormat()}" - }; - await context.Send(historyCommand); + var historyCommand = new StoreLearningHistoryCommand + { + ApprenticeshipId = message.ApprenticeshipId, + Source = LearningSourceType.ILRStatusChange, + ChangeType = LearningChangeType.AutoApproved, + LearningKey = message.LearningKey, + AppliedDate = message.Created, + Description = $"Learning has been resumed on {message.ResumeDate.ToGdsFormat()}" + }; + await context.Send(historyCommand); - logger.LogInformation(" Executing {Event} completed", nameof(LearningResumedEvent)); + logger.LogInformation(" Executing {Event} completed", nameof(LearningResumedEvent)); + } } catch (Exception e) { @@ -91,9 +97,9 @@ private void ValidateResumeDate(DateTime resumeDate, Apprenticeship apprenticesh throw new DomainException(nameof(resumeDate), "Invalid resume date. Resume date cannot be before the pausedate."); } - if(!apprenticeship.PauseDate.HasValue && resumeDate.Date != DateTime.MinValue) + if (!apprenticeship.PauseDate.HasValue && resumeDate.Date != DateTime.MinValue) { - logger.LogInformation("apprenticeship paused date is missing for apprenticeship {ApprenticeshipId}.", apprenticeship.Id); + logger.LogInformation("Apprenticeship paused date is missing for apprenticeship {ApprenticeshipId}.", apprenticeship.Id); } } } \ No newline at end of file diff --git a/src/CommitmentsV2/SFA.DAS.CommitmentsV2.ExternalMessageHandlers.UnitTests/EventHandlers/LearningResumedEventHandlerTests.cs b/src/CommitmentsV2/SFA.DAS.CommitmentsV2.ExternalMessageHandlers.UnitTests/EventHandlers/LearningResumedEventHandlerTests.cs index 791e7d169b..372e06ef7f 100644 --- a/src/CommitmentsV2/SFA.DAS.CommitmentsV2.ExternalMessageHandlers.UnitTests/EventHandlers/LearningResumedEventHandlerTests.cs +++ b/src/CommitmentsV2/SFA.DAS.CommitmentsV2.ExternalMessageHandlers.UnitTests/EventHandlers/LearningResumedEventHandlerTests.cs @@ -109,7 +109,7 @@ public async Task Handle_WhenLearningResumedEventReceivedAndNoPauseDate() _fixture.VerifyLearnerResumed(); _fixture.VerifyStoreLearnerHistoryCommandIsSent(); _fixture.VerifyLearningResumedEventIsPublished(); - _fixture.VerifyLoggerLoggedInformation($"apprenticeship paused date is missing for apprenticeship"); + _fixture.VerifyLoggerLoggedInformation($"Apprenticeship paused date is missing for apprenticeship"); } [Test] @@ -121,6 +121,16 @@ public async Task ThenThrowsDomainException_WhenStatusIsWithdrawn(PaymentStatus await act.Should().ThrowAsync().Where(ex => ex.DomainErrors.First().ErrorMessage.Contains("Learning cannot be Resumed if Payment Status is Completed or Withdrawn")); _fixture.VerifyLearningResumedEventIsNotPublished(); } + + [Test] + public async Task ThenLogstheInformation_WhenStatusIsActive() + { + await _fixture.SetPaymentStatus(PaymentStatus.Active).Handle(); + _fixture.VerifyLearningResumedEventIsNotPublished(); + _fixture.VerifyStoreLearningHistoryCommandIsNotSent(); + _fixture.VerifyLoggerLoggedInformation($"Apprenticeship {_fixture.apprenticeshipId} is already active and resumed."); + + } } public class LearningResumedEventHandlerTestsFixture @@ -269,6 +279,11 @@ public void VerifyLearningResumedEventIsNotPublished() resumedEvent.Should().BeNull(); } + public void VerifyStoreLearningHistoryCommandIsNotSent() + { + _mockContext.Verify(x => x.Send(It.IsAny(), It.IsAny()), Times.Never); + } + public void VerifyLearnerResumed() { var updatedApprenticeship = _dbContext.Apprenticeships.Find(apprenticeshipId); From 98f9918cd252c51aa79c3ef92c8e72c9ba480b90 Mon Sep 17 00:00:00 2001 From: skumardfe Date: Wed, 5 Aug 2026 12:56:17 +0100 Subject: [PATCH 3/3] Refactor code --- .../EventHandlers/LearningResumedEventHandler.cs | 4 ++-- .../LearningResumedEventHandlerTests.cs | 14 +++++++++++++- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/src/CommitmentsV2/SFA.DAS.CommitmentsV2.ExternalHandlers/EventHandlers/LearningResumedEventHandler.cs b/src/CommitmentsV2/SFA.DAS.CommitmentsV2.ExternalHandlers/EventHandlers/LearningResumedEventHandler.cs index 8959949083..73e16e00cb 100644 --- a/src/CommitmentsV2/SFA.DAS.CommitmentsV2.ExternalHandlers/EventHandlers/LearningResumedEventHandler.cs +++ b/src/CommitmentsV2/SFA.DAS.CommitmentsV2.ExternalHandlers/EventHandlers/LearningResumedEventHandler.cs @@ -27,7 +27,7 @@ public async Task Handle(LearningResumedEvent message, IMessageHandlerContext co if (message is null) { - logger.LogInformation(" {Event} received null message : {Event}", nameof(LearningResumedEvent), message == null); + logger.LogInformation("Event received null message : {Event}", nameof(LearningResumedEvent)); return; } @@ -70,7 +70,7 @@ public async Task Handle(LearningResumedEvent message, IMessageHandlerContext co } catch (Exception e) { - logger.LogError(e, "Error processing LearningResumedEventHandler for ApprenticeshipId {0}", message.ApprenticeshipId); + logger.LogError(e, "Error processing LearningResumedEventHandler for ApprenticeshipId {0}", message?.ApprenticeshipId); throw; } } diff --git a/src/CommitmentsV2/SFA.DAS.CommitmentsV2.ExternalMessageHandlers.UnitTests/EventHandlers/LearningResumedEventHandlerTests.cs b/src/CommitmentsV2/SFA.DAS.CommitmentsV2.ExternalMessageHandlers.UnitTests/EventHandlers/LearningResumedEventHandlerTests.cs index 372e06ef7f..5e565575e1 100644 --- a/src/CommitmentsV2/SFA.DAS.CommitmentsV2.ExternalMessageHandlers.UnitTests/EventHandlers/LearningResumedEventHandlerTests.cs +++ b/src/CommitmentsV2/SFA.DAS.CommitmentsV2.ExternalMessageHandlers.UnitTests/EventHandlers/LearningResumedEventHandlerTests.cs @@ -125,11 +125,17 @@ public async Task ThenThrowsDomainException_WhenStatusIsWithdrawn(PaymentStatus [Test] public async Task ThenLogstheInformation_WhenStatusIsActive() { - await _fixture.SetPaymentStatus(PaymentStatus.Active).Handle(); + await _fixture.SetPaymentStatus(PaymentStatus.Active).Handle(); _fixture.VerifyLearningResumedEventIsNotPublished(); _fixture.VerifyStoreLearningHistoryCommandIsNotSent(); _fixture.VerifyLoggerLoggedInformation($"Apprenticeship {_fixture.apprenticeshipId} is already active and resumed."); + } + [Test] + public async Task ThenLogstheInformation_WhenEventIsNull() + { + await _fixture.SetEventAsNull().Handle(); + _fixture.VerifyLoggerLoggedInformation($"Event received null message : {nameof(LearningResumedEvent)}"); } } @@ -246,6 +252,12 @@ public LearningResumedEventHandlerTestsFixture SetEventApprenticeshipId(long id) return this; } + public LearningResumedEventHandlerTestsFixture SetEventAsNull() + { + _event = null; + return this; + } + public async Task Handle() { await _handler.Handle(_event, _mockContext.Object);