Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Ps release7.8 #2000

Open
wants to merge 21 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
e2d95b7
FLP-696 FM36 block updates for private beta withdrawals
MikeV-TalentConsulting Feb 10, 2025
01fc161
Unit test
MikeV-TalentConsulting Feb 10, 2025
34484d9
Update JoinedDataModelsExtensions.cs
MikeV-TalentConsulting Feb 11, 2025
e9ba0b6
FLP-788 LearnerStatus now includes withdrawal details
tom-gough Feb 12, 2025
589d627
Merge pull request #1933 from SkillsFundingAgency/FLP-696-FM36-block-…
ShakilUmar Feb 12, 2025
edf0aed
FLP-788 remove erroneous file
tom-gough Feb 12, 2025
418e6f6
FLP-788 Added LastCensusDateOfLearning
tom-gough Feb 12, 2025
cd07c79
Merge branch 'master' into ps-release7.8
ShakilUmar Feb 14, 2025
3362ce2
Merge branch 'ps-release7.8' into FLP-788-withdrawal-dates
ShakilUmar Feb 14, 2025
7d6d41b
FLP-701 WIP
tom-gough Feb 17, 2025
8f092a1
FLP-701 fixed
tom-gough Feb 17, 2025
3fb7e8d
FLP-701 unit tests
tom-gough Feb 18, 2025
cf7d2e8
FLP-788 Added LastDayOfLearning
tom-gough Feb 18, 2025
5d0c028
Merge branch 'FLP-788-withdrawal-dates' of github.com:SkillsFundingAg…
tom-gough Feb 18, 2025
a9ea81f
Merge branch 'master' into ps-release7.8
ShakilUmar Feb 19, 2025
a8522a8
Merge branch 'ps-release7.8' into FLP-788-withdrawal-dates
ShakilUmar Feb 19, 2025
7a4f4a6
Fix to handle null response
chrisfoster76 Feb 19, 2025
6450758
Merge pull request #1942 from SkillsFundingAgency/FLP-788-withdrawal-…
sameikleham Feb 19, 2025
34608a2
Merge branch 'ps-release7.8' into FLP-701-notification
ShakilUmar Feb 19, 2025
beea5c1
Merge pull request #1954 from SkillsFundingAgency/FLP-701-notification
sameikleham Feb 20, 2025
8427150
Merge branch 'master' into ps-release7.8
ShakilUmar Feb 28, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
using NUnit.Framework;
using SFA.DAS.Apprenticeships.Api.Controllers;
using SFA.DAS.Apprenticeships.Api.Models;
using SFA.DAS.Apprenticeships.Application.Notifications;
using SFA.DAS.Apprenticeships.Application.Notifications.Handlers;
using SFA.DAS.SharedOuterApi.Configuration;
using SFA.DAS.SharedOuterApi.InnerApi.Requests.Apprenticeships;
using SFA.DAS.SharedOuterApi.Interfaces;
Expand All @@ -22,6 +24,7 @@ public class WhenFreezePayments
private readonly Fixture _fixture;
private readonly Mock<ILogger<ApprenticeshipController>> _mockLogger;
private Mock<IApprenticeshipsApiClient<ApprenticeshipsApiConfiguration>> _mockApiClient;
private Mock<IMediator> _mockMediator;

public WhenFreezePayments()
{
Expand All @@ -34,19 +37,22 @@ public WhenFreezePayments()
public void Arrange()
{
_mockApiClient = new Mock<IApprenticeshipsApiClient<ApprenticeshipsApiConfiguration>>();
_mockMediator = new Mock<IMediator>();
}

[Test]
public async Task ThenSendsPostRequest()
{
// Arrange
var sut = new ApprenticeshipController(_mockLogger.Object, _mockApiClient.Object, Mock.Of<ICommitmentsV2ApiClient<CommitmentsV2ApiConfiguration>>(), Mock.Of<IMediator>());
var sut = new ApprenticeshipController(_mockLogger.Object, _mockApiClient.Object, Mock.Of<ICommitmentsV2ApiClient<CommitmentsV2ApiConfiguration>>(), _mockMediator.Object);

var apprenticeshipKey = Guid.NewGuid();
var request = _fixture.Create<FreezePaymentsRequest>();

_mockApiClient.Setup(x => x.PostWithResponseCode<object>(It.IsAny<IPostApiRequest>(), false))
.ReturnsAsync(new ApiResponse<object>("", HttpStatusCode.OK, ""));
_mockMediator.Setup(x => x.Send(It.IsAny<PaymentStatusInactiveCommand>(), default))
.ReturnsAsync(new NotificationResponse { Success = true });

// Act
var response = await sut.FreezeApprenticeshipPayments(apprenticeshipKey, request);
Expand All @@ -58,11 +64,32 @@ public async Task ThenSendsPostRequest()
response.Should().BeOfType<OkResult>();
}

[Test]
public async Task ThenSendsNotification()
{
// Arrange
var sut = new ApprenticeshipController(_mockLogger.Object, _mockApiClient.Object, Mock.Of<ICommitmentsV2ApiClient<CommitmentsV2ApiConfiguration>>(), _mockMediator.Object);

var apprenticeshipKey = Guid.NewGuid();
var request = _fixture.Create<FreezePaymentsRequest>();

_mockApiClient.Setup(x => x.PostWithResponseCode<object>(It.IsAny<IPostApiRequest>(), false))
.ReturnsAsync(new ApiResponse<object>("", HttpStatusCode.OK, ""));
_mockMediator.Setup(x => x.Send(It.IsAny<PaymentStatusInactiveCommand>(), default))
.ReturnsAsync(new NotificationResponse { Success = true });

// Act
await sut.FreezeApprenticeshipPayments(apprenticeshipKey, request);

// Assert
_mockMediator.Verify(x => x.Send(It.Is<PaymentStatusInactiveCommand>(cmd => cmd.ApprenticeshipKey == apprenticeshipKey), default), Times.Once);
}

[Test]
public async Task IfRequestFailsReturnsBadRequest()
{
// Arrange
var sut = new ApprenticeshipController(_mockLogger.Object, _mockApiClient.Object, Mock.Of<ICommitmentsV2ApiClient<CommitmentsV2ApiConfiguration>>(), Mock.Of<IMediator>());
var sut = new ApprenticeshipController(_mockLogger.Object, _mockApiClient.Object, Mock.Of<ICommitmentsV2ApiClient<CommitmentsV2ApiConfiguration>>(), _mockMediator.Object);

var apprenticeshipKey = Guid.NewGuid();
var request = _fixture.Create<FreezePaymentsRequest>();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -313,13 +313,22 @@ public async Task<ActionResult> FreezeApprenticeshipPayments(Guid apprenticeship
{
var response = await _apiClient.PostWithResponseCode<object>(new PostFreezePaymentsRequest(apprenticeshipKey, request.Reason), false);

if (string.IsNullOrEmpty(response.ErrorContent))
if (!string.IsNullOrEmpty(response.ErrorContent))
{
return Ok();
_logger.LogError("Error attempting to freeze apprenticeship {apprenticeshipKey} payments. {statusCode} returned from inner api. {message}", apprenticeshipKey, response.StatusCode, response.ErrorContent);
return BadRequest();
}

_logger.LogError("Error attempting to freeze apprenticeship {apprenticeshipKey} payments. {statusCode} returned from inner api. {message}", apprenticeshipKey, response.StatusCode, response.ErrorContent);
return BadRequest();
var notificationCommand = request.ToNotificationCommand(apprenticeshipKey);
var notificationResponse = await _mediator.Send(notificationCommand);

if (!notificationResponse.Success)
{
_logger.LogError("Error attempting to freeze apprenticeship payments Notification(s) to the related part(ies)");
return BadRequest();
}

return Ok();
}

[HttpPost]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,5 +52,13 @@ public static ChangeOfPriceRejectedCommand ToNotificationCommand(this PatchRejec
Rejector = response.Rejector
};
}

public static PaymentStatusInactiveCommand ToNotificationCommand(this FreezePaymentsRequest request, Guid apprenticeshipKey)
{
return new PaymentStatusInactiveCommand
{
ApprenticeshipKey = apprenticeshipKey
};
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using NUnit.Framework;
using SFA.DAS.Apprenticeships.Application.Notifications.Handlers;
using SFA.DAS.Employer.Shared.UI;

namespace SFA.DAS.Apprenticeships.UnitTests.Application.Notifications.Handlers;

internal class WhenHandlePaymentsStatusInactiveCommand : BaseHandlerTestHelper
{
private UrlBuilder _externalProviderUrlHelper;

public WhenHandlePaymentsStatusInactiveCommand()
{
_externalProviderUrlHelper = new UrlBuilder("AT");
}

[SetUp]
public void SetUp()
{
Reset();
}

[Test]
public async Task ShouldSendToProviderWithCorrectTokens()
{
// Arrange
var command = new PaymentStatusInactiveCommand
{
ApprenticeshipKey = Guid.NewGuid()
};
var handler = new PaymentsStatusInactiveCommandHandler(GetExtendedNotificationService(), _externalProviderUrlHelper);

// Act
var response = await handler.Handle(command, new CancellationToken());

// Assert
VerifySentToProvider("PaymentStatusInactiveToProvider", new Dictionary<string, string>
{
{ "Training provider", ExpectedApprenticeshipDetails.ProviderName },
{ "Employer", ExpectedApprenticeshipDetails.EmployerName },
{ "apprentice", $"{ExpectedApprenticeshipDetails.ApprenticeFirstName} {ExpectedApprenticeshipDetails.ApprenticeLastName}" },
{ "Apprentice details URL", $"https://approvals.at-eas.apprenticeships.education.gov.uk/{ExpectedApprenticeshipDetails.EmployerAccountHashedId}/apprentices/{ExpectedApprenticeshipDetails.ApprenticeshipHashedId}/details" },
{ "date", DateTime.Now.ToString("d MMMM yyyy") }
});
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
using MediatR;
using SFA.DAS.Apprenticeships.Application.Notifications.Templates;
using SFA.DAS.Employer.Shared.UI;

namespace SFA.DAS.Apprenticeships.Application.Notifications.Handlers
{

public class PaymentStatusInactiveCommand : NotificationCommandBase, IRequest<NotificationResponse> { }

public class PaymentsStatusInactiveCommandHandler : NotificationCommandHandlerBase<PaymentStatusInactiveCommand>
{
private readonly UrlBuilder _externalProviderUrlHelper;

public PaymentsStatusInactiveCommandHandler(
IExtendedNotificationService notificationService,
UrlBuilder externalProviderUrlHelper)
: base(notificationService)
{
_externalProviderUrlHelper = externalProviderUrlHelper;
}

public override async Task<NotificationResponse> Handle(PaymentStatusInactiveCommand request, CancellationToken cancellationToken)
{
return await SendToProvider(request, PaymentStatusInactiveToProvider.TemplateId, (_, apprenticeship) => GetProviderTokens(apprenticeship));
}

private Dictionary<string, string> GetProviderTokens(CommitmentsApprenticeshipDetails apprenticeshipDetails)
{
var linkUrl = _externalProviderUrlHelper.CommitmentsV2Link("ApprenticeDetails", apprenticeshipDetails.EmployerAccountHashedId, apprenticeshipDetails.ApprenticeshipHashedId);

var tokens = new Dictionary<string, string>();
tokens.Add(PaymentStatusInactiveToProvider.TrainingProvider, apprenticeshipDetails.ProviderName);
tokens.Add(PaymentStatusInactiveToProvider.Employer, apprenticeshipDetails.EmployerName);
tokens.Add(PaymentStatusInactiveToProvider.Apprentice, $"{apprenticeshipDetails.ApprenticeFirstName} {apprenticeshipDetails.ApprenticeLastName}");
tokens.Add(PaymentStatusInactiveToProvider.ApprenticeshipDetailsUrl, linkUrl);
tokens.Add(PaymentStatusInactiveToProvider.Date, DateTime.Now.ToString("d MMMM yyyy"));

return tokens;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
namespace SFA.DAS.Apprenticeships.Application.Notifications.Templates;

internal class PaymentStatusInactiveToProvider
{
public const string TemplateId = "PaymentStatusInactiveToProvider";
public const string Employer = "Employer";
public const string TrainingProvider = "Training provider";
public const string Date = "date";
public const string Apprentice = "apprentice";
public const string ApprenticeshipDetailsUrl = "Apprentice details URL";
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ public MappingProfile()
CreateMap<GetApprenticeshipsResponse.ApprenticeshipDetailsResponse, GetApprenticeshipsCSVQueryResult.ApprenticeshipDetailsCSVResponse>();
CreateMap<GetApprenticeshipsCSVQueryResult, PostApprenticeshipsCSVResponse>();
CreateMap<GetApprenticeshipsCSVQueryResult.ApprenticeshipDetailsCSVResponse, PostApprenticeshipsCSVResponse.ApprenticeshipDetailsCSVResponse>();
CreateMap<Application.Apprentices.Queries.Apprenticeship.GetManageApprenticeshipDetails.LearnerStatusDetails, Models.Apprentices.LearnerStatusDetails>();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ public class GetManageApprenticeshipDetailsResponse
public PendingStartDateChangeDetails PendingStartDateChange { get; set; }
public bool? CanActualStartDateBeChanged { get; set; }
public PaymentsStatusDetails PaymentsStatus { get; set; }
public LearnerStatus LearnerStatus { get; set; }
public LearnerStatusDetails LearnerStatusDetails { get; set; }

public class ApprenticeshipDetails
{
Expand Down Expand Up @@ -199,4 +199,13 @@ public class PaymentsStatusDetails
public DateTime? FrozenOn { get; set; }
}
}

public class LearnerStatusDetails
{
public LearnerStatus LearnerStatus { get; set; }
public DateTime? WithdrawalChangedDate { get; set; }
public string WithdrawalReason { get; set; }
public DateTime? LastCensusDateOfLearning { get; set; }
public DateTime? LastDayOfLearning { get; set; }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
using SFA.DAS.Approvals.InnerApi.CommitmentsV2Api.Requests;
using SFA.DAS.Approvals.InnerApi.CommitmentsV2Api.Responses;
using SFA.DAS.Approvals.Services;
using SFA.DAS.SharedOuterApi.Common;
using SFA.DAS.SharedOuterApi.Configuration;
using SFA.DAS.SharedOuterApi.Exceptions;
using SFA.DAS.SharedOuterApi.Extensions;
Expand All @@ -36,8 +37,6 @@ public class GetManageApprenticeshipDetailsQueryHandler(
ICollectionCalendarApiClient<CollectionCalendarApiConfiguration> collectionCalendarApiClient)
: IRequestHandler<GetManageApprenticeshipDetailsQuery, GetManageApprenticeshipDetailsQueryResult>
{
public const int QualifyingPeriod = 42; // number of days

public async Task<GetManageApprenticeshipDetailsQueryResult> Handle(GetManageApprenticeshipDetailsQuery request, CancellationToken cancellationToken)
{
var apprenticeshipResponse = await apiClient.GetWithResponseCode<GetApprenticeshipResponse>(new GetApprenticeshipRequest(request.ApprenticeshipId));
Expand Down Expand Up @@ -120,7 +119,7 @@ await Task.WhenAll(priceEpisodesResponseTask,
result.CanActualStartDateBeChanged = canActualStartDateBeChanged;
result.PendingStartDateChange = ToResponse(pendingStartDateResponse.Body);
result.PaymentsStatus = ToResponse(paymentStatusResponse.Body);
result.LearnerStatus = ToResponse(learnerStatusResponse.Body);
result.LearnerStatusDetails = ToResponse(learnerStatusResponse.Body);

return result;
}
Expand Down Expand Up @@ -171,11 +170,18 @@ private PaymentsStatus ToResponse(GetPaymentStatusApiResponse source)
};
}

private LearnerStatus ToResponse(GetLearnerStatusResponse source)
private LearnerStatusDetails ToResponse(GetLearnerStatusResponse source)
{
if (source == null) return LearnerStatus.None;
if (source?.LearnerStatus == null) return new LearnerStatusDetails{ LearnerStatus = LearnerStatus.None };

return source.LearnerStatus;
return new LearnerStatusDetails
{
LearnerStatus = source.LearnerStatus.Value,
WithdrawalChangedDate = source.WithdrawalChangedDate,
WithdrawalReason = source.WithdrawalReason,
LastCensusDateOfLearning = source.LastCensusDateOfLearning,
LastDayOfLearning = source.LastDayOfLearning
};
}

private async Task<bool?> CanActualStartDateBeChanged(DateTime? actualStartDate)
Expand All @@ -185,7 +191,7 @@ private LearnerStatus ToResponse(GetLearnerStatusResponse source)
return null;
}

var fundingQualifyingPeriodEnd = actualStartDate.Value.AddDays(QualifyingPeriod + 1).AddTicks(-1);
var fundingQualifyingPeriodEnd = actualStartDate.Value.AddDays(Constants.QualifyingPeriod + 1).AddTicks(-1);
if (fundingQualifyingPeriodEnd < DateTime.Now)
{
return false;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ public class GetManageApprenticeshipDetailsQueryResult
public bool? CanActualStartDateBeChanged { get; set; }
public PendingStartDateChange PendingStartDateChange { get; set; }
public PaymentsStatus PaymentsStatus { get; set; }
public LearnerStatus LearnerStatus { get; set; }
public LearnerStatusDetails LearnerStatusDetails { get; set; }
}

public class PendingPriceChange
Expand Down Expand Up @@ -56,4 +56,13 @@ public class PaymentsStatus
public string ReasonFrozen { get; set; }
public DateTime? FrozenOn { get; set; }
}

public class LearnerStatusDetails
{
public LearnerStatus LearnerStatus { get; set; }
public DateTime? WithdrawalChangedDate { get; set; }
public string WithdrawalReason { get; set; }
public DateTime? LastCensusDateOfLearning { get; set; }
public DateTime? LastDayOfLearning { get; set; }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -277,4 +277,19 @@ public async Task CallSubjectUnderTest()
}
}
}
}

public static class GetAllEarningsQueryTestFixtureExtensions
{
public static void EditApprenticeshipResponse(this GetAllEarningsQueryTestFixture fixture, int index, Action<Apprenticeship> editAction)
{
if (index < 0 || index >= fixture.ApprenticeshipsResponse.Apprenticeships.Count)
{
throw new ArgumentOutOfRangeException(nameof(index), "Index is out of range.");
}

var apprenticeship = fixture.ApprenticeshipsResponse.Apprenticeships[index];
editAction(apprenticeship);
}

}
Loading
Loading