From e183e428a780b3bc0f381952dd35d6598a2c4ee6 Mon Sep 17 00:00:00 2001 From: Paul Graham Date: Wed, 22 Jul 2026 10:35:53 +0100 Subject: [PATCH 01/13] WIP --- .../ApprenticeshipApprovalsController.cs | 19 +++++++ .../GetApprenticeshipApprovalQuery.cs | 14 +++++ .../GetApprenticeshipApprovalQueryHandler.cs | 53 +++++++++++++++++++ .../GetApprenticeshipApprovalQueryResult.cs | 30 +++++++++++ ...GetApprenticeshipApprovalQueryValidator.cs | 12 +++++ 5 files changed, 128 insertions(+) create mode 100644 src/CommitmentsV2/SFA.DAS.CommitmentsV2.Api/Controllers/ApprenticeshipApprovalsController.cs create mode 100644 src/CommitmentsV2/SFA.DAS.CommitmentsV2/Application/Queries/GetApprenticeshipApproval/GetApprenticeshipApprovalQuery.cs create mode 100644 src/CommitmentsV2/SFA.DAS.CommitmentsV2/Application/Queries/GetApprenticeshipApproval/GetApprenticeshipApprovalQueryHandler.cs create mode 100644 src/CommitmentsV2/SFA.DAS.CommitmentsV2/Application/Queries/GetApprenticeshipApproval/GetApprenticeshipApprovalQueryResult.cs create mode 100644 src/CommitmentsV2/SFA.DAS.CommitmentsV2/Application/Queries/GetApprenticeshipApproval/GetApprenticeshipApprovalQueryValidator.cs diff --git a/src/CommitmentsV2/SFA.DAS.CommitmentsV2.Api/Controllers/ApprenticeshipApprovalsController.cs b/src/CommitmentsV2/SFA.DAS.CommitmentsV2.Api/Controllers/ApprenticeshipApprovalsController.cs new file mode 100644 index 000000000..2a177782c --- /dev/null +++ b/src/CommitmentsV2/SFA.DAS.CommitmentsV2.Api/Controllers/ApprenticeshipApprovalsController.cs @@ -0,0 +1,19 @@ +using Microsoft.AspNetCore.Authorization; +using SFA.DAS.CommitmentsV2.Application.Queries.GetApprenticeshipApproval; +using SFA.DAS.CommitmentsV2.Shared.Interfaces; + +namespace SFA.DAS.CommitmentsV2.Api.Controllers; + +[ApiController] +[Authorize] +[Route("api/apprenticeships/{ApprenticeshipId:long}/approvals/{ApprovalRequestId:Guid}")] +public class ApprenticeshipApprovalsController(IMediator mediator, IModelMapper modelMapper) : ControllerBase +{ + [HttpGet] + public async Task GetApprenticeshipApproval(long apprenticeshipId, Guid approvalRequestId) + { + var result = await mediator.Send(new GetApprenticeshipApprovalQuery(apprenticeshipId, approvalRequestId)); + //var response = await modelMapper.Map(result); + return Ok(null); // response); + } +} \ No newline at end of file diff --git a/src/CommitmentsV2/SFA.DAS.CommitmentsV2/Application/Queries/GetApprenticeshipApproval/GetApprenticeshipApprovalQuery.cs b/src/CommitmentsV2/SFA.DAS.CommitmentsV2/Application/Queries/GetApprenticeshipApproval/GetApprenticeshipApprovalQuery.cs new file mode 100644 index 000000000..9f08d52db --- /dev/null +++ b/src/CommitmentsV2/SFA.DAS.CommitmentsV2/Application/Queries/GetApprenticeshipApproval/GetApprenticeshipApprovalQuery.cs @@ -0,0 +1,14 @@ +namespace SFA.DAS.CommitmentsV2.Application.Queries.GetApprenticeshipApproval; + +public class GetApprenticeshipApprovalQuery : IRequest +{ + public long ApprenticeshipId { get; } + + public Guid ApprovalRequestId { get; } + + public GetApprenticeshipApprovalQuery(long apprenticeshipId, Guid approvalRequestId) + { + ApprenticeshipId = apprenticeshipId; + ApprovalRequestId = approvalRequestId; + } +} \ No newline at end of file diff --git a/src/CommitmentsV2/SFA.DAS.CommitmentsV2/Application/Queries/GetApprenticeshipApproval/GetApprenticeshipApprovalQueryHandler.cs b/src/CommitmentsV2/SFA.DAS.CommitmentsV2/Application/Queries/GetApprenticeshipApproval/GetApprenticeshipApprovalQueryHandler.cs new file mode 100644 index 000000000..86c45c322 --- /dev/null +++ b/src/CommitmentsV2/SFA.DAS.CommitmentsV2/Application/Queries/GetApprenticeshipApproval/GetApprenticeshipApprovalQueryHandler.cs @@ -0,0 +1,53 @@ +using SFA.DAS.CommitmentsV2.Data; +using SFA.DAS.CommitmentsV2.Exceptions; +using SFA.DAS.CommitmentsV2.Models; + +namespace SFA.DAS.CommitmentsV2.Application.Queries.GetApprenticeshipApproval; + +public class GetApprenticeshipApprovalQueryHandler(Lazy dbContext) : IRequestHandler +{ + public async Task Handle(GetApprenticeshipApprovalQuery request, CancellationToken cancellationToken) + { + var approvalRequest = await dbContext.Value.ApprovalRequests + .Include(x => x.Items) + .FirstOrDefaultAsync(x => x.Id == request.ApprovalRequestId, cancellationToken); + + if (approvalRequest == null) + { + return null; + } + + if (approvalRequest.ApprenticeshipId != request.ApprenticeshipId) + { + throw new BadRequestException($"ApprenticeshipId {request.ApprenticeshipId} does not match on Approval Request record"); + } + + var apprenticeship = await dbContext.Value.Apprenticeships + .Include(x => x.Cohort).ThenInclude(x => x.Provider) + .Include(a => a.Cohort).ThenInclude(c => c.AccountLegalEntity) + .FirstOrDefaultAsync(x => x.Id == request.ApprenticeshipId, cancellationToken); + + + return new GetApprenticeshipApprovalQueryResult + { + ApprenticeshipId = apprenticeship.Id, + Name = $"{apprenticeship.FirstName} {apprenticeship.LastName}", + Uln = apprenticeship.Uln, + ProviderId = apprenticeship.Cohort.Provider.Id, + ProviderName = apprenticeship.Cohort.Provider.Name, + AccountLegalEntityId = apprenticeship.Cohort.AccountLegalEntity.Id, + AccountLegalEntityName = apprenticeship.Cohort.AccountLegalEntity.Name, + TrainingCode = apprenticeship.TrainingCode, + TrainingName = apprenticeship.TrainingName, + Cost = apprenticeship.Cost, + StartDate = apprenticeship.StartDate, + EndDate = apprenticeship.EndDate, + DeliveryModel = (DeliveryModel)apprenticeship.DeliveryModel, + OriginatorReference = apprenticeship.OriginatorReference, + ReservationId = apprenticeship.ReservationId, + EmployerReference = apprenticeship.EmployerReference + }; + } +} + + diff --git a/src/CommitmentsV2/SFA.DAS.CommitmentsV2/Application/Queries/GetApprenticeshipApproval/GetApprenticeshipApprovalQueryResult.cs b/src/CommitmentsV2/SFA.DAS.CommitmentsV2/Application/Queries/GetApprenticeshipApproval/GetApprenticeshipApprovalQueryResult.cs new file mode 100644 index 000000000..63c56ca72 --- /dev/null +++ b/src/CommitmentsV2/SFA.DAS.CommitmentsV2/Application/Queries/GetApprenticeshipApproval/GetApprenticeshipApprovalQueryResult.cs @@ -0,0 +1,30 @@ +using SFA.DAS.CommitmentsV2.Models; +using SFA.DAS.CommitmentsV2.Types; + +namespace SFA.DAS.CommitmentsV2.Application.Queries.GetApprenticeshipApproval; + +public class GetApprenticeshipApprovalQueryResult +{ + public long ApprenticeshipId { get; set; } + public Guid ApprovalRequestId { get; set; } + public CocApprovalResultStatus? Status { get; set; } + public virtual ICollection Items { get; set; } + public string Name { get; set; } + public string Uln { get; set; } + public string Email { get; set; } + public string TrainingName { get; set; } + public string TrainingName { get; set; } + public string ProviderName { get; set; } + public long UKPRN { get; set; } + public string AccountLegalEntityName { get; set; } + public long AccountLegalEntityId { get; set; } + + + public class ChangeItem + { + public string FieldName { get; set; } + public string OldValue { get; set; } + public string NewValue { get; set; } + public DateTime? EffectiveFromDate { get; set; } + } +} \ No newline at end of file diff --git a/src/CommitmentsV2/SFA.DAS.CommitmentsV2/Application/Queries/GetApprenticeshipApproval/GetApprenticeshipApprovalQueryValidator.cs b/src/CommitmentsV2/SFA.DAS.CommitmentsV2/Application/Queries/GetApprenticeshipApproval/GetApprenticeshipApprovalQueryValidator.cs new file mode 100644 index 000000000..f38b445c7 --- /dev/null +++ b/src/CommitmentsV2/SFA.DAS.CommitmentsV2/Application/Queries/GetApprenticeshipApproval/GetApprenticeshipApprovalQueryValidator.cs @@ -0,0 +1,12 @@ +using FluentValidation; + +namespace SFA.DAS.CommitmentsV2.Application.Queries.GetApprenticeshipApproval; + +public class GetApprenticeshipApprovalQueryValidator : AbstractValidator +{ + public GetApprenticeshipApprovalQueryValidator() + { + RuleFor(q => q.ApprenticeshipId).GreaterThan(0); + RuleFor(q => q.ApprovalRequestId).NotEmpty(); + } +} \ No newline at end of file From c0d18148d96e3255a82e0c8779e2b35b0825abf3 Mon Sep 17 00:00:00 2001 From: Paul Graham Date: Wed, 22 Jul 2026 12:21:48 +0100 Subject: [PATCH 02/13] WIP --- .../ApprenticeshipApprovalsController.cs | 12 ++++++--- .../GetApprenticeshipApprovalQueryHandler.cs | 26 ++++++++++--------- .../GetApprenticeshipApprovalQueryResult.cs | 9 +++---- 3 files changed, 25 insertions(+), 22 deletions(-) diff --git a/src/CommitmentsV2/SFA.DAS.CommitmentsV2.Api/Controllers/ApprenticeshipApprovalsController.cs b/src/CommitmentsV2/SFA.DAS.CommitmentsV2.Api/Controllers/ApprenticeshipApprovalsController.cs index 2a177782c..8e97b80d3 100644 --- a/src/CommitmentsV2/SFA.DAS.CommitmentsV2.Api/Controllers/ApprenticeshipApprovalsController.cs +++ b/src/CommitmentsV2/SFA.DAS.CommitmentsV2.Api/Controllers/ApprenticeshipApprovalsController.cs @@ -1,19 +1,23 @@ using Microsoft.AspNetCore.Authorization; using SFA.DAS.CommitmentsV2.Application.Queries.GetApprenticeshipApproval; -using SFA.DAS.CommitmentsV2.Shared.Interfaces; namespace SFA.DAS.CommitmentsV2.Api.Controllers; [ApiController] [Authorize] [Route("api/apprenticeships/{ApprenticeshipId:long}/approvals/{ApprovalRequestId:Guid}")] -public class ApprenticeshipApprovalsController(IMediator mediator, IModelMapper modelMapper) : ControllerBase +public class ApprenticeshipApprovalsController(IMediator mediator) : ControllerBase { [HttpGet] public async Task GetApprenticeshipApproval(long apprenticeshipId, Guid approvalRequestId) { var result = await mediator.Send(new GetApprenticeshipApprovalQuery(apprenticeshipId, approvalRequestId)); - //var response = await modelMapper.Map(result); - return Ok(null); // response); + + if(result == null) + { + return NotFound(); + } + + return Ok(result); } } \ No newline at end of file diff --git a/src/CommitmentsV2/SFA.DAS.CommitmentsV2/Application/Queries/GetApprenticeshipApproval/GetApprenticeshipApprovalQueryHandler.cs b/src/CommitmentsV2/SFA.DAS.CommitmentsV2/Application/Queries/GetApprenticeshipApproval/GetApprenticeshipApprovalQueryHandler.cs index 86c45c322..e9c5a3355 100644 --- a/src/CommitmentsV2/SFA.DAS.CommitmentsV2/Application/Queries/GetApprenticeshipApproval/GetApprenticeshipApprovalQueryHandler.cs +++ b/src/CommitmentsV2/SFA.DAS.CommitmentsV2/Application/Queries/GetApprenticeshipApproval/GetApprenticeshipApprovalQueryHandler.cs @@ -19,7 +19,8 @@ public async Task Handle(GetApprenticeship if (approvalRequest.ApprenticeshipId != request.ApprenticeshipId) { - throw new BadRequestException($"ApprenticeshipId {request.ApprenticeshipId} does not match on Approval Request record"); + //throw new BadRequestException($"ApprenticeshipId {request.ApprenticeshipId} does not match on Approval Request record"); + return null; } var apprenticeship = await dbContext.Value.Apprenticeships @@ -32,20 +33,21 @@ public async Task Handle(GetApprenticeship { ApprenticeshipId = apprenticeship.Id, Name = $"{apprenticeship.FirstName} {apprenticeship.LastName}", - Uln = apprenticeship.Uln, - ProviderId = apprenticeship.Cohort.Provider.Id, + ULN = apprenticeship.Uln, + UKPRN = apprenticeship.Cohort.Provider.UkPrn, ProviderName = apprenticeship.Cohort.Provider.Name, AccountLegalEntityId = apprenticeship.Cohort.AccountLegalEntity.Id, AccountLegalEntityName = apprenticeship.Cohort.AccountLegalEntity.Name, - TrainingCode = apprenticeship.TrainingCode, - TrainingName = apprenticeship.TrainingName, - Cost = apprenticeship.Cost, - StartDate = apprenticeship.StartDate, - EndDate = apprenticeship.EndDate, - DeliveryModel = (DeliveryModel)apprenticeship.DeliveryModel, - OriginatorReference = apprenticeship.OriginatorReference, - ReservationId = apprenticeship.ReservationId, - EmployerReference = apprenticeship.EmployerReference + CourseName = apprenticeship.CourseName, + ApprovalRequestStatus = approvalRequest.Status, + ApprovalRequestId = approvalRequest.Id, + Items = approvalRequest.Items.Select(i => new GetApprenticeshipApprovalQueryResult.ChangeItem + { + FieldName = i.Field, + OldValue = i.Old, + NewValue = i.New, + EffectiveFromDate = i.EffectiveFromDate + }).ToList() }; } } diff --git a/src/CommitmentsV2/SFA.DAS.CommitmentsV2/Application/Queries/GetApprenticeshipApproval/GetApprenticeshipApprovalQueryResult.cs b/src/CommitmentsV2/SFA.DAS.CommitmentsV2/Application/Queries/GetApprenticeshipApproval/GetApprenticeshipApprovalQueryResult.cs index 63c56ca72..c91bde5c5 100644 --- a/src/CommitmentsV2/SFA.DAS.CommitmentsV2/Application/Queries/GetApprenticeshipApproval/GetApprenticeshipApprovalQueryResult.cs +++ b/src/CommitmentsV2/SFA.DAS.CommitmentsV2/Application/Queries/GetApprenticeshipApproval/GetApprenticeshipApprovalQueryResult.cs @@ -1,5 +1,4 @@ using SFA.DAS.CommitmentsV2.Models; -using SFA.DAS.CommitmentsV2.Types; namespace SFA.DAS.CommitmentsV2.Application.Queries.GetApprenticeshipApproval; @@ -7,13 +6,11 @@ public class GetApprenticeshipApprovalQueryResult { public long ApprenticeshipId { get; set; } public Guid ApprovalRequestId { get; set; } - public CocApprovalResultStatus? Status { get; set; } + public CocApprovalResultStatus? ApprovalRequestStatus { get; set; } public virtual ICollection Items { get; set; } public string Name { get; set; } - public string Uln { get; set; } - public string Email { get; set; } - public string TrainingName { get; set; } - public string TrainingName { get; set; } + public string ULN { get; set; } + public string CourseName { get; set; } public string ProviderName { get; set; } public long UKPRN { get; set; } public string AccountLegalEntityName { get; set; } From e38e76ed7de577431b64587c3d3d4ba5954f38ff Mon Sep 17 00:00:00 2001 From: Paul Graham Date: Wed, 22 Jul 2026 14:09:41 +0100 Subject: [PATCH 03/13] update packages --- .../SFA.DAS.CommitmentsV2/SFA.DAS.CommitmentsV2.csproj | 4 ++-- .../SFA.DAS.ReservationsV2.Api.Client.UnitTests.csproj | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/CommitmentsV2/SFA.DAS.CommitmentsV2/SFA.DAS.CommitmentsV2.csproj b/src/CommitmentsV2/SFA.DAS.CommitmentsV2/SFA.DAS.CommitmentsV2.csproj index af5f69c46..103889a95 100644 --- a/src/CommitmentsV2/SFA.DAS.CommitmentsV2/SFA.DAS.CommitmentsV2.csproj +++ b/src/CommitmentsV2/SFA.DAS.CommitmentsV2/SFA.DAS.CommitmentsV2.csproj @@ -50,8 +50,8 @@ - - + + diff --git a/src/CommitmentsV2/SFA.DAS.ReservationsV2.Api.Client.UnitTests/SFA.DAS.ReservationsV2.Api.Client.UnitTests.csproj b/src/CommitmentsV2/SFA.DAS.ReservationsV2.Api.Client.UnitTests/SFA.DAS.ReservationsV2.Api.Client.UnitTests.csproj index 94cce9362..15cd42763 100644 --- a/src/CommitmentsV2/SFA.DAS.ReservationsV2.Api.Client.UnitTests/SFA.DAS.ReservationsV2.Api.Client.UnitTests.csproj +++ b/src/CommitmentsV2/SFA.DAS.ReservationsV2.Api.Client.UnitTests/SFA.DAS.ReservationsV2.Api.Client.UnitTests.csproj @@ -23,8 +23,8 @@ true - - + + From a2f7cc6a6df307507dc7c83963e866e21bd13b38 Mon Sep 17 00:00:00 2001 From: Paul Graham Date: Thu, 23 Jul 2026 09:03:50 +0100 Subject: [PATCH 04/13] added tests --- .../GetApprenticeshipApprovalHandlerTests.cs | 203 ++++++++++++++++++ ...etApprenticeshipApprovalValidationTests.cs | 34 +++ .../GetApprenticeshipUpdateValidationTests.cs | 45 ++-- .../GetApprenticeshipApprovalQueryHandler.cs | 8 +- .../SFA.DAS.CommitmentsV2.csproj | 5 - 5 files changed, 260 insertions(+), 35 deletions(-) create mode 100644 src/CommitmentsV2/SFA.DAS.CommitmentsV2.UnitTests/Application/Queries/GetApprenticeshipApproval/GetApprenticeshipApprovalHandlerTests.cs create mode 100644 src/CommitmentsV2/SFA.DAS.CommitmentsV2.UnitTests/Application/Queries/GetApprenticeshipApproval/GetApprenticeshipApprovalValidationTests.cs diff --git a/src/CommitmentsV2/SFA.DAS.CommitmentsV2.UnitTests/Application/Queries/GetApprenticeshipApproval/GetApprenticeshipApprovalHandlerTests.cs b/src/CommitmentsV2/SFA.DAS.CommitmentsV2.UnitTests/Application/Queries/GetApprenticeshipApproval/GetApprenticeshipApprovalHandlerTests.cs new file mode 100644 index 000000000..5f65e7a71 --- /dev/null +++ b/src/CommitmentsV2/SFA.DAS.CommitmentsV2.UnitTests/Application/Queries/GetApprenticeshipApproval/GetApprenticeshipApprovalHandlerTests.cs @@ -0,0 +1,203 @@ +using SFA.DAS.CommitmentsV2.Application.Queries.GetApprenticeshipApproval; +using SFA.DAS.CommitmentsV2.Data; +using SFA.DAS.CommitmentsV2.Models; +using SFA.DAS.CommitmentsV2.Types; + +namespace SFA.DAS.CommitmentsV2.UnitTests.Application.Queries.GetApprenticeshipApproval; + +[TestFixture] +public class GetApprenticeshipApprovalHandlerTests +{ + private GetApprenticeshipApprovalHandlerTestsFixture _fixture; + + [SetUp] + public void Arrange() + { + _fixture = new GetApprenticeshipApprovalHandlerTestsFixture(); + } + + [Test] + public async Task Handle_ThenShouldReturn_CoreValues() + { + var result = await _fixture.Handle(); + + result.Should().NotBeNull(); + result.ApprenticeshipId.Should().Be(_fixture.ApprenticeshipId); + result.ApprovalRequestId.Should().Be(_fixture.ApprovalRequestId); + result.AccountLegalEntityId.Should().Be(_fixture.AccountLegalEntityId); + result.AccountLegalEntityName.Should().Be(_fixture.AccountLegalEntity.Name); + result.Name.Should().Be($"{_fixture.Apprenticeship.FirstName} {_fixture.Apprenticeship.LastName}"); + result.ULN.Should().Be(_fixture.Apprenticeship.Uln); + result.CourseName.Should().Be(_fixture.Apprenticeship.CourseName); + result.ProviderName.Should().Be(_fixture.Provider.Name); + result.UKPRN.Should().Be(_fixture.Provider.UkPrn); + result.ApprovalRequestStatus.Should().Be(_fixture.ApprovalRequest.Status); + result.Items.Should().HaveCount(_fixture.ApprovalFieldRequests.Count); + } + + [Test] + public async Task Handle_ThenShouldReturn_MappedItems() + { + var result = await _fixture.Handle(); + + result.Should().NotBeNull(); + result.Items.Should().BeEquivalentTo(_fixture.ApprovalFieldRequests + .Select(x=> new GetApprenticeshipApprovalQueryResult.ChangeItem { FieldName = x.Field, NewValue = x.New, OldValue = x.Old, EffectiveFromDate = x.EffectiveFromDate } )); + } + + [Test] + public async Task WhenApprovalRequestNotFound_ThenShouldReturn_Null() + { + _fixture.Request = new GetApprenticeshipApprovalQuery(_fixture.ApprenticeshipId, Guid.NewGuid()); + var result = await _fixture.Handle(); + + result.Should().BeNull(); + } + + [Test] + public async Task WhenApprenticeshipIdDoesNotMatch_ThenShouldReturn_Null() + { + _fixture.Request = new GetApprenticeshipApprovalQuery(_fixture.ApprenticeshipId + 1, _fixture.ApprovalRequestId); + var result = await _fixture.Handle(); + + result.Should().BeNull(); + } + + public class GetApprenticeshipApprovalHandlerTestsFixture + { + public long ApprenticeshipId { get; private set; } + public Guid ApprovalRequestId { get; private set; } = Guid.NewGuid(); + public long AccountLegalEntityId { get; private set; } + public ApprovalRequest ApprovalRequest { get; private set; } + public List ApprovalFieldRequests { get; private set; } + public Apprenticeship Apprenticeship { get; private set; } + public Cohort Cohort { get; private set; } + public Provider Provider { get; private set; } + public AccountLegalEntity AccountLegalEntity { get; private set; } + public Course Course { get; private set; } + + public GetApprenticeshipApprovalQuery Request; + public GetApprenticeshipApprovalQueryResult Result; + + private readonly GetApprenticeshipApprovalQueryHandler _handler; + private readonly ProviderCommitmentsDbContext _db; + private Fixture _autoFixture; + + public GetApprenticeshipApprovalHandlerTestsFixture() + { + _db = new ProviderCommitmentsDbContext(new DbContextOptionsBuilder().UseInMemoryDatabase(Guid.NewGuid().ToString(), b => b.EnableNullChecks(false)).Options); + SeedData(); + Request = new GetApprenticeshipApprovalQuery(ApprenticeshipId, ApprovalRequestId); + + + _handler = new GetApprenticeshipApprovalQueryHandler(new Lazy(() => _db)); + } + + public async Task Handle() + { + Result = await _handler.Handle(Request, new CancellationToken()); + return Result; + } + + private GetApprenticeshipApprovalHandlerTestsFixture SeedData() + { + _autoFixture = new Fixture(); + _autoFixture.Behaviors.Add(new OmitOnRecursionBehavior()); + + ApprenticeshipId = _autoFixture.Create(); + + Provider = new Provider + { + UkPrn = _autoFixture.Create(), + Name = _autoFixture.Create() + }; + + var account = new Account(1, "", "", "", DateTime.UtcNow); + + AccountLegalEntity = new AccountLegalEntity(account, + AccountLegalEntityId, + 0, + "", + publicHashedId: _autoFixture.Create(), + _autoFixture.Create(), + OrganisationType.PublicBodies, + "", + DateTime.UtcNow); + + Cohort = new Cohort + { + Id = _autoFixture.CreateMany().Last(), + AccountLegalEntity = AccountLegalEntity, + EmployerAccountId = _autoFixture.Create(), + ProviderId = Provider.UkPrn, + Provider = Provider, + ApprenticeshipEmployerTypeOnApproval = ApprenticeshipEmployerType.Levy + }; + + var courseCode = _autoFixture.Create(); + + Apprenticeship = new Apprenticeship + { + Id = ApprenticeshipId, + CommitmentId = Cohort.Id, + Cohort = Cohort, + AgreedOn = _autoFixture.Create(), + CourseCode = courseCode, + StandardUId = "ST0001_1.0", + TrainingCourseVersion = "1.0", + CourseName = _autoFixture.Create(), + FirstName = _autoFixture.Create(), + LastName = _autoFixture.Create(), + DateOfBirth = _autoFixture.Create(), + StartDate = _autoFixture.Create(), + EndDate = _autoFixture.Create(), + Uln = _autoFixture.Create(), + PaymentStatus = _autoFixture.Create(), + EmployerRef = _autoFixture.Create(), + MadeRedundant = _autoFixture.Create(), + FlexibleEmployment = _autoFixture.Create(), + PriorLearning = _autoFixture.Create(), + TrainingTotalHours = _autoFixture.Create(), + EmployerHasEditedCost = _autoFixture.Create(), + StopDate = _autoFixture.Create(), + WithdrawnReasonCode = _autoFixture.Create(), + PaymentFreezeDate = DateTime.UtcNow.Date.AddDays(-7), + FreezePaymentsReason = FreezePaymentsReason.LearnerOnBreak, + }; + + switch (Apprenticeship.PaymentStatus) + { + case PaymentStatus.Withdrawn: + Apprenticeship.StopDate = _autoFixture.Create(); + break; + case PaymentStatus.Paused: + Apprenticeship.PauseDate = _autoFixture.Create(); + break; + case PaymentStatus.Completed: + Apprenticeship.CompletionDate = _autoFixture.Create(); + break; + } + + _db.Apprenticeships.Add(Apprenticeship); + + Course = _autoFixture.Build().With(c => c.LarsCode, courseCode).Create(); + _db.Courses.Add(Course); + + ApprovalFieldRequests = _autoFixture.Build() + .With(afr => afr.ApprovalRequestId, ApprovalRequestId) + .CreateMany(3).ToList(); + + ApprovalRequest = _autoFixture.Build() + .With(ar => ar.Id, ApprovalRequestId) + .With(ar => ar.ApprenticeshipId, ApprenticeshipId) + .With(ar => ar.Status, CocApprovalResultStatus.Pending) + .With(ar => ar.Items, ApprovalFieldRequests) + .Create(); + _db.ApprovalRequests.Add(ApprovalRequest); + + _db.SaveChanges(); + + return this; + } + } +} diff --git a/src/CommitmentsV2/SFA.DAS.CommitmentsV2.UnitTests/Application/Queries/GetApprenticeshipApproval/GetApprenticeshipApprovalValidationTests.cs b/src/CommitmentsV2/SFA.DAS.CommitmentsV2.UnitTests/Application/Queries/GetApprenticeshipApproval/GetApprenticeshipApprovalValidationTests.cs new file mode 100644 index 000000000..37f000637 --- /dev/null +++ b/src/CommitmentsV2/SFA.DAS.CommitmentsV2.UnitTests/Application/Queries/GetApprenticeshipApproval/GetApprenticeshipApprovalValidationTests.cs @@ -0,0 +1,34 @@ +using SFA.DAS.CommitmentsV2.Application.Queries.GetApprenticeshipApproval; + +namespace SFA.DAS.CommitmentsV2.UnitTests.Application.Queries.GetApprenticeshipApproval; + +[TestFixture] +public class GetApprenticeshipApprovalValidationTests +{ + [TestCase(-1, false)] + [TestCase(0, false)] + [TestCase(1, true)] + public void Validate_WithSpecifiedAppprenticeshipId(int apprenticeshipId, bool expectedIsValid) + { + var validator = new GetApprenticeshipApprovalQueryValidator(); + var validationResults = validator.Validate(new GetApprenticeshipApprovalQuery(apprenticeshipId, Guid.NewGuid())); + validationResults.IsValid.Should().Be(expectedIsValid); + } + + [Test] + public void Validate_WithNonEmptyApprovalRequestId_Then_Should_Pass() + { + var validator = new GetApprenticeshipApprovalQueryValidator(); + var validationResults = validator.Validate(new GetApprenticeshipApprovalQuery(111, Guid.NewGuid())); + validationResults.IsValid.Should().Be(true); + } + + [Test] + public void Validate_WithEmptyApprovalRequestId_Then_Should_Fail() + { + var validator = new GetApprenticeshipApprovalQueryValidator(); + var validationResults = validator.Validate(new GetApprenticeshipApprovalQuery(111, Guid.Empty)); + validationResults.IsValid.Should().Be(false); + } + +} \ No newline at end of file diff --git a/src/CommitmentsV2/SFA.DAS.CommitmentsV2.UnitTests/Application/Queries/GetApprenticeshipUpdate/GetApprenticeshipUpdateValidationTests.cs b/src/CommitmentsV2/SFA.DAS.CommitmentsV2.UnitTests/Application/Queries/GetApprenticeshipUpdate/GetApprenticeshipUpdateValidationTests.cs index 72a4236e4..dfd352c59 100644 --- a/src/CommitmentsV2/SFA.DAS.CommitmentsV2.UnitTests/Application/Queries/GetApprenticeshipUpdate/GetApprenticeshipUpdateValidationTests.cs +++ b/src/CommitmentsV2/SFA.DAS.CommitmentsV2.UnitTests/Application/Queries/GetApprenticeshipUpdate/GetApprenticeshipUpdateValidationTests.cs @@ -1,30 +1,29 @@ using SFA.DAS.CommitmentsV2.Application.Queries.GetApprenticeshipUpdate; using SFA.DAS.CommitmentsV2.Types; -namespace SFA.DAS.CommitmentsV2.UnitTests.Application.Queries.GetApprenticeshipUpdate +namespace SFA.DAS.CommitmentsV2.UnitTests.Application.Queries.GetApprenticeshipUpdate; + +[TestFixture] +public class GetApprenticeshipUpdateValidationTests { - [TestFixture] - public class GetApprenticeshipUpdateValidationTests + [TestCase(-1, false)] + [TestCase(0, false)] + [TestCase(1, true)] + public void Validate_WithSpecifiedAppprenticeshipId(int apprenticeshipId, bool expectedIsValid) { - [TestCase(-1, false)] - [TestCase(0, false)] - [TestCase(1, true)] - public void Validate_WithSpecifiedAppprenticeshipId(int apprenticeshipId, bool expectedIsValid) - { - var validator = new GetApprenticeshipUpdateQueryValidator(); - var validationResults = validator.Validate(new GetApprenticeshipUpdateQuery(apprenticeshipId, null)); - Assert.That(validationResults.IsValid, Is.EqualTo(expectedIsValid)); - } + var validator = new GetApprenticeshipUpdateQueryValidator(); + var validationResults = validator.Validate(new GetApprenticeshipUpdateQuery(apprenticeshipId, null)); + Assert.That(validationResults.IsValid, Is.EqualTo(expectedIsValid)); + } - [TestCase(null, true)] - [TestCase(ApprenticeshipUpdateStatus.Approved, true)] - [TestCase(ApprenticeshipUpdateStatus.Deleted, true)] - [TestCase(ApprenticeshipUpdateStatus.Superceded, true)] - public void DontValidate_ApprenticeshipUpdateStatus(ApprenticeshipUpdateStatus? status, bool expectedIsValid) - { - var validator = new GetApprenticeshipUpdateQueryValidator(); - var validationResults = validator.Validate(new GetApprenticeshipUpdateQuery(1, status)); - Assert.That(validationResults.IsValid, Is.EqualTo(expectedIsValid)); - } + [TestCase(null, true)] + [TestCase(ApprenticeshipUpdateStatus.Approved, true)] + [TestCase(ApprenticeshipUpdateStatus.Deleted, true)] + [TestCase(ApprenticeshipUpdateStatus.Superceded, true)] + public void DontValidate_ApprenticeshipUpdateStatus(ApprenticeshipUpdateStatus? status, bool expectedIsValid) + { + var validator = new GetApprenticeshipUpdateQueryValidator(); + var validationResults = validator.Validate(new GetApprenticeshipUpdateQuery(1, status)); + Assert.That(validationResults.IsValid, Is.EqualTo(expectedIsValid)); } -} +} \ No newline at end of file diff --git a/src/CommitmentsV2/SFA.DAS.CommitmentsV2/Application/Queries/GetApprenticeshipApproval/GetApprenticeshipApprovalQueryHandler.cs b/src/CommitmentsV2/SFA.DAS.CommitmentsV2/Application/Queries/GetApprenticeshipApproval/GetApprenticeshipApprovalQueryHandler.cs index e9c5a3355..f378c052f 100644 --- a/src/CommitmentsV2/SFA.DAS.CommitmentsV2/Application/Queries/GetApprenticeshipApproval/GetApprenticeshipApprovalQueryHandler.cs +++ b/src/CommitmentsV2/SFA.DAS.CommitmentsV2/Application/Queries/GetApprenticeshipApproval/GetApprenticeshipApprovalQueryHandler.cs @@ -1,6 +1,4 @@ using SFA.DAS.CommitmentsV2.Data; -using SFA.DAS.CommitmentsV2.Exceptions; -using SFA.DAS.CommitmentsV2.Models; namespace SFA.DAS.CommitmentsV2.Application.Queries.GetApprenticeshipApproval; @@ -19,7 +17,6 @@ public async Task Handle(GetApprenticeship if (approvalRequest.ApprenticeshipId != request.ApprenticeshipId) { - //throw new BadRequestException($"ApprenticeshipId {request.ApprenticeshipId} does not match on Approval Request record"); return null; } @@ -28,7 +25,6 @@ public async Task Handle(GetApprenticeship .Include(a => a.Cohort).ThenInclude(c => c.AccountLegalEntity) .FirstOrDefaultAsync(x => x.Id == request.ApprenticeshipId, cancellationToken); - return new GetApprenticeshipApprovalQueryResult { ApprenticeshipId = apprenticeship.Id, @@ -50,6 +46,4 @@ public async Task Handle(GetApprenticeship }).ToList() }; } -} - - +} \ No newline at end of file diff --git a/src/CommitmentsV2/SFA.DAS.CommitmentsV2/SFA.DAS.CommitmentsV2.csproj b/src/CommitmentsV2/SFA.DAS.CommitmentsV2/SFA.DAS.CommitmentsV2.csproj index 103889a95..2d377e4fb 100644 --- a/src/CommitmentsV2/SFA.DAS.CommitmentsV2/SFA.DAS.CommitmentsV2.csproj +++ b/src/CommitmentsV2/SFA.DAS.CommitmentsV2/SFA.DAS.CommitmentsV2.csproj @@ -65,10 +65,5 @@ - - - - - From ae5d3ce2e165f7a59e616fe70c0b6298f10aade1 Mon Sep 17 00:00:00 2001 From: Paul Graham Date: Thu, 23 Jul 2026 09:06:42 +0100 Subject: [PATCH 05/13] tidy --- .../GetApprenticeshipApprovalQueryResult.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/CommitmentsV2/SFA.DAS.CommitmentsV2/Application/Queries/GetApprenticeshipApproval/GetApprenticeshipApprovalQueryResult.cs b/src/CommitmentsV2/SFA.DAS.CommitmentsV2/Application/Queries/GetApprenticeshipApproval/GetApprenticeshipApprovalQueryResult.cs index c91bde5c5..db799b363 100644 --- a/src/CommitmentsV2/SFA.DAS.CommitmentsV2/Application/Queries/GetApprenticeshipApproval/GetApprenticeshipApprovalQueryResult.cs +++ b/src/CommitmentsV2/SFA.DAS.CommitmentsV2/Application/Queries/GetApprenticeshipApproval/GetApprenticeshipApprovalQueryResult.cs @@ -16,7 +16,6 @@ public class GetApprenticeshipApprovalQueryResult public string AccountLegalEntityName { get; set; } public long AccountLegalEntityId { get; set; } - public class ChangeItem { public string FieldName { get; set; } From 53290c94d284a651012e3d348ee6cd659582c3a7 Mon Sep 17 00:00:00 2001 From: Paul Graham Date: Fri, 24 Jul 2026 11:23:51 +0100 Subject: [PATCH 06/13] added accountId --- .../GetApprenticeshipApprovalHandlerTests.cs | 1 + .../GetApprenticeshipApprovalQueryHandler.cs | 1 + .../GetApprenticeshipApprovalQueryResult.cs | 1 + 3 files changed, 3 insertions(+) diff --git a/src/CommitmentsV2/SFA.DAS.CommitmentsV2.UnitTests/Application/Queries/GetApprenticeshipApproval/GetApprenticeshipApprovalHandlerTests.cs b/src/CommitmentsV2/SFA.DAS.CommitmentsV2.UnitTests/Application/Queries/GetApprenticeshipApproval/GetApprenticeshipApprovalHandlerTests.cs index 5f65e7a71..0604f7211 100644 --- a/src/CommitmentsV2/SFA.DAS.CommitmentsV2.UnitTests/Application/Queries/GetApprenticeshipApproval/GetApprenticeshipApprovalHandlerTests.cs +++ b/src/CommitmentsV2/SFA.DAS.CommitmentsV2.UnitTests/Application/Queries/GetApprenticeshipApproval/GetApprenticeshipApprovalHandlerTests.cs @@ -26,6 +26,7 @@ public async Task Handle_ThenShouldReturn_CoreValues() result.ApprovalRequestId.Should().Be(_fixture.ApprovalRequestId); result.AccountLegalEntityId.Should().Be(_fixture.AccountLegalEntityId); result.AccountLegalEntityName.Should().Be(_fixture.AccountLegalEntity.Name); + result.AccountId.Should().Be(_fixture.AccountLegalEntity.AccountId); result.Name.Should().Be($"{_fixture.Apprenticeship.FirstName} {_fixture.Apprenticeship.LastName}"); result.ULN.Should().Be(_fixture.Apprenticeship.Uln); result.CourseName.Should().Be(_fixture.Apprenticeship.CourseName); diff --git a/src/CommitmentsV2/SFA.DAS.CommitmentsV2/Application/Queries/GetApprenticeshipApproval/GetApprenticeshipApprovalQueryHandler.cs b/src/CommitmentsV2/SFA.DAS.CommitmentsV2/Application/Queries/GetApprenticeshipApproval/GetApprenticeshipApprovalQueryHandler.cs index f378c052f..638cd986a 100644 --- a/src/CommitmentsV2/SFA.DAS.CommitmentsV2/Application/Queries/GetApprenticeshipApproval/GetApprenticeshipApprovalQueryHandler.cs +++ b/src/CommitmentsV2/SFA.DAS.CommitmentsV2/Application/Queries/GetApprenticeshipApproval/GetApprenticeshipApprovalQueryHandler.cs @@ -34,6 +34,7 @@ public async Task Handle(GetApprenticeship ProviderName = apprenticeship.Cohort.Provider.Name, AccountLegalEntityId = apprenticeship.Cohort.AccountLegalEntity.Id, AccountLegalEntityName = apprenticeship.Cohort.AccountLegalEntity.Name, + AccountId = apprenticeship.Cohort.AccountLegalEntity.AccountId, CourseName = apprenticeship.CourseName, ApprovalRequestStatus = approvalRequest.Status, ApprovalRequestId = approvalRequest.Id, diff --git a/src/CommitmentsV2/SFA.DAS.CommitmentsV2/Application/Queries/GetApprenticeshipApproval/GetApprenticeshipApprovalQueryResult.cs b/src/CommitmentsV2/SFA.DAS.CommitmentsV2/Application/Queries/GetApprenticeshipApproval/GetApprenticeshipApprovalQueryResult.cs index db799b363..ea059e909 100644 --- a/src/CommitmentsV2/SFA.DAS.CommitmentsV2/Application/Queries/GetApprenticeshipApproval/GetApprenticeshipApprovalQueryResult.cs +++ b/src/CommitmentsV2/SFA.DAS.CommitmentsV2/Application/Queries/GetApprenticeshipApproval/GetApprenticeshipApprovalQueryResult.cs @@ -15,6 +15,7 @@ public class GetApprenticeshipApprovalQueryResult public long UKPRN { get; set; } public string AccountLegalEntityName { get; set; } public long AccountLegalEntityId { get; set; } + public long AccountId { get; set; } public class ChangeItem { From 2ce708552cecf4020dcc1c806ebd6a827a49071e Mon Sep 17 00:00:00 2001 From: Paul Graham Date: Wed, 29 Jul 2026 08:25:12 +0100 Subject: [PATCH 07/13] added course code --- .../GetApprenticeshipApprovalHandlerTests.cs | 1 + .../GetApprenticeshipApprovalQueryHandler.cs | 1 + .../GetApprenticeshipApprovalQueryResult.cs | 1 + 3 files changed, 3 insertions(+) diff --git a/src/CommitmentsV2/SFA.DAS.CommitmentsV2.UnitTests/Application/Queries/GetApprenticeshipApproval/GetApprenticeshipApprovalHandlerTests.cs b/src/CommitmentsV2/SFA.DAS.CommitmentsV2.UnitTests/Application/Queries/GetApprenticeshipApproval/GetApprenticeshipApprovalHandlerTests.cs index 0604f7211..20ad5512f 100644 --- a/src/CommitmentsV2/SFA.DAS.CommitmentsV2.UnitTests/Application/Queries/GetApprenticeshipApproval/GetApprenticeshipApprovalHandlerTests.cs +++ b/src/CommitmentsV2/SFA.DAS.CommitmentsV2.UnitTests/Application/Queries/GetApprenticeshipApproval/GetApprenticeshipApprovalHandlerTests.cs @@ -29,6 +29,7 @@ public async Task Handle_ThenShouldReturn_CoreValues() result.AccountId.Should().Be(_fixture.AccountLegalEntity.AccountId); result.Name.Should().Be($"{_fixture.Apprenticeship.FirstName} {_fixture.Apprenticeship.LastName}"); result.ULN.Should().Be(_fixture.Apprenticeship.Uln); + result.CourseCode.Should().Be(_fixture.Apprenticeship.CourseCode); result.CourseName.Should().Be(_fixture.Apprenticeship.CourseName); result.ProviderName.Should().Be(_fixture.Provider.Name); result.UKPRN.Should().Be(_fixture.Provider.UkPrn); diff --git a/src/CommitmentsV2/SFA.DAS.CommitmentsV2/Application/Queries/GetApprenticeshipApproval/GetApprenticeshipApprovalQueryHandler.cs b/src/CommitmentsV2/SFA.DAS.CommitmentsV2/Application/Queries/GetApprenticeshipApproval/GetApprenticeshipApprovalQueryHandler.cs index 638cd986a..a049c77c6 100644 --- a/src/CommitmentsV2/SFA.DAS.CommitmentsV2/Application/Queries/GetApprenticeshipApproval/GetApprenticeshipApprovalQueryHandler.cs +++ b/src/CommitmentsV2/SFA.DAS.CommitmentsV2/Application/Queries/GetApprenticeshipApproval/GetApprenticeshipApprovalQueryHandler.cs @@ -35,6 +35,7 @@ public async Task Handle(GetApprenticeship AccountLegalEntityId = apprenticeship.Cohort.AccountLegalEntity.Id, AccountLegalEntityName = apprenticeship.Cohort.AccountLegalEntity.Name, AccountId = apprenticeship.Cohort.AccountLegalEntity.AccountId, + CourseCode = apprenticeship.CourseCode, CourseName = apprenticeship.CourseName, ApprovalRequestStatus = approvalRequest.Status, ApprovalRequestId = approvalRequest.Id, diff --git a/src/CommitmentsV2/SFA.DAS.CommitmentsV2/Application/Queries/GetApprenticeshipApproval/GetApprenticeshipApprovalQueryResult.cs b/src/CommitmentsV2/SFA.DAS.CommitmentsV2/Application/Queries/GetApprenticeshipApproval/GetApprenticeshipApprovalQueryResult.cs index ea059e909..22288e5e5 100644 --- a/src/CommitmentsV2/SFA.DAS.CommitmentsV2/Application/Queries/GetApprenticeshipApproval/GetApprenticeshipApprovalQueryResult.cs +++ b/src/CommitmentsV2/SFA.DAS.CommitmentsV2/Application/Queries/GetApprenticeshipApproval/GetApprenticeshipApprovalQueryResult.cs @@ -10,6 +10,7 @@ public class GetApprenticeshipApprovalQueryResult public virtual ICollection Items { get; set; } public string Name { get; set; } public string ULN { get; set; } + public string CourseCode { get; set; } public string CourseName { get; set; } public string ProviderName { get; set; } public long UKPRN { get; set; } From 4ba8ae5d0e38074614d37a8df261d786826d3842 Mon Sep 17 00:00:00 2001 From: Paul Graham Date: Wed, 29 Jul 2026 11:05:38 +0100 Subject: [PATCH 08/13] added StartDate --- .../GetApprenticeshipApprovalHandlerTests.cs | 12 ++++++++++++ .../GetApprenticeshipApprovalQueryHandler.cs | 1 + .../GetApprenticeshipApprovalQueryResult.cs | 1 + 3 files changed, 14 insertions(+) diff --git a/src/CommitmentsV2/SFA.DAS.CommitmentsV2.UnitTests/Application/Queries/GetApprenticeshipApproval/GetApprenticeshipApprovalHandlerTests.cs b/src/CommitmentsV2/SFA.DAS.CommitmentsV2.UnitTests/Application/Queries/GetApprenticeshipApproval/GetApprenticeshipApprovalHandlerTests.cs index 20ad5512f..1fdfd7e57 100644 --- a/src/CommitmentsV2/SFA.DAS.CommitmentsV2.UnitTests/Application/Queries/GetApprenticeshipApproval/GetApprenticeshipApprovalHandlerTests.cs +++ b/src/CommitmentsV2/SFA.DAS.CommitmentsV2.UnitTests/Application/Queries/GetApprenticeshipApproval/GetApprenticeshipApprovalHandlerTests.cs @@ -29,6 +29,7 @@ public async Task Handle_ThenShouldReturn_CoreValues() result.AccountId.Should().Be(_fixture.AccountLegalEntity.AccountId); result.Name.Should().Be($"{_fixture.Apprenticeship.FirstName} {_fixture.Apprenticeship.LastName}"); result.ULN.Should().Be(_fixture.Apprenticeship.Uln); + result.StartDate.Should().Be(_fixture.Apprenticeship.StartDate); result.CourseCode.Should().Be(_fixture.Apprenticeship.CourseCode); result.CourseName.Should().Be(_fixture.Apprenticeship.CourseName); result.ProviderName.Should().Be(_fixture.Provider.Name); @@ -37,6 +38,17 @@ public async Task Handle_ThenShouldReturn_CoreValues() result.Items.Should().HaveCount(_fixture.ApprovalFieldRequests.Count); } + [Test] + public async Task Handle_ForContinuationApprenticeship_ThenShouldReturn_OriginalStartDate() + { + _fixture.Apprenticeship.ContinuationOfId = 123; + _fixture.Apprenticeship.OriginalStartDate = DateTime.Now.AddYears(-2); + var result = await _fixture.Handle(); + + result.Should().NotBeNull(); + result.StartDate.Should().Be(_fixture.Apprenticeship.OriginalStartDate); + } + [Test] public async Task Handle_ThenShouldReturn_MappedItems() { diff --git a/src/CommitmentsV2/SFA.DAS.CommitmentsV2/Application/Queries/GetApprenticeshipApproval/GetApprenticeshipApprovalQueryHandler.cs b/src/CommitmentsV2/SFA.DAS.CommitmentsV2/Application/Queries/GetApprenticeshipApproval/GetApprenticeshipApprovalQueryHandler.cs index a049c77c6..fca5fe30d 100644 --- a/src/CommitmentsV2/SFA.DAS.CommitmentsV2/Application/Queries/GetApprenticeshipApproval/GetApprenticeshipApprovalQueryHandler.cs +++ b/src/CommitmentsV2/SFA.DAS.CommitmentsV2/Application/Queries/GetApprenticeshipApproval/GetApprenticeshipApprovalQueryHandler.cs @@ -35,6 +35,7 @@ public async Task Handle(GetApprenticeship AccountLegalEntityId = apprenticeship.Cohort.AccountLegalEntity.Id, AccountLegalEntityName = apprenticeship.Cohort.AccountLegalEntity.Name, AccountId = apprenticeship.Cohort.AccountLegalEntity.AccountId, + StartDate = apprenticeship.OriginalStartDate ?? apprenticeship.StartDate, CourseCode = apprenticeship.CourseCode, CourseName = apprenticeship.CourseName, ApprovalRequestStatus = approvalRequest.Status, diff --git a/src/CommitmentsV2/SFA.DAS.CommitmentsV2/Application/Queries/GetApprenticeshipApproval/GetApprenticeshipApprovalQueryResult.cs b/src/CommitmentsV2/SFA.DAS.CommitmentsV2/Application/Queries/GetApprenticeshipApproval/GetApprenticeshipApprovalQueryResult.cs index 22288e5e5..17cd66ac9 100644 --- a/src/CommitmentsV2/SFA.DAS.CommitmentsV2/Application/Queries/GetApprenticeshipApproval/GetApprenticeshipApprovalQueryResult.cs +++ b/src/CommitmentsV2/SFA.DAS.CommitmentsV2/Application/Queries/GetApprenticeshipApproval/GetApprenticeshipApprovalQueryResult.cs @@ -10,6 +10,7 @@ public class GetApprenticeshipApprovalQueryResult public virtual ICollection Items { get; set; } public string Name { get; set; } public string ULN { get; set; } + public DateTime? StartDate { get; set; } public string CourseCode { get; set; } public string CourseName { get; set; } public string ProviderName { get; set; } From b874358c6580b4872ab6729055d23c8c7290cf75 Mon Sep 17 00:00:00 2001 From: Paul Graham Date: Fri, 31 Jul 2026 17:34:47 +0100 Subject: [PATCH 09/13] wip --- .../ProcessApprenticeshipApprovalRequest.cs | 6 ++ .../ApprenticeshipApprovalsController.cs | 18 +++++ .../ProcessApprenticeshipApprovalCommand.cs | 12 +++ ...essApprenticeshipApprovalCommandHandler.cs | 73 +++++++++++++++++++ 4 files changed, 109 insertions(+) create mode 100644 src/CommitmentsV2/SFA.DAS.CommitmentsV2.Api.Types/Requests/ProcessApprenticeshipApprovalRequest.cs create mode 100644 src/CommitmentsV2/SFA.DAS.CommitmentsV2/Application/Commands/ProcessApprenticeshipApproval/ProcessApprenticeshipApprovalCommand.cs create mode 100644 src/CommitmentsV2/SFA.DAS.CommitmentsV2/Application/Commands/ProcessApprenticeshipApproval/ProcessApprenticeshipApprovalCommandHandler.cs diff --git a/src/CommitmentsV2/SFA.DAS.CommitmentsV2.Api.Types/Requests/ProcessApprenticeshipApprovalRequest.cs b/src/CommitmentsV2/SFA.DAS.CommitmentsV2.Api.Types/Requests/ProcessApprenticeshipApprovalRequest.cs new file mode 100644 index 000000000..fb5d418bd --- /dev/null +++ b/src/CommitmentsV2/SFA.DAS.CommitmentsV2.Api.Types/Requests/ProcessApprenticeshipApprovalRequest.cs @@ -0,0 +1,6 @@ +namespace SFA.DAS.CommitmentsV2.Api.Types.Requests; + +public class ProcessApprenticeshipApprovalRequest : SaveDataRequest +{ + public bool ApplyChanges { get; set; } +} diff --git a/src/CommitmentsV2/SFA.DAS.CommitmentsV2.Api/Controllers/ApprenticeshipApprovalsController.cs b/src/CommitmentsV2/SFA.DAS.CommitmentsV2.Api/Controllers/ApprenticeshipApprovalsController.cs index 8e97b80d3..d8260cdba 100644 --- a/src/CommitmentsV2/SFA.DAS.CommitmentsV2.Api/Controllers/ApprenticeshipApprovalsController.cs +++ b/src/CommitmentsV2/SFA.DAS.CommitmentsV2.Api/Controllers/ApprenticeshipApprovalsController.cs @@ -1,4 +1,6 @@ using Microsoft.AspNetCore.Authorization; +using SFA.DAS.CommitmentsV2.Api.Types.Requests; +using SFA.DAS.CommitmentsV2.Application.Commands.ProcessApprenticeshipApproval; using SFA.DAS.CommitmentsV2.Application.Queries.GetApprenticeshipApproval; namespace SFA.DAS.CommitmentsV2.Api.Controllers; @@ -20,4 +22,20 @@ public async Task GetApprenticeshipApproval(long apprenticeshipId return Ok(result); } + + [HttpPost] + public async Task PostApprenticeshipApproval(long apprenticeshipId, Guid approvalRequestId, [FromBody] ProcessApprenticeshipApprovalRequest request) + { + await mediator.Send(new ProcessApprenticeshipApprovalCommand + { + ApprenticeshipId = apprenticeshipId, + ApprovalRequestId = approvalRequestId, + ApplyChanges = request.ApplyChanges, + UserInfo = request.UserInfo + }); + + return Ok(); + } + + } \ No newline at end of file diff --git a/src/CommitmentsV2/SFA.DAS.CommitmentsV2/Application/Commands/ProcessApprenticeshipApproval/ProcessApprenticeshipApprovalCommand.cs b/src/CommitmentsV2/SFA.DAS.CommitmentsV2/Application/Commands/ProcessApprenticeshipApproval/ProcessApprenticeshipApprovalCommand.cs new file mode 100644 index 000000000..cfa05f147 --- /dev/null +++ b/src/CommitmentsV2/SFA.DAS.CommitmentsV2/Application/Commands/ProcessApprenticeshipApproval/ProcessApprenticeshipApprovalCommand.cs @@ -0,0 +1,12 @@ +using SFA.DAS.CommitmentsV2.Types; + +namespace SFA.DAS.CommitmentsV2.Application.Commands.ProcessApprenticeshipApproval; + +public class ProcessApprenticeshipApprovalCommand : IRequest +{ + public long ApprenticeshipId { get; set; } + public Guid ApprovalRequestId { get; set; } + public bool ApplyChanges { get; set; } + + public UserInfo UserInfo { get; set; } +} \ No newline at end of file diff --git a/src/CommitmentsV2/SFA.DAS.CommitmentsV2/Application/Commands/ProcessApprenticeshipApproval/ProcessApprenticeshipApprovalCommandHandler.cs b/src/CommitmentsV2/SFA.DAS.CommitmentsV2/Application/Commands/ProcessApprenticeshipApproval/ProcessApprenticeshipApprovalCommandHandler.cs new file mode 100644 index 000000000..0b9e864bc --- /dev/null +++ b/src/CommitmentsV2/SFA.DAS.CommitmentsV2/Application/Commands/ProcessApprenticeshipApproval/ProcessApprenticeshipApprovalCommandHandler.cs @@ -0,0 +1,73 @@ +using System.Runtime.Loader; +using System.Security.Cryptography; +using System.Threading.Channels; +using Newtonsoft.Json; +using SFA.DAS.CommitmentsV2.Data; +using SFA.DAS.CommitmentsV2.Models; + +namespace SFA.DAS.CommitmentsV2.Application.Commands.ProcessApprenticeshipApproval; + +public class ProcessApprenticeshipApprovalCommandHandler( + Lazy dbContext) + : IRequestHandler +{ + public async Task Handle(ProcessApprenticeshipApprovalCommand command, CancellationToken cancellationToken) + { + var db = dbContext.Value; + + var approval = await db.ApprovalRequests.Include(x => x.Items).FirstOrDefaultAsync(x => x.Id == command.ApprovalRequestId, cancellationToken); + + if (approval == null) + { + throw new Exception($"Approval request {command.ApprovalRequestId} not found "); + } + if (approval.ApprenticeshipId != command.ApprenticeshipId) + { + throw new Exception($"Approval request {command.ApprovalRequestId} not found for apprenticeship {command.ApprenticeshipId}"); + } + if (approval.Status != CocApprovalResultStatus.Pending) + { + throw new Exception($"Approval request {command.ApprovalRequestId} is no longer pending. It#s status is {approval.Status}"); + } + + if (command.ApplyChanges) + { + approval.Status = CocApprovalResultStatus.Complete; + + } + else + { + approval.Status = CocApprovalResultStatus.Cancelled; + } + + await db.SaveChangesAsync(cancellationToken); + + + + } + + private void SendAppliedMessage(ApprovalRequest approvalRequest) + { + + var jsonObject = new + { + approvalRequest.LearningKey, + approvalRequest.ApprenticeshipId, + + Changes = approvalRequest.Items.ToDictionary( + x => x.Field, + x => new + { + x.Old, + x.New, + x.EffectiveFromDate + }) + + }; + var json = JsonConvert.SerializeObject(jsonObject, Formatting.Indented); + + + + + } +} \ No newline at end of file From d5490bea17260c0eb65c2229953c64a82a59cf07 Mon Sep 17 00:00:00 2001 From: Paul Graham Date: Mon, 3 Aug 2026 08:41:28 +0100 Subject: [PATCH 10/13] added controller unit tests --- ...henGettingApprenticeshipApprovalRequest.cs | 44 +++++++++++++++++++ ...ProcessingApprenticeshipApprovalRequest.cs | 32 ++++++++++++++ .../ApprenticeshipApprovalsController.cs | 2 - 3 files changed, 76 insertions(+), 2 deletions(-) create mode 100644 src/CommitmentsV2/SFA.DAS.CommitmentsV2.Api.UnitTests/Controllers/ApprenticeshipApprovalControllerTests/WhenGettingApprenticeshipApprovalRequest.cs create mode 100644 src/CommitmentsV2/SFA.DAS.CommitmentsV2.Api.UnitTests/Controllers/ApprenticeshipApprovalControllerTests/WhenProcessingApprenticeshipApprovalRequest.cs diff --git a/src/CommitmentsV2/SFA.DAS.CommitmentsV2.Api.UnitTests/Controllers/ApprenticeshipApprovalControllerTests/WhenGettingApprenticeshipApprovalRequest.cs b/src/CommitmentsV2/SFA.DAS.CommitmentsV2.Api.UnitTests/Controllers/ApprenticeshipApprovalControllerTests/WhenGettingApprenticeshipApprovalRequest.cs new file mode 100644 index 000000000..91224bbaf --- /dev/null +++ b/src/CommitmentsV2/SFA.DAS.CommitmentsV2.Api.UnitTests/Controllers/ApprenticeshipApprovalControllerTests/WhenGettingApprenticeshipApprovalRequest.cs @@ -0,0 +1,44 @@ +using AutoFixture.NUnit3; +using Microsoft.AspNetCore.Mvc; +using SFA.DAS.CommitmentsV2.Api.Controllers; +using SFA.DAS.CommitmentsV2.Application.Queries.GetApprenticeshipApproval; +using SFA.DAS.Testing.AutoFixture; + +namespace SFA.DAS.CommitmentsV2.Api.UnitTests.Controllers.ApprenticeshipApprovalControllerTests +{ + public class WhenGettingApprenticeshipApprovalRequest + { + [Test, MoqAutoData] + public async Task Then_The_Request_Is_Passed_To_Mediator_And_Data_Returned( + GetApprenticeshipApprovalQueryResult result, + long apprenticeshipId, + Guid ApprovalRequestId, + [Frozen] Mock mediator, + [Greedy] ApprenticeshipApprovalsController controller) + { + mediator.Setup(x => x.Send(It.Is(q => q.ApprenticeshipId == apprenticeshipId && q.ApprovalRequestId == ApprovalRequestId), + CancellationToken.None)).ReturnsAsync(result); + + var actual = await controller.GetApprenticeshipApproval(apprenticeshipId, ApprovalRequestId) as OkObjectResult; + + actual.Should().NotBeNull(); + var model = actual.Value as GetApprenticeshipApprovalQueryResult; + model.Should().Be(result); + } + + [Test, MoqAutoData] + public async Task Then_The_Request_Is_Passed_To_Mediator_And_NoData_Returned( + long apprenticeshipId, + Guid ApprovalRequestId, + [Frozen] Mock mediator, + [Greedy] ApprenticeshipApprovalsController controller) + { + mediator.Setup(x => x.Send(It.Is(q => q.ApprenticeshipId == apprenticeshipId && q.ApprovalRequestId == ApprovalRequestId), + CancellationToken.None)).ReturnsAsync((GetApprenticeshipApprovalQueryResult)null); + + var actual = await controller.GetApprenticeshipApproval(apprenticeshipId, ApprovalRequestId) as NotFoundResult; + + actual.Should().NotBeNull(); + } + } +} \ No newline at end of file diff --git a/src/CommitmentsV2/SFA.DAS.CommitmentsV2.Api.UnitTests/Controllers/ApprenticeshipApprovalControllerTests/WhenProcessingApprenticeshipApprovalRequest.cs b/src/CommitmentsV2/SFA.DAS.CommitmentsV2.Api.UnitTests/Controllers/ApprenticeshipApprovalControllerTests/WhenProcessingApprenticeshipApprovalRequest.cs new file mode 100644 index 000000000..a08a69b04 --- /dev/null +++ b/src/CommitmentsV2/SFA.DAS.CommitmentsV2.Api.UnitTests/Controllers/ApprenticeshipApprovalControllerTests/WhenProcessingApprenticeshipApprovalRequest.cs @@ -0,0 +1,32 @@ +using AutoFixture.NUnit3; +using Microsoft.AspNetCore.Mvc; +using SFA.DAS.CommitmentsV2.Api.Controllers; +using SFA.DAS.CommitmentsV2.Api.Types.Requests; +using SFA.DAS.CommitmentsV2.Application.Commands.ProcessApprenticeshipApproval; +using SFA.DAS.Testing.AutoFixture; + +namespace SFA.DAS.CommitmentsV2.Api.UnitTests.Controllers.ApprenticeshipApprovalControllerTests +{ + public class WhenProcessingApprenticeshipApprovalRequest + { + [Test, MoqAutoData] + public async Task Then_The_Request_Is_Passed_To_Mediator_And_Ok_Returned( + long apprenticeshipId, + Guid ApprovalRequestId, + ProcessApprenticeshipApprovalRequest request, + [Frozen] Mock mediator, + [Greedy] ApprenticeshipApprovalsController controller) + { + mediator.Setup(x => x.Send(It.IsAny(), CancellationToken.None)); + + var actual = await controller.PostApprenticeshipApproval(apprenticeshipId, ApprovalRequestId, request) as OkResult; + + actual.Should().NotBeNull(); + mediator.Verify(x => x.Send(It.Is(c => + c.ApprenticeshipId == apprenticeshipId && + c.ApprovalRequestId == ApprovalRequestId && + c.ApplyChanges == request.ApplyChanges && + c.UserInfo == request.UserInfo), CancellationToken.None), Times.Once); + } + } +} \ No newline at end of file diff --git a/src/CommitmentsV2/SFA.DAS.CommitmentsV2.Api/Controllers/ApprenticeshipApprovalsController.cs b/src/CommitmentsV2/SFA.DAS.CommitmentsV2.Api/Controllers/ApprenticeshipApprovalsController.cs index d8260cdba..f66a837ee 100644 --- a/src/CommitmentsV2/SFA.DAS.CommitmentsV2.Api/Controllers/ApprenticeshipApprovalsController.cs +++ b/src/CommitmentsV2/SFA.DAS.CommitmentsV2.Api/Controllers/ApprenticeshipApprovalsController.cs @@ -36,6 +36,4 @@ await mediator.Send(new ProcessApprenticeshipApprovalCommand return Ok(); } - - } \ No newline at end of file From 2715ed742366a9905c1ed91f8d039db086e41109 Mon Sep 17 00:00:00 2001 From: Paul Graham Date: Mon, 3 Aug 2026 15:33:40 +0100 Subject: [PATCH 11/13] added unit tests --- .../Events/LearningChangeEvents.cs | 24 +++ ...prenticeshipApprovalCommandHandlerTests.cs | 159 ++++++++++++++++++ ...essApprenticeshipApprovalCommandHandler.cs | 112 +++++++++--- 3 files changed, 269 insertions(+), 26 deletions(-) create mode 100644 src/CommitmentsV2/SFA.DAS.CommitmentsV2.Messages/Events/LearningChangeEvents.cs create mode 100644 src/CommitmentsV2/SFA.DAS.CommitmentsV2.UnitTests/Application/Commands/ProcessApprenticeshipApprovalCommandHandlerTests.cs diff --git a/src/CommitmentsV2/SFA.DAS.CommitmentsV2.Messages/Events/LearningChangeEvents.cs b/src/CommitmentsV2/SFA.DAS.CommitmentsV2.Messages/Events/LearningChangeEvents.cs new file mode 100644 index 000000000..a7dc43114 --- /dev/null +++ b/src/CommitmentsV2/SFA.DAS.CommitmentsV2.Messages/Events/LearningChangeEvents.cs @@ -0,0 +1,24 @@ +using System; +using System.Collections.Generic; + +namespace SFA.DAS.CommitmentsV2.Messages.Events; + + +public class LearningChangeApprovedEvent : LearningChangeEvent { } + +public class LearningChangeRejectedEvent : LearningChangeEvent { } + + +public class LearningChangeEvent +{ + public Guid LearningKey { get; set; } + public long ApprenticeshipId { get; set; } + public Dictionary Changes { get; set; } + + public class Change + { + public string Old { get; set; } + public string New { get; set; } + public DateTime? EffectiveFromDate { get; set; } + } +} \ No newline at end of file diff --git a/src/CommitmentsV2/SFA.DAS.CommitmentsV2.UnitTests/Application/Commands/ProcessApprenticeshipApprovalCommandHandlerTests.cs b/src/CommitmentsV2/SFA.DAS.CommitmentsV2.UnitTests/Application/Commands/ProcessApprenticeshipApprovalCommandHandlerTests.cs new file mode 100644 index 000000000..5cb18dfa8 --- /dev/null +++ b/src/CommitmentsV2/SFA.DAS.CommitmentsV2.UnitTests/Application/Commands/ProcessApprenticeshipApprovalCommandHandlerTests.cs @@ -0,0 +1,159 @@ +using NServiceBus; +using SFA.DAS.CommitmentsV2.Application.Commands.ProcessApprenticeshipApproval; +using SFA.DAS.CommitmentsV2.Data; +using SFA.DAS.CommitmentsV2.Messages.Commands; +using SFA.DAS.CommitmentsV2.Messages.Events; +using SFA.DAS.CommitmentsV2.Models; +using SFA.DAS.CommitmentsV2.Types; + +namespace SFA.DAS.CommitmentsV2.UnitTests.Application.Commands; +[TestFixture] +public class ProcessApprenticeshipApprovalCommandHandlerTests +{ + private Fixture _autoFixture; + private ProcessApprenticeshipApprovalCommandHandlerTestsFixture _fixture; + + [SetUp] + public void Arrange() + { + _autoFixture = new Fixture(); + _fixture = new ProcessApprenticeshipApprovalCommandHandlerTestsFixture(); + } + + [Test] + public async Task When_HandlingCommand_And_ApprovalRequest_NotFound_Throw_Exception() + { + _fixture.ApprovalRequest = null; + var act = async () => await _fixture.Handle(); + await act.Should().ThrowAsync().WithMessage($"Approval request {_fixture.Command.ApprovalRequestId} not found"); + } + + [Test] + public async Task When_HandlingCommand_And_ApprovalRequest_Found_But_Wrong_Apprenticeship_Throw_Exception() + { + _fixture.ApprovalRequest.ApprenticeshipId = _fixture.Command.ApprenticeshipId + 1; + await _fixture.SeedData(); + var act = async () => await _fixture.Handle(); + await act.Should().ThrowAsync().WithMessage($"Approval request {_fixture.Command.ApprovalRequestId} not found for apprenticeship {_fixture.Command.ApprenticeshipId}"); + } + + [Test] + public async Task When_HandlingCommand_And_ApprovalRequest_Found_But_Status_Not_Pending_Throw_Exception() + { + var items = new List(); + _fixture.ApprovalRequest.Status = CocApprovalResultStatus.Cancelled; + await _fixture.SeedData(); + var act = async () => await _fixture.Handle(); + await act.Should().ThrowAsync().WithMessage($"Approval request {_fixture.Command.ApprovalRequestId} is no longer pending. It's status is {_fixture.ApprovalRequest.Status}"); + } + + [Test] + public async Task When_HandlingCommand_Should_Send_Command_ToChangeHistory() + { + await _fixture.SeedData(); + await _fixture.Handle(); + + _fixture.MessageSession.Verify(y => y.Send(It.Is(x => x.ApprenticeshipId == _fixture.Command.ApprenticeshipId && + x.Source == LearningSourceType.ApprovalAPI && + x.ChangeType == (_fixture.Command.ApplyChanges ? LearningChangeType.EmployerApproved : LearningChangeType.EmployerRejected) && + x.Description == "Total price change from £1,100 to £2,200" + ), It.IsAny()), Times.Once); + } + + + [Test] + public async Task When_HandlingCommand_Should_Publish_LearningChangeApprovedEvent() + { + _fixture.Command.ApplyChanges = true; + await _fixture.SeedData(); + await _fixture.Handle(); + + _fixture.MessageSession.Verify(y => y.Publish(It.Is(x => x.ApprenticeshipId == _fixture.Command.ApprenticeshipId && + x.LearningKey == _fixture.ApprovalRequest.LearningKey && + x.Changes["TrainingPrice"].Old == "1000" && + x.Changes["TrainingPrice"].New == "2000" && + x.Changes["AssessmentPrice"].Old == "100" && + x.Changes["AssessmentPrice"].New == "200"), It.IsAny()), Times.Once); + } + + [Test] + public async Task When_HandlingCommand_Should_Publish_LearningChangeRejectedEvent() + { + _fixture.Command.ApplyChanges = false; + await _fixture.SeedData(); + await _fixture.Handle(); + + _fixture.MessageSession.Verify(y => y.Publish(It.Is(x => x.ApprenticeshipId == _fixture.Command.ApprenticeshipId && + x.LearningKey == _fixture.ApprovalRequest.LearningKey && + x.Changes["TrainingPrice"].Old == "1000" && + x.Changes["TrainingPrice"].New == "2000" && + x.Changes["AssessmentPrice"].Old == "100" && + x.Changes["AssessmentPrice"].New == "200"), It.IsAny()), Times.Once); + } + + + public class ProcessApprenticeshipApprovalCommandHandlerTestsFixture + { + public ProcessApprenticeshipApprovalCommandHandler Handler; + public ProcessApprenticeshipApprovalCommand Command; + public ProviderCommitmentsDbContext Db { get; set; } + public ApprovalRequest ApprovalRequest; + public List Items; + public Mock MessageSession; + + public ProcessApprenticeshipApprovalCommandHandlerTestsFixture() + { + var autoFixture = new Fixture(); + Db = new ProviderCommitmentsDbContext(new DbContextOptionsBuilder() + .UseInMemoryDatabase(Guid.NewGuid().ToString()) + .Options); + MessageSession = new Mock(); + + Handler = new ProcessApprenticeshipApprovalCommandHandler( + new Lazy(() => Db), + MessageSession.Object); + + Command = autoFixture.Create(); + Items = + [ + autoFixture.Build() + .With(x => x.Id, Guid.NewGuid()) + .With(x => x.Field, "TNP1") + .With(x => x.Old, "1000") + .With(x => x.New, "2000") + .Without(x => x.ApprovalRequestId) + .Without(x => x.ApprovalRequest) + .Create(), + autoFixture.Build() + .With(x => x.Id, Guid.NewGuid()) + .With(x => x.Field, "TNP2") + .With(x => x.Old, "100") + .With(x => x.New, "200") + .Without(x => x.ApprovalRequestId) + .Without(x => x.ApprovalRequest) + .Create(), + ]; + + ApprovalRequest = autoFixture.Build() + .With(x => x.Items, Items) + .With(x => x.Id, Command.ApprovalRequestId) + .With(x => x.ApprenticeshipId, Command.ApprenticeshipId) + .With(x => x.Status, CocApprovalResultStatus.Pending).Create(); + } + + public async Task Handle() + { + await Handler.Handle(Command, CancellationToken.None); + } + + public async Task SeedData() + { + if (ApprovalRequest != null) + { + Db.ApprovalRequests.Add(ApprovalRequest); + await Db.SaveChangesAsync(); + } + return this; + } + } +} diff --git a/src/CommitmentsV2/SFA.DAS.CommitmentsV2/Application/Commands/ProcessApprenticeshipApproval/ProcessApprenticeshipApprovalCommandHandler.cs b/src/CommitmentsV2/SFA.DAS.CommitmentsV2/Application/Commands/ProcessApprenticeshipApproval/ProcessApprenticeshipApprovalCommandHandler.cs index 0b9e864bc..3f06945c5 100644 --- a/src/CommitmentsV2/SFA.DAS.CommitmentsV2/Application/Commands/ProcessApprenticeshipApproval/ProcessApprenticeshipApprovalCommandHandler.cs +++ b/src/CommitmentsV2/SFA.DAS.CommitmentsV2/Application/Commands/ProcessApprenticeshipApproval/ProcessApprenticeshipApprovalCommandHandler.cs @@ -1,14 +1,16 @@ -using System.Runtime.Loader; -using System.Security.Cryptography; -using System.Threading.Channels; -using Newtonsoft.Json; +using System.Globalization; +using NServiceBus; using SFA.DAS.CommitmentsV2.Data; +using SFA.DAS.CommitmentsV2.Messages.Commands; +using SFA.DAS.CommitmentsV2.Messages.Events; using SFA.DAS.CommitmentsV2.Models; +using SFA.DAS.CommitmentsV2.Types; namespace SFA.DAS.CommitmentsV2.Application.Commands.ProcessApprenticeshipApproval; public class ProcessApprenticeshipApprovalCommandHandler( - Lazy dbContext) + Lazy dbContext, + IMessageSession messageSession) : IRequestHandler { public async Task Handle(ProcessApprenticeshipApprovalCommand command, CancellationToken cancellationToken) @@ -19,7 +21,7 @@ public async Task Handle(ProcessApprenticeshipApprovalCommand command, Cancellat if (approval == null) { - throw new Exception($"Approval request {command.ApprovalRequestId} not found "); + throw new Exception($"Approval request {command.ApprovalRequestId} not found"); } if (approval.ApprenticeshipId != command.ApprenticeshipId) { @@ -27,47 +29,105 @@ public async Task Handle(ProcessApprenticeshipApprovalCommand command, Cancellat } if (approval.Status != CocApprovalResultStatus.Pending) { - throw new Exception($"Approval request {command.ApprovalRequestId} is no longer pending. It#s status is {approval.Status}"); + throw new Exception($"Approval request {command.ApprovalRequestId} is no longer pending. It's status is {approval.Status}"); } - if (command.ApplyChanges) + if(command.ApplyChanges) { approval.Status = CocApprovalResultStatus.Complete; - + var approved = new LearningChangeApprovedEvent + { + LearningKey = approval.LearningKey, + ApprenticeshipId = approval.ApprenticeshipId, + Changes = ConvertItemsToChangeDictionary(approval.Items) + }; + await messageSession.Publish(approved); } else { approval.Status = CocApprovalResultStatus.Cancelled; + var rejected = new LearningChangeRejectedEvent + { + LearningKey = approval.LearningKey, + ApprenticeshipId = approval.ApprenticeshipId, + Changes = ConvertItemsToChangeDictionary(approval.Items) + }; + await messageSession.Publish(rejected); } - await db.SaveChangesAsync(cancellationToken); + await RecordCocUpdatesInLearnerHistory(approval, command.UserInfo, command.ApplyChanges); + } + private async Task RecordCocUpdatesInLearnerHistory(ApprovalRequest approval, UserInfo userInfo, bool applyChanges) + { + if(approval.Items != null && approval.Items.Any(x=>x.Field == "TNP1" || x.Field == "TNP2")) + { + var totalOldValues = SumStringList(approval.Items.Where(x => x.Field == "TNP1" || x.Field == "TNP2").Select(x => x.Old).ToList()); + var totalNewValues = SumStringList(approval.Items.Where(x => x.Field == "TNP1" || x.Field == "TNP2").Select(x => x.New).ToList()); + + await messageSession.Send(new StoreLearningHistoryCommand + { + ApprenticeshipId = approval.ApprenticeshipId, + Source = LearningSourceType.ApprovalAPI, + ChangeType = applyChanges ? LearningChangeType.EmployerApproved : LearningChangeType.EmployerRejected, + AppliedDate = DateTime.UtcNow, + Description = $"Total price change from {ToCurrency(totalOldValues)} to {ToCurrency(totalNewValues)}", + UserId = GetUserId(userInfo) + }); + } + } + public static string ToCurrency(int input) + { + var culture = new CultureInfo("en-GB"); + return input.ToString("C0", culture); } - private void SendAppliedMessage(ApprovalRequest approvalRequest) + private int SumStringList(List list) { + int total = 0; - var jsonObject = new + foreach (var s in list) { - approvalRequest.LearningKey, - approvalRequest.ApprenticeshipId, - - Changes = approvalRequest.Items.ToDictionary( - x => x.Field, - x => new - { - x.Old, - x.New, - x.EffectiveFromDate - }) - - }; - var json = JsonConvert.SerializeObject(jsonObject, Formatting.Indented); + if (int.TryParse(s, out int value)) + total += value; + } + return total; + } + private static Guid? GetUserId(UserInfo userInfo) + { + if (userInfo?.UserId != null && Guid.TryParse(userInfo.UserId, out var userId)) + { + return userId; + } + return null; + } + private Dictionary ConvertItemsToChangeDictionary(ICollection items) + { + var changes = new Dictionary(); + foreach (var item in items) + { + changes[MapToLearningFieldName(item.Field)] = new LearningChangeEvent.Change + { + Old = item.Old, + New = item.New, + EffectiveFromDate = item.EffectiveFromDate + }; + } + return changes; + } + private string MapToLearningFieldName(string fieldName) + { + return fieldName switch + { + "TNP1" => "TrainingPrice", + "TNP2" => "AssessmentPrice", + _ => fieldName + }; } } \ No newline at end of file From 7829a882d0fa87f340633e9094033aac8f473f80 Mon Sep 17 00:00:00 2001 From: Paul Graham Date: Fri, 7 Aug 2026 13:38:06 +0100 Subject: [PATCH 12/13] added domain exception --- ...rocessApprenticeshipApprovalCommandHandlerTests.cs | 11 +++++++++++ .../ProcessApprenticeshipApprovalCommandHandler.cs | 11 +++++++++++ 2 files changed, 22 insertions(+) diff --git a/src/CommitmentsV2/SFA.DAS.CommitmentsV2.UnitTests/Application/Commands/ProcessApprenticeshipApprovalCommandHandlerTests.cs b/src/CommitmentsV2/SFA.DAS.CommitmentsV2.UnitTests/Application/Commands/ProcessApprenticeshipApprovalCommandHandlerTests.cs index 5cb18dfa8..7acb7f965 100644 --- a/src/CommitmentsV2/SFA.DAS.CommitmentsV2.UnitTests/Application/Commands/ProcessApprenticeshipApprovalCommandHandlerTests.cs +++ b/src/CommitmentsV2/SFA.DAS.CommitmentsV2.UnitTests/Application/Commands/ProcessApprenticeshipApprovalCommandHandlerTests.cs @@ -1,6 +1,7 @@ using NServiceBus; using SFA.DAS.CommitmentsV2.Application.Commands.ProcessApprenticeshipApproval; using SFA.DAS.CommitmentsV2.Data; +using SFA.DAS.CommitmentsV2.Domain.Exceptions; using SFA.DAS.CommitmentsV2.Messages.Commands; using SFA.DAS.CommitmentsV2.Messages.Events; using SFA.DAS.CommitmentsV2.Models; @@ -47,6 +48,16 @@ public async Task When_HandlingCommand_And_ApprovalRequest_Found_But_Status_Not_ await act.Should().ThrowAsync().WithMessage($"Approval request {_fixture.Command.ApprovalRequestId} is no longer pending. It's status is {_fixture.ApprovalRequest.Status}"); } + [Test] + public async Task When_HandlingCommand_And_ApprovalRequest_TNPValues_Exceed_Upper_Limit_Throw_DomainException() + { + var items = new List(); + await _fixture.SeedData(); + _fixture.ApprovalRequest.Items.First(x => x.Field == "TNP1").New = "100001"; + var act = async () => await _fixture.Handle(); + await act.Should().ThrowAsync(); + } + [Test] public async Task When_HandlingCommand_Should_Send_Command_ToChangeHistory() { diff --git a/src/CommitmentsV2/SFA.DAS.CommitmentsV2/Application/Commands/ProcessApprenticeshipApproval/ProcessApprenticeshipApprovalCommandHandler.cs b/src/CommitmentsV2/SFA.DAS.CommitmentsV2/Application/Commands/ProcessApprenticeshipApproval/ProcessApprenticeshipApprovalCommandHandler.cs index 3f06945c5..91cf4b0f8 100644 --- a/src/CommitmentsV2/SFA.DAS.CommitmentsV2/Application/Commands/ProcessApprenticeshipApproval/ProcessApprenticeshipApprovalCommandHandler.cs +++ b/src/CommitmentsV2/SFA.DAS.CommitmentsV2/Application/Commands/ProcessApprenticeshipApproval/ProcessApprenticeshipApprovalCommandHandler.cs @@ -1,6 +1,7 @@ using System.Globalization; using NServiceBus; using SFA.DAS.CommitmentsV2.Data; +using SFA.DAS.CommitmentsV2.Domain.Exceptions; using SFA.DAS.CommitmentsV2.Messages.Commands; using SFA.DAS.CommitmentsV2.Messages.Events; using SFA.DAS.CommitmentsV2.Models; @@ -32,6 +33,11 @@ public async Task Handle(ProcessApprenticeshipApprovalCommand command, Cancellat throw new Exception($"Approval request {command.ApprovalRequestId} is no longer pending. It's status is {approval.Status}"); } + if(TotalPriceExceedsLimit(approval)) + { + throw new DomainException("ApproveChanges", "The total cost must be £100,000 or less"); + } + if(command.ApplyChanges) { approval.Status = CocApprovalResultStatus.Complete; @@ -78,6 +84,11 @@ await messageSession.Send(new StoreLearningHistoryCommand } } + private bool TotalPriceExceedsLimit(ApprovalRequest approval) + { + return SumStringList(approval.Items.Where(x => x.Field == "TNP1" || x.Field == "TNP2").Select(x => x.New).ToList()) > 100000; + } + public static string ToCurrency(int input) { var culture = new CultureInfo("en-GB"); From c74c2fc7f1bfbb9f612b89fcf93998f2a613ab65 Mon Sep 17 00:00:00 2001 From: Paul Graham Date: Fri, 7 Aug 2026 14:16:56 +0100 Subject: [PATCH 13/13] updated statuses --- ...prenticeshipApprovalCommandHandlerTests.cs | 31 +++++++++++++++++++ ...essApprenticeshipApprovalCommandHandler.cs | 17 +++++++++- 2 files changed, 47 insertions(+), 1 deletion(-) diff --git a/src/CommitmentsV2/SFA.DAS.CommitmentsV2.UnitTests/Application/Commands/ProcessApprenticeshipApprovalCommandHandlerTests.cs b/src/CommitmentsV2/SFA.DAS.CommitmentsV2.UnitTests/Application/Commands/ProcessApprenticeshipApprovalCommandHandlerTests.cs index 7acb7f965..d74a1cb40 100644 --- a/src/CommitmentsV2/SFA.DAS.CommitmentsV2.UnitTests/Application/Commands/ProcessApprenticeshipApprovalCommandHandlerTests.cs +++ b/src/CommitmentsV2/SFA.DAS.CommitmentsV2.UnitTests/Application/Commands/ProcessApprenticeshipApprovalCommandHandlerTests.cs @@ -71,6 +71,37 @@ public async Task When_HandlingCommand_Should_Send_Command_ToChangeHistory() ), It.IsAny()), Times.Once); } + [Test] + public async Task When_HandlingCommand_Should_SaveRequestAsApproved() + { + _fixture.Command.ApplyChanges = true; + await _fixture.SeedData(); + await _fixture.Handle(); + + var request = await _fixture.Db.ApprovalRequests.FirstOrDefaultAsync(x => x.Id == _fixture.Command.ApprovalRequestId); + + request.Status.Should().Be(CocApprovalResultStatus.Complete); + request.Items.First().Status.Should().Be(CocApprovalItemStatus.EmployerApproved); + request.Items.First().ApproverId.Should().Be(_fixture.Command.UserInfo.UserId); + request.Items.Last().Status.Should().Be(CocApprovalItemStatus.EmployerApproved); + request.Items.Last().ApproverId.Should().Be(_fixture.Command.UserInfo.UserId); + } + + [Test] + public async Task When_HandlingCommand_Should_SaveRequestAsRejected() + { + _fixture.Command.ApplyChanges = false; + await _fixture.SeedData(); + await _fixture.Handle(); + + var request = await _fixture.Db.ApprovalRequests.FirstOrDefaultAsync(x => x.Id == _fixture.Command.ApprovalRequestId); + + request.Status.Should().Be(CocApprovalResultStatus.Complete); + request.Items.First().Status.Should().Be(CocApprovalItemStatus.EmployerRejected); + request.Items.First().ApproverId.Should().Be(_fixture.Command.UserInfo.UserId); + request.Items.Last().Status.Should().Be(CocApprovalItemStatus.EmployerRejected); + request.Items.Last().ApproverId.Should().Be(_fixture.Command.UserInfo.UserId); + } [Test] public async Task When_HandlingCommand_Should_Publish_LearningChangeApprovedEvent() diff --git a/src/CommitmentsV2/SFA.DAS.CommitmentsV2/Application/Commands/ProcessApprenticeshipApproval/ProcessApprenticeshipApprovalCommandHandler.cs b/src/CommitmentsV2/SFA.DAS.CommitmentsV2/Application/Commands/ProcessApprenticeshipApproval/ProcessApprenticeshipApprovalCommandHandler.cs index 91cf4b0f8..16faa4738 100644 --- a/src/CommitmentsV2/SFA.DAS.CommitmentsV2/Application/Commands/ProcessApprenticeshipApproval/ProcessApprenticeshipApprovalCommandHandler.cs +++ b/src/CommitmentsV2/SFA.DAS.CommitmentsV2/Application/Commands/ProcessApprenticeshipApproval/ProcessApprenticeshipApprovalCommandHandler.cs @@ -6,6 +6,7 @@ using SFA.DAS.CommitmentsV2.Messages.Events; using SFA.DAS.CommitmentsV2.Models; using SFA.DAS.CommitmentsV2.Types; +using UserInfo = SFA.DAS.CommitmentsV2.Types.UserInfo; namespace SFA.DAS.CommitmentsV2.Application.Commands.ProcessApprenticeshipApproval; @@ -41,6 +42,12 @@ public async Task Handle(ProcessApprenticeshipApprovalCommand command, Cancellat if(command.ApplyChanges) { approval.Status = CocApprovalResultStatus.Complete; + foreach (var item in approval.Items) + { + item.Status = CocApprovalItemStatus.EmployerApproved; + item.ApproverId = command.UserInfo?.UserId; + } + var approved = new LearningChangeApprovedEvent { LearningKey = approval.LearningKey, @@ -51,7 +58,13 @@ public async Task Handle(ProcessApprenticeshipApprovalCommand command, Cancellat } else { - approval.Status = CocApprovalResultStatus.Cancelled; + approval.Status = CocApprovalResultStatus.Complete; + foreach (var item in approval.Items) + { + item.Status = CocApprovalItemStatus.EmployerRejected; + item.ApproverId = command.UserInfo?.UserId; + } + var rejected = new LearningChangeRejectedEvent { LearningKey = approval.LearningKey, @@ -86,6 +99,8 @@ await messageSession.Send(new StoreLearningHistoryCommand private bool TotalPriceExceedsLimit(ApprovalRequest approval) { + if(approval.Items == null) + return false; return SumStringList(approval.Items.Where(x => x.Field == "TNP1" || x.Field == "TNP2").Select(x => x.New).ToList()) > 100000; }