From 4f5ba9b23fc3fd23730420d7063780939488d745 Mon Sep 17 00:00:00 2001 From: Kevin Joy Date: Fri, 16 Feb 2024 09:35:44 +0000 Subject: [PATCH] Added v3 put QTLS endpoint --- .../Properties/StringResources.Designer.cs | 9 + .../Properties/StringResources.resx | 3 + .../V3/Core/Operations/GetQTLS.cs | 36 ++ .../V3/Core/Operations/SetQTLS.cs | 81 +++ .../V3/Core/SharedModels/QTLSInfo.cs | 7 + .../Controllers/PersonsController.cs | 73 +++ .../V3/V20240307/Requests/SetQTLSRequest.cs | 12 + .../Validators/SetQTLSSRequestValidator.cs | 18 + .../Validation/ErrorRegistry.cs | 3 + .../Dqt/Models/GeneratedCode.cs | 21 + .../Dqt/Models/GeneratedOptionSets.cs | 21 + .../Dqt/Queries/CreateReviewTaskQuery.cs | 8 + .../Dqt/Queries/SetQTLSDateQuery.cs | 3 + .../QueryHandlers/CreateReviewTaskHandler.cs | 20 + .../Dqt/QueryHandlers/SetQTLSDateHandler.cs | 22 + .../V3/V20240307/GetQTLSDateRequestTests.cs | 138 +++++ .../V3/V20240307/SetQTLSDateRequestTests.cs | 584 ++++++++++++++++++ .../QueryTests/CreateReviewTaskTests.cs | 44 ++ .../QueryTests/SetQTLSDateTests.cs | 26 + .../Plugins/QtsRegistrationUpdatedPlugin.cs | 102 ++- .../Plugins/UpdateInductionStatusPlugin.cs | 336 ++++++++++ .../ServiceCollectionExtensions.cs | 2 + .../TestData.CreatePerson.cs | 48 +- crm_attributes.json | 3 +- 24 files changed, 1612 insertions(+), 8 deletions(-) create mode 100644 TeachingRecordSystem/src/TeachingRecordSystem.Api/V3/Core/Operations/GetQTLS.cs create mode 100644 TeachingRecordSystem/src/TeachingRecordSystem.Api/V3/Core/Operations/SetQTLS.cs create mode 100644 TeachingRecordSystem/src/TeachingRecordSystem.Api/V3/Core/SharedModels/QTLSInfo.cs create mode 100644 TeachingRecordSystem/src/TeachingRecordSystem.Api/V3/V20240307/Controllers/PersonsController.cs create mode 100644 TeachingRecordSystem/src/TeachingRecordSystem.Api/V3/V20240307/Requests/SetQTLSRequest.cs create mode 100644 TeachingRecordSystem/src/TeachingRecordSystem.Api/V3/V20240307/Validators/SetQTLSSRequestValidator.cs create mode 100644 TeachingRecordSystem/src/TeachingRecordSystem.Core/Dqt/Queries/CreateReviewTaskQuery.cs create mode 100644 TeachingRecordSystem/src/TeachingRecordSystem.Core/Dqt/Queries/SetQTLSDateQuery.cs create mode 100644 TeachingRecordSystem/src/TeachingRecordSystem.Core/Dqt/QueryHandlers/CreateReviewTaskHandler.cs create mode 100644 TeachingRecordSystem/src/TeachingRecordSystem.Core/Dqt/QueryHandlers/SetQTLSDateHandler.cs create mode 100644 TeachingRecordSystem/tests/TeachingRecordSystem.Api.Tests/V3/V20240307/GetQTLSDateRequestTests.cs create mode 100644 TeachingRecordSystem/tests/TeachingRecordSystem.Api.Tests/V3/V20240307/SetQTLSDateRequestTests.cs create mode 100644 TeachingRecordSystem/tests/TeachingRecordSystem.Core.Dqt.CrmIntegrationTests/QueryTests/CreateReviewTaskTests.cs create mode 100644 TeachingRecordSystem/tests/TeachingRecordSystem.Core.Dqt.CrmIntegrationTests/QueryTests/SetQTLSDateTests.cs create mode 100644 TeachingRecordSystem/tests/TeachingRecordSystem.TestCommon/Infrastructure/FakeXrmEasy/Plugins/UpdateInductionStatusPlugin.cs diff --git a/TeachingRecordSystem/src/TeachingRecordSystem.Api/Properties/StringResources.Designer.cs b/TeachingRecordSystem/src/TeachingRecordSystem.Api/Properties/StringResources.Designer.cs index c31a81dd92..fbf2df0a16 100644 --- a/TeachingRecordSystem/src/TeachingRecordSystem.Api/Properties/StringResources.Designer.cs +++ b/TeachingRecordSystem/src/TeachingRecordSystem.Api/Properties/StringResources.Designer.cs @@ -455,5 +455,14 @@ public static string Errors_10029_Title { return ResourceManager.GetString("Errors.10029.Title", resourceCulture); } } + + /// + /// Looks up a localized string similar to Unable to set QTLSDate. + /// + public static string Errors_10030_Title { + get { + return ResourceManager.GetString("Errors.10030.Title", resourceCulture); + } + } } } diff --git a/TeachingRecordSystem/src/TeachingRecordSystem.Api/Properties/StringResources.resx b/TeachingRecordSystem/src/TeachingRecordSystem.Api/Properties/StringResources.resx index 1528bcad96..f1009370f3 100644 --- a/TeachingRecordSystem/src/TeachingRecordSystem.Api/Properties/StringResources.resx +++ b/TeachingRecordSystem/src/TeachingRecordSystem.Api/Properties/StringResources.resx @@ -249,4 +249,7 @@ Request has already been submitted + + Unable to set QTLSDate + \ No newline at end of file diff --git a/TeachingRecordSystem/src/TeachingRecordSystem.Api/V3/Core/Operations/GetQTLS.cs b/TeachingRecordSystem/src/TeachingRecordSystem.Api/V3/Core/Operations/GetQTLS.cs new file mode 100644 index 0000000000..4d6313813e --- /dev/null +++ b/TeachingRecordSystem/src/TeachingRecordSystem.Api/V3/Core/Operations/GetQTLS.cs @@ -0,0 +1,36 @@ +using Microsoft.Xrm.Sdk.Query; +using TeachingRecordSystem.Api.V3.Core.SharedModels; +using TeachingRecordSystem.Core.Dqt; +using TeachingRecordSystem.Core.Dqt.Models; +using TeachingRecordSystem.Core.Dqt.Queries; + +namespace TeachingRecordSystem.Api.V3.Core.Operations; + +public record GetQTLSCommand(string Trn); + +public class GetQTLSHandler(ICrmQueryDispatcher _crmQueryDispatcher) +{ + public async Task Handle(GetQTLSCommand command) + { + var contact = (await _crmQueryDispatcher.ExecuteQuery( + new GetActiveContactByTrnQuery( + command.Trn, + new ColumnSet( + Contact.Fields.dfeta_TRN, + Contact.Fields.dfeta_qtlsdate + ) + ) + ))!; + + if (contact is null) + { + return null; + } + + return new QTLSInfo() + { + Trn = command.Trn, + QTSDate = contact.dfeta_qtlsdate.ToDateOnlyWithDqtBstFix(isLocalTime: true), + }; + } +} diff --git a/TeachingRecordSystem/src/TeachingRecordSystem.Api/V3/Core/Operations/SetQTLS.cs b/TeachingRecordSystem/src/TeachingRecordSystem.Api/V3/Core/Operations/SetQTLS.cs new file mode 100644 index 0000000000..963f59e084 --- /dev/null +++ b/TeachingRecordSystem/src/TeachingRecordSystem.Api/V3/Core/Operations/SetQTLS.cs @@ -0,0 +1,81 @@ +using Microsoft.Xrm.Sdk.Query; +using TeachingRecordSystem.Api.V3.Core.SharedModels; +using TeachingRecordSystem.Api.Validation; +using TeachingRecordSystem.Core.Dqt; +using TeachingRecordSystem.Core.Dqt.Models; +using TeachingRecordSystem.Core.Dqt.Queries; + +namespace TeachingRecordSystem.Api.V3.Core.Operations; + + +public record SetQTLSCommand(string Trn, DateOnly? QTSDate); + +public class SetQTLSHandler(ICrmQueryDispatcher _crmQueryDispatcher) +{ + public async Task Handle(SetQTLSCommand command) + { + var contact = (await _crmQueryDispatcher.ExecuteQuery( + new GetActiveContactByTrnQuery( + command.Trn, + new ColumnSet( + Contact.Fields.dfeta_TRN, + Contact.Fields.dfeta_InductionStatus, + Contact.Fields.dfeta_qtlsdate + ) + ) + ))!; + + if (contact == null) + { + return null; + } + + if (!CanSetQTLSDate(contact.dfeta_InductionStatus, contact.dfeta_qtlsdate, command.QTSDate)) + { + var _ = await _crmQueryDispatcher.ExecuteQuery( + new CreateReviewTaskQuery() + { + TeacherId = contact.Id, + Category = "Unable to set QTLSDate", + Description = $"Unable to set QTLSDate {command.QTSDate}", + Subject = "Notification for SET QTLS data collections team" + } + ); + + throw new ErrorException(ErrorRegistry.UnableToUpdateQTLSDate()); + } + + await _crmQueryDispatcher.ExecuteQuery( + new SetQTLSDateQuery(contact.Id, command.QTSDate))!; + + contact = (await _crmQueryDispatcher.ExecuteQuery( + new GetActiveContactByTrnQuery( + command.Trn, + new ColumnSet( + Contact.Fields.dfeta_TRN, + Contact.Fields.dfeta_qtlsdate + ) + ) + ))!; + + return new QTLSInfo() + { + Trn = command.Trn, + QTSDate = contact.dfeta_qtlsdate.ToDateOnlyWithDqtBstFix(isLocalTime: true) + }; + } + + private bool CanSetQTLSDate(dfeta_InductionStatus? inductionStatus, DateTime? existingQtlsdate, DateOnly? incomingQtlsDate) => + inductionStatus switch + { + dfeta_InductionStatus.InProgress when !existingQtlsdate.HasValue && incomingQtlsDate.HasValue => false, + dfeta_InductionStatus.InductionExtended when !existingQtlsdate.HasValue && incomingQtlsDate.HasValue => false, + dfeta_InductionStatus.Fail => false, + dfeta_InductionStatus.FailedinWales when !existingQtlsdate.HasValue && incomingQtlsDate.HasValue => false, + dfeta_InductionStatus.FailedinWales when existingQtlsdate.HasValue && incomingQtlsDate.HasValue => false, + dfeta_InductionStatus.Exempt when existingQtlsdate.HasValue => false, + _ when existingQtlsdate.HasValue && incomingQtlsDate.HasValue && incomingQtlsDate.Value != existingQtlsdate.ToDateOnlyWithDqtBstFix(isLocalTime: false) => false, + _ => true + }; +} + diff --git a/TeachingRecordSystem/src/TeachingRecordSystem.Api/V3/Core/SharedModels/QTLSInfo.cs b/TeachingRecordSystem/src/TeachingRecordSystem.Api/V3/Core/SharedModels/QTLSInfo.cs new file mode 100644 index 0000000000..0344af6a60 --- /dev/null +++ b/TeachingRecordSystem/src/TeachingRecordSystem.Api/V3/Core/SharedModels/QTLSInfo.cs @@ -0,0 +1,7 @@ +namespace TeachingRecordSystem.Api.V3.Core.SharedModels; + +public record QTLSInfo +{ + public required DateOnly? QTSDate { get; init; } + public required string Trn { get; init; } +} diff --git a/TeachingRecordSystem/src/TeachingRecordSystem.Api/V3/V20240307/Controllers/PersonsController.cs b/TeachingRecordSystem/src/TeachingRecordSystem.Api/V3/V20240307/Controllers/PersonsController.cs new file mode 100644 index 0000000000..296bcd4c94 --- /dev/null +++ b/TeachingRecordSystem/src/TeachingRecordSystem.Api/V3/V20240307/Controllers/PersonsController.cs @@ -0,0 +1,73 @@ +using Mapster; +using MediatR; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Swashbuckle.AspNetCore.Annotations; +using TeachingRecordSystem.Api.V3.Core.Operations; +using TeachingRecordSystem.Api.V3.Core.SharedModels; +using TeachingRecordSystem.Api.V3.VNext.Requests; + +namespace TeachingRecordSystem.Api.V3.V20240307.Controllers; + +[ApiController] +[Route("persons")] +[Authorize(Roles = ApiRoles.UpdatePerson)] +public class PersonsController : ControllerBase +{ + private readonly IMediator _mediator; + + public PersonsController(IMediator mediator) + { + _mediator = mediator; + } + + + [HttpPut("{trn}/qtls")] + [SwaggerOperation( + OperationId = "PutQTLS", + Summary = "Sets QTLS status for a teacher", + Description = "Sets QTLS status for a teacher.")] + [ProducesResponseType(typeof(QTLSInfo), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(void), StatusCodes.Status403Forbidden)] + [MapError(10030, statusCode: StatusCodes.Status202Accepted)] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)] + public async Task Put( + [FromBody] SetQTLSRequest request, + [FromServices] SetQTLSHandler handler) + { + var command = new SetQTLSCommand(request.Trn!, request.QTSDate); + var result = await handler.Handle(command); + + if (result is null) + { + return NotFound(); + } + + var response = result.Adapt(); + return Ok(response); + } + + [HttpGet("{trn}/qtls")] + [SwaggerOperation( + OperationId = "GetQTLS", + Summary = "Gets QTLS status for a teacher", + Description = "Gets QTLS status for a teacher.")] + [ProducesResponseType(typeof(QTLSInfo), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(void), StatusCodes.Status403Forbidden)] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)] + public async Task Get( + [FromRoute] string trn, + [FromServices] GetQTLSHandler handler) + { + var command = new GetQTLSCommand(trn); + var result = await handler.Handle(command); + + if (result is null) + { + return NotFound(); + } + + var response = result.Adapt(); + return Ok(response); + } +} diff --git a/TeachingRecordSystem/src/TeachingRecordSystem.Api/V3/V20240307/Requests/SetQTLSRequest.cs b/TeachingRecordSystem/src/TeachingRecordSystem.Api/V3/V20240307/Requests/SetQTLSRequest.cs new file mode 100644 index 0000000000..c80da0b738 --- /dev/null +++ b/TeachingRecordSystem/src/TeachingRecordSystem.Api/V3/V20240307/Requests/SetQTLSRequest.cs @@ -0,0 +1,12 @@ +using Microsoft.AspNetCore.Mvc; + +namespace TeachingRecordSystem.Api.V3.VNext.Requests; + +public record SetQTLSRequest +{ + public required DateOnly? QTSDate { get; init; } + + [FromRoute] + public string? Trn { get; set; } + +} diff --git a/TeachingRecordSystem/src/TeachingRecordSystem.Api/V3/V20240307/Validators/SetQTLSSRequestValidator.cs b/TeachingRecordSystem/src/TeachingRecordSystem.Api/V3/V20240307/Validators/SetQTLSSRequestValidator.cs new file mode 100644 index 0000000000..e9550dac6e --- /dev/null +++ b/TeachingRecordSystem/src/TeachingRecordSystem.Api/V3/V20240307/Validators/SetQTLSSRequestValidator.cs @@ -0,0 +1,18 @@ +using FluentValidation; +using TeachingRecordSystem.Api.V3.VNext.Requests; + +namespace TeachingRecordSystem.Api.V3.VNext.Validators; + +public class SetQTLSSRequestValidator : AbstractValidator +{ + public SetQTLSSRequestValidator(IClock clock) + { + RuleFor(x => x.Trn) + .Matches(@"^\d{7}$") + .WithMessage(Properties.StringResources.ErrorMessages_TRNMustBe7Digits); + + RuleFor(x => x.QTSDate) + .LessThanOrEqualTo(clock.Today) + .WithMessage($"QTLS Date cannot be in the future."); + } +} diff --git a/TeachingRecordSystem/src/TeachingRecordSystem.Api/Validation/ErrorRegistry.cs b/TeachingRecordSystem/src/TeachingRecordSystem.Api/Validation/ErrorRegistry.cs index a3556222a8..b112f5cb64 100644 --- a/TeachingRecordSystem/src/TeachingRecordSystem.Api/Validation/ErrorRegistry.cs +++ b/TeachingRecordSystem/src/TeachingRecordSystem.Api/Validation/ErrorRegistry.cs @@ -32,6 +32,7 @@ public static class ErrorRegistry ErrorDescriptor.Create(10027), // Unable to change Failed Result. ErrorDescriptor.Create(10028), // The specified URL does not exist ErrorDescriptor.Create(10029), // Request has already been submitted + ErrorDescriptor.Create(10030), // Unable to update QTLSDate }.ToDictionary(d => d.ErrorCode, d => d); public static Error TeacherWithSpecifiedTrnNotFound() => CreateError(10001); @@ -90,6 +91,8 @@ public static class ErrorRegistry public static Error CannotResubmitRequest() => CreateError(10029); + public static Error UnableToUpdateQTLSDate() => CreateError(10030); + private static Error CreateError(int errorCode) { var descriptor = _all[errorCode]; diff --git a/TeachingRecordSystem/src/TeachingRecordSystem.Core/Dqt/Models/GeneratedCode.cs b/TeachingRecordSystem/src/TeachingRecordSystem.Core/Dqt/Models/GeneratedCode.cs index e67a053310..62fe1eee05 100644 --- a/TeachingRecordSystem/src/TeachingRecordSystem.Core/Dqt/Models/GeneratedCode.cs +++ b/TeachingRecordSystem/src/TeachingRecordSystem.Core/Dqt/Models/GeneratedCode.cs @@ -2231,6 +2231,7 @@ public static class Fields public const string dfeta_loginfailedcounter = "dfeta_loginfailedcounter"; public const string dfeta_NINumber = "dfeta_ninumber"; public const string dfeta_PreviousLastName = "dfeta_previouslastname"; + public const string dfeta_qtlsdate = "dfeta_qtlsdate"; public const string dfeta_QTSDate = "dfeta_qtsdate"; public const string dfeta_SlugId = "dfeta_slugid"; public const string dfeta_StatedFirstName = "dfeta_statedfirstname"; @@ -2737,6 +2738,26 @@ public string dfeta_PreviousLastName } } + /// + /// + /// + [Microsoft.Xrm.Sdk.AttributeLogicalNameAttribute("dfeta_qtlsdate")] + public System.Nullable dfeta_qtlsdate + { + [System.Diagnostics.DebuggerNonUserCode()] + get + { + return this.GetAttributeValue>("dfeta_qtlsdate"); + } + [System.Diagnostics.DebuggerNonUserCode()] + set + { + this.OnPropertyChanging("dfeta_qtlsdate"); + this.SetAttributeValue("dfeta_qtlsdate", value); + this.OnPropertyChanged("dfeta_qtlsdate"); + } + } + /// /// /// diff --git a/TeachingRecordSystem/src/TeachingRecordSystem.Core/Dqt/Models/GeneratedOptionSets.cs b/TeachingRecordSystem/src/TeachingRecordSystem.Core/Dqt/Models/GeneratedOptionSets.cs index 594e492c08..41ffdb67d9 100644 --- a/TeachingRecordSystem/src/TeachingRecordSystem.Core/Dqt/Models/GeneratedOptionSets.cs +++ b/TeachingRecordSystem/src/TeachingRecordSystem.Core/Dqt/Models/GeneratedOptionSets.cs @@ -561,6 +561,14 @@ public enum Audit_Action [OptionSetMetadataAttribute("Add To Queue", 46)] AddToQueue = 52, + [System.Runtime.Serialization.EnumMemberAttribute()] + [OptionSetMetadataAttribute("ApplicationBasedAccessAllowed", 82)] + ApplicationBasedAccessAllowed = 122, + + [System.Runtime.Serialization.EnumMemberAttribute()] + [OptionSetMetadataAttribute("ApplicationBasedAccessDenied", 81)] + ApplicationBasedAccessDenied = 121, + [System.Runtime.Serialization.EnumMemberAttribute()] [OptionSetMetadataAttribute("Approve", 22)] Approve = 28, @@ -2824,6 +2832,19 @@ public enum IncidentResolution_StatusCode Open = 1, } + [System.Runtime.Serialization.DataContractAttribute()] + public enum IsInherited + { + + [System.Runtime.Serialization.EnumMemberAttribute()] + [OptionSetMetadataAttribute("Direct User (Basic) access level and Team privileges", 1)] + DirectUser_BasicaccesslevelandTeamprivileges = 1, + + [System.Runtime.Serialization.EnumMemberAttribute()] + [OptionSetMetadataAttribute("Team privileges only", 0)] + Teamprivilegesonly = 0, + } + [System.Runtime.Serialization.DataContractAttribute()] public enum msft_DataState { diff --git a/TeachingRecordSystem/src/TeachingRecordSystem.Core/Dqt/Queries/CreateReviewTaskQuery.cs b/TeachingRecordSystem/src/TeachingRecordSystem.Core/Dqt/Queries/CreateReviewTaskQuery.cs new file mode 100644 index 0000000000..41bcf4d149 --- /dev/null +++ b/TeachingRecordSystem/src/TeachingRecordSystem.Core/Dqt/Queries/CreateReviewTaskQuery.cs @@ -0,0 +1,8 @@ +namespace TeachingRecordSystem.Core.Dqt.Queries; +public record CreateReviewTaskQuery : ICrmQuery +{ + public required Guid TeacherId { get; set; } + public required string Category { get; set; } + public required string Subject { get; set; } + public required string Description { get; set; } +} diff --git a/TeachingRecordSystem/src/TeachingRecordSystem.Core/Dqt/Queries/SetQTLSDateQuery.cs b/TeachingRecordSystem/src/TeachingRecordSystem.Core/Dqt/Queries/SetQTLSDateQuery.cs new file mode 100644 index 0000000000..6a239022c0 --- /dev/null +++ b/TeachingRecordSystem/src/TeachingRecordSystem.Core/Dqt/Queries/SetQTLSDateQuery.cs @@ -0,0 +1,3 @@ +namespace TeachingRecordSystem.Core.Dqt.Queries; + +public record SetQTLSDateQuery(Guid contactId, DateOnly? qtlsDate) : ICrmQuery; diff --git a/TeachingRecordSystem/src/TeachingRecordSystem.Core/Dqt/QueryHandlers/CreateReviewTaskHandler.cs b/TeachingRecordSystem/src/TeachingRecordSystem.Core/Dqt/QueryHandlers/CreateReviewTaskHandler.cs new file mode 100644 index 0000000000..e29f464f11 --- /dev/null +++ b/TeachingRecordSystem/src/TeachingRecordSystem.Core/Dqt/QueryHandlers/CreateReviewTaskHandler.cs @@ -0,0 +1,20 @@ +using Microsoft.PowerPlatform.Dataverse.Client; +using TeachingRecordSystem.Core.Dqt; +using TeachingRecordSystem.Core.Dqt.Queries; + +public class CreateReviewTaskHandler : ICrmQueryHandler +{ + public async Task Execute(CreateReviewTaskQuery query, IOrganizationServiceAsync organizationService) + { + var crmTaskId = await organizationService.CreateAsync(new CrmTask() + { + Id = Guid.NewGuid(), + RegardingObjectId = query.TeacherId.ToEntityReference(Contact.EntityLogicalName), + Category = query.Category, + Subject = query.Subject, + Description = query.Description + }); + + return crmTaskId; + } +} diff --git a/TeachingRecordSystem/src/TeachingRecordSystem.Core/Dqt/QueryHandlers/SetQTLSDateHandler.cs b/TeachingRecordSystem/src/TeachingRecordSystem.Core/Dqt/QueryHandlers/SetQTLSDateHandler.cs new file mode 100644 index 0000000000..5dacf11511 --- /dev/null +++ b/TeachingRecordSystem/src/TeachingRecordSystem.Core/Dqt/QueryHandlers/SetQTLSDateHandler.cs @@ -0,0 +1,22 @@ +using Microsoft.PowerPlatform.Dataverse.Client; +using Microsoft.Xrm.Sdk.Messages; +using TeachingRecordSystem.Core.Dqt.Queries; + +namespace TeachingRecordSystem.Core.Dqt.QueryHandlers; + +public class SetQTLSDateHandler : ICrmQueryHandler +{ + public async Task Execute(SetQTLSDateQuery query, IOrganizationServiceAsync organizationService) + { + await organizationService.ExecuteAsync(new UpdateRequest() + { + Target = new Contact() + { + Id = query.contactId, + dfeta_qtlsdate = query.qtlsDate.ToDateTimeWithDqtBstFix(isLocalTime: false) + } + }); + + return true; + } +} diff --git a/TeachingRecordSystem/tests/TeachingRecordSystem.Api.Tests/V3/V20240307/GetQTLSDateRequestTests.cs b/TeachingRecordSystem/tests/TeachingRecordSystem.Api.Tests/V3/V20240307/GetQTLSDateRequestTests.cs new file mode 100644 index 0000000000..97a966d620 --- /dev/null +++ b/TeachingRecordSystem/tests/TeachingRecordSystem.Api.Tests/V3/V20240307/GetQTLSDateRequestTests.cs @@ -0,0 +1,138 @@ +using System.Net; + +namespace TeachingRecordSystem.Api.Tests.V3.V20240307; + +public class GetQTLSDateRequestTests : TestBase +{ + public GetQTLSDateRequestTests(HostFixture hostFixture) + : base(hostFixture) + { + SetCurrentApiClient(new[] { ApiRoles.UpdatePerson }); + } + + [Theory, RoleNamesData(except: ApiRoles.UpdatePerson)] + public async Task Get_ClientDoesNotHavePermission_ReturnsForbidden(string[] roles) + { + // Arrange + SetCurrentApiClient(roles); + + var requestId = Guid.NewGuid().ToString(); + var firstName = Faker.Name.First(); + var middleName = Faker.Name.Middle(); + var lastName = Faker.Name.Last(); + var dateOfBirth = new DateOnly(1990, 01, 01); + var email = Faker.Internet.Email(); + var nationalInsuranceNumber = Faker.Identification.UkNationalInsuranceNumber(); + + var existingContact = await TestData.CreatePerson(p => p + .WithTrn(hasTrn: true) + .WithFirstName(firstName) + .WithMiddleName(middleName) + .WithLastName(lastName) + .WithDateOfBirth(dateOfBirth) + .WithEmail(email) + .WithNationalInsuranceNumber(nationalInsuranceNumber: nationalInsuranceNumber)); + + var request = new HttpRequestMessage(HttpMethod.Get, $"v3/persons/{existingContact.Trn}/qtls"); + + // Act + var response = await GetHttpClientWithApiKey().SendAsync(request); + + // Assert + Assert.Equal(HttpStatusCode.Forbidden, response.StatusCode); + } + + [Fact] + public async Task Trn_TrnNotFound_ReturnsNotFound() + { + // Arrange + var nonExistentTrn = "1234567"; + + var request = new HttpRequestMessage(HttpMethod.Get, $"v3/persons/{nonExistentTrn}/qtls"); + + // Act + var response = await GetHttpClientWithApiKey().SendAsync(request); + + // Assert + Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); + } + + + [Fact] + public async Task Get_NoQTLS_ReturnsExpectedResult() + { + // Arrange + var requestId = Guid.NewGuid().ToString(); + var firstName = Faker.Name.First(); + var middleName = Faker.Name.Middle(); + var lastName = Faker.Name.Last(); + var dateOfBirth = new DateOnly(1990, 01, 01); + var email = Faker.Internet.Email(); + var nationalInsuranceNumber = Faker.Identification.UkNationalInsuranceNumber(); + + var existingContact = await TestData.CreatePerson(p => p + .WithTrn(hasTrn: true) + .WithFirstName(firstName) + .WithMiddleName(middleName) + .WithLastName(lastName) + .WithDateOfBirth(dateOfBirth) + .WithEmail(email) + .WithNationalInsuranceNumber(nationalInsuranceNumber: nationalInsuranceNumber)); + + var request = new HttpRequestMessage(HttpMethod.Get, $"v3/persons/{existingContact.Trn}/qtls"); + + // Act + var response = await GetHttpClientWithApiKey().SendAsync(request); + + // Assert + await AssertEx.JsonResponseEquals( + response, + expected: new + { + Trn = existingContact.Trn, + QTSDate = default(DateOnly?) + + }, + expectedStatusCode: 200); + } + + [Fact] + public async Task Get_WithQTLS_ReturnsExpectedResult() + { + // Arrange + var qtlsDate = new DateOnly(2020, 01, 01); + var requestId = Guid.NewGuid().ToString(); + var firstName = Faker.Name.First(); + var middleName = Faker.Name.Middle(); + var lastName = Faker.Name.Last(); + var dateOfBirth = new DateOnly(1990, 01, 01); + var email = Faker.Internet.Email(); + var nationalInsuranceNumber = Faker.Identification.UkNationalInsuranceNumber(); + + var existingContact = await TestData.CreatePerson(p => p + .WithTrn(hasTrn: true) + .WithFirstName(firstName) + .WithMiddleName(middleName) + .WithLastName(lastName) + .WithDateOfBirth(dateOfBirth) + .WithEmail(email) + .WithNationalInsuranceNumber(nationalInsuranceNumber: nationalInsuranceNumber) + .WithQTLSDate(qtlsDate)); + + var request = new HttpRequestMessage(HttpMethod.Get, $"v3/persons/{existingContact.Trn}/qtls"); + + // Act + var response = await GetHttpClientWithApiKey().SendAsync(request); + + // Assert + await AssertEx.JsonResponseEquals( + response, + expected: new + { + Trn = existingContact.Trn, + QTSDate = qtlsDate + + }, + expectedStatusCode: 200); + } +} diff --git a/TeachingRecordSystem/tests/TeachingRecordSystem.Api.Tests/V3/V20240307/SetQTLSDateRequestTests.cs b/TeachingRecordSystem/tests/TeachingRecordSystem.Api.Tests/V3/V20240307/SetQTLSDateRequestTests.cs new file mode 100644 index 0000000000..65a0da88e0 --- /dev/null +++ b/TeachingRecordSystem/tests/TeachingRecordSystem.Api.Tests/V3/V20240307/SetQTLSDateRequestTests.cs @@ -0,0 +1,584 @@ +using System.Net; +using TeachingRecordSystem.Api.Properties; +using TeachingRecordSystem.Api.V3.VNext.Requests; +using TeachingRecordSystem.Core.Dqt; + +namespace TeachingRecordSystem.Api.Tests.V3.V20240307; + +public class SetQTLSDateRequestTests : TestBase +{ + public SetQTLSDateRequestTests(HostFixture hostFixture) + : base(hostFixture) + { + SetCurrentApiClient(new[] { ApiRoles.UpdatePerson }); + } + + [Theory, RoleNamesData(except: ApiRoles.UpdatePerson)] + public async Task Put_ClientDoesNotHavePermission_ReturnsForbidden(string[] roles) + { + // Arrange + SetCurrentApiClient(roles); + + var requestId = Guid.NewGuid().ToString(); + var firstName = Faker.Name.First(); + var middleName = Faker.Name.Middle(); + var lastName = Faker.Name.Last(); + var dateOfBirth = new DateOnly(1990, 01, 01); + var email = Faker.Internet.Email(); + var nationalInsuranceNumber = Faker.Identification.UkNationalInsuranceNumber(); + + var existingContact = await TestData.CreatePerson(p => p + .WithTrn(hasTrn: true) + .WithFirstName(firstName) + .WithMiddleName(middleName) + .WithLastName(lastName) + .WithDateOfBirth(dateOfBirth) + .WithEmail(email) + .WithNationalInsuranceNumber(nationalInsuranceNumber: nationalInsuranceNumber)); + + var requestBody = CreateJsonContent(new { QTSDate = new DateOnly(1990, 01, 01) }); + var request = new HttpRequestMessage(HttpMethod.Put, $"v3/persons/{existingContact.Trn}/qtls") + { + Content = requestBody + }; + + // Act + var response = await GetHttpClientWithApiKey().SendAsync(request); + + // Assert + Assert.Equal(HttpStatusCode.Forbidden, response.StatusCode); + } + + [Theory] + [InlineData("123456")] + [InlineData("12345678")] + [InlineData("xxx")] + public async Task Put_InvalidTrn_ReturnsErrror(string trn) + { + // Arrange + var requestBody = CreateJsonContent(new { QTSDate = new DateOnly(1990, 01, 01) }); + var request = new HttpRequestMessage(HttpMethod.Put, $"v3/persons/{trn}/qtls") + { + Content = requestBody + }; + + // Act + var response = await GetHttpClientWithApiKey().SendAsync(request); + + // Assert + await AssertEx.JsonResponseHasValidationErrorForProperty(response, "trn", expectedError: StringResources.ErrorMessages_TRNMustBe7Digits); + } + + [Fact] + public async Task Put_AwardedDateInFuture_ReturnsErrror() + { + // Arrange + var futureDate = Clock.UtcNow.AddDays(1); + var requestId = Guid.NewGuid().ToString(); + var firstName = Faker.Name.First(); + var middleName = Faker.Name.Middle(); + var lastName = Faker.Name.Last(); + var dateOfBirth = new DateOnly(1990, 01, 01); + var email = Faker.Internet.Email(); + var nationalInsuranceNumber = Faker.Identification.UkNationalInsuranceNumber(); + + var existingContact = await TestData.CreatePerson(p => p + .WithTrn(hasTrn: true) + .WithFirstName(firstName) + .WithMiddleName(middleName) + .WithLastName(lastName) + .WithDateOfBirth(dateOfBirth) + .WithEmail(email) + .WithNationalInsuranceNumber(nationalInsuranceNumber: nationalInsuranceNumber)); + + var requestBody = CreateJsonContent(new { QTSDate = futureDate.ToDateOnlyWithDqtBstFix(isLocalTime: true) }); + var request = new HttpRequestMessage(HttpMethod.Put, $"v3/persons/{existingContact.Trn}/qtls") + { + Content = requestBody + }; + + // Act + var response = await GetHttpClientWithApiKey().SendAsync(request); + + // Assert + await AssertEx.JsonResponseHasValidationErrorForProperty( + response, + propertyName: nameof(SetQTLSRequest.QTSDate), + expectedError: "QTLS Date cannot be in the future."); + } + + [Fact] + public async Task Put_TrnNotFound_ReturnsNotFound() + { + // Arrange + var futureDate = Clock.UtcNow.AddDays(1); + var requestId = Guid.NewGuid().ToString(); + var firstName = Faker.Name.First(); + var middleName = Faker.Name.Middle(); + var lastName = Faker.Name.Last(); + var dateOfBirth = new DateOnly(1990, 01, 01); + var email = Faker.Internet.Email(); + var nationalInsuranceNumber = Faker.Identification.UkNationalInsuranceNumber(); + var nonExistentTrn = "1234567"; + + var existingContact = await TestData.CreatePerson(p => p + .WithTrn(hasTrn: true) + .WithFirstName(firstName) + .WithMiddleName(middleName) + .WithLastName(lastName) + .WithDateOfBirth(dateOfBirth) + .WithEmail(email) + .WithNationalInsuranceNumber(nationalInsuranceNumber: nationalInsuranceNumber)); + + var requestBody = CreateJsonContent(new { QTSDate = new DateOnly(1990, 01, 01) }); + var request = new HttpRequestMessage(HttpMethod.Put, $"v3/persons/{nonExistentTrn}/qtls") + { + Content = requestBody + }; + + // Act + var response = await GetHttpClientWithApiKey().SendAsync(request); + + // Assert + Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); + } + + [Fact] + public async Task Put_ValidQTLSDate_ReturnsExpectedResult() + { + // Arrange + var qtlsDate = new DateOnly(2020, 01, 01); + var requestId = Guid.NewGuid().ToString(); + var firstName = Faker.Name.First(); + var middleName = Faker.Name.Middle(); + var lastName = Faker.Name.Last(); + var dateOfBirth = new DateOnly(1990, 01, 01); + var email = Faker.Internet.Email(); + var nationalInsuranceNumber = Faker.Identification.UkNationalInsuranceNumber(); + + var existingContact = await TestData.CreatePerson(p => p + .WithTrn(hasTrn: true) + .WithFirstName(firstName) + .WithMiddleName(middleName) + .WithLastName(lastName) + .WithDateOfBirth(dateOfBirth) + .WithEmail(email) + .WithNationalInsuranceNumber(nationalInsuranceNumber: nationalInsuranceNumber)); + + var requestBody = CreateJsonContent(new { QTSDate = qtlsDate }); + var request = new HttpRequestMessage(HttpMethod.Put, $"v3/persons/{existingContact.Trn}/qtls") + { + Content = requestBody + }; + + // Act + var response = await GetHttpClientWithApiKey().SendAsync(request); + + // Assert + await AssertEx.JsonResponseEquals( + response, + expected: new + { + Trn = existingContact.Trn, + QTSDate = qtlsDate + + }, + expectedStatusCode: 200); + } + + [Fact] + public async Task Put_ClearExistingQTLSDate_ReturnsExpectedResult() + { + // Arrange + var qtlsDate = default(DateOnly?); + var requestId = Guid.NewGuid().ToString(); + var firstName = Faker.Name.First(); + var middleName = Faker.Name.Middle(); + var lastName = Faker.Name.Last(); + var dateOfBirth = new DateOnly(1990, 01, 01); + var email = Faker.Internet.Email(); + var nationalInsuranceNumber = Faker.Identification.UkNationalInsuranceNumber(); + + var existingContact = await TestData.CreatePerson(p => p + .WithTrn(hasTrn: true) + .WithFirstName(firstName) + .WithMiddleName(middleName) + .WithLastName(lastName) + .WithDateOfBirth(dateOfBirth) + .WithEmail(email) + .WithNationalInsuranceNumber(nationalInsuranceNumber: nationalInsuranceNumber) + .WithQTLSDate(new DateOnly(2020, 01, 01))); + + var requestBody = CreateJsonContent(new { QTSDate = qtlsDate }); + var request = new HttpRequestMessage(HttpMethod.Put, $"v3/teachers/{existingContact.Trn}/qtls") + { + Content = requestBody + }; + + // Act + var response = await GetHttpClientWithApiKey().SendAsync(request); + + // Assert + await AssertEx.JsonResponseEquals( + response, + expected: new + { + existingContact.Trn, + QTSDate = qtlsDate + + }, + expectedStatusCode: 200); + } + + [Fact] + public async Task Put_ValidQTLSWithNoQTS_SetsQTSDate() + { + // Arrange + var qtlsDate = new DateOnly(2010, 01, 01); + var requestId = Guid.NewGuid().ToString(); + var firstName = Faker.Name.First(); + var middleName = Faker.Name.Middle(); + var lastName = Faker.Name.Last(); + var dateOfBirth = new DateOnly(1990, 01, 01); + var email = Faker.Internet.Email(); + var nationalInsuranceNumber = Faker.Identification.UkNationalInsuranceNumber(); + var existingContact = await TestData.CreatePerson(p => p + .WithTrn(hasTrn: true) + .WithFirstName(firstName) + .WithMiddleName(middleName) + .WithLastName(lastName) + .WithDateOfBirth(dateOfBirth) + .WithEmail(email) + .WithNationalInsuranceNumber(nationalInsuranceNumber: nationalInsuranceNumber) + .WithQTLSDate(qtlsDate)); + + var requestBody = CreateJsonContent(new { QTSDate = qtlsDate }); + var request = new HttpRequestMessage(HttpMethod.Put, $"v3/persons/{existingContact.Trn}/qtls") + { + Content = requestBody + }; + + // Act + var response = await GetHttpClientWithApiKey().SendAsync(request); + var contact = XrmFakedContext.CreateQuery().FirstOrDefault(x => x.ContactId == existingContact.PersonId); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Equal(qtlsDate, contact!.dfeta_QTSDate.ToDateOnlyWithDqtBstFix(isLocalTime: true)); + } + + [Fact] + public async Task Put_RemoveQTLSDate_SetsQTSDate() + { + // Arrange + var qtlsDate = new DateOnly(2010, 01, 01); + var qtsDate = new DateOnly(2008, 01, 01); + var requestId = Guid.NewGuid().ToString(); + var firstName = Faker.Name.First(); + var middleName = Faker.Name.Middle(); + var lastName = Faker.Name.Last(); + var dateOfBirth = new DateOnly(1990, 01, 01); + var email = Faker.Internet.Email(); + var nationalInsuranceNumber = Faker.Identification.UkNationalInsuranceNumber(); + var existingContact = await TestData.CreatePerson(p => p + .WithTrn(hasTrn: true) + .WithFirstName(firstName) + .WithMiddleName(middleName) + .WithLastName(lastName) + .WithDateOfBirth(dateOfBirth) + .WithEmail(email) + .WithNationalInsuranceNumber(nationalInsuranceNumber: nationalInsuranceNumber) + .WithQts(qtsDate) + .WithQTLSDate(qtlsDate)); + + var requestBody = CreateJsonContent(new { QTSDate = default(DateOnly?) }); + var request = new HttpRequestMessage(HttpMethod.Put, $"v3/persons/{existingContact.Trn}/qtls") + { + Content = requestBody + }; + + // Act + var response = await GetHttpClientWithApiKey().SendAsync(request); + var contact = XrmFakedContext.CreateQuery().FirstOrDefault(x => x.ContactId == existingContact.PersonId); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Equal(qtsDate, contact!.dfeta_QTSDate.ToDateOnlyWithDqtBstFix(isLocalTime: true)); + } + + [Fact] + public async Task Put_QTLSDateAfterAllQTSDates_SetsQTSDate() + { + // Arrange + var qtlsDate = new DateOnly(2020, 01, 01); + var qtsDate1 = new DateOnly(2010, 01, 01); + var earliestQTSDate = new DateOnly(2008, 01, 01); + var requestId = Guid.NewGuid().ToString(); + var firstName = Faker.Name.First(); + var middleName = Faker.Name.Middle(); + var lastName = Faker.Name.Last(); + var dateOfBirth = new DateOnly(1990, 01, 01); + var email = Faker.Internet.Email(); + var nationalInsuranceNumber = Faker.Identification.UkNationalInsuranceNumber(); + var existingContact = await TestData.CreatePerson(p => p + .WithTrn(hasTrn: true) + .WithFirstName(firstName) + .WithMiddleName(middleName) + .WithLastName(lastName) + .WithDateOfBirth(dateOfBirth) + .WithEmail(email) + .WithNationalInsuranceNumber(nationalInsuranceNumber: nationalInsuranceNumber) + .WithQts(qtsDate1) + .WithQts(earliestQTSDate)); + + + var requestBody = CreateJsonContent(new { QTSDate = qtlsDate }); + var request = new HttpRequestMessage(HttpMethod.Put, $"v3/persons/{existingContact.Trn}/qtls") + { + Content = requestBody + }; + + // Act + var response = await GetHttpClientWithApiKey().SendAsync(request); + var contact = XrmFakedContext.CreateQuery().FirstOrDefault(x => x.ContactId == existingContact.PersonId); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Equal(earliestQTSDate, contact!.dfeta_QTSDate.ToDateOnlyWithDqtBstFix(isLocalTime: true)); + } + + [Fact] + public async Task Put_QTLSDateBeforeAllQTSDates_SetsQTSDate() + { + // Arrange + var earliestQTLSDate = new DateOnly(2001, 01, 01); + var qtsDate1 = new DateOnly(2010, 01, 01); + var qtsDate2 = new DateOnly(2008, 01, 01); + var requestId = Guid.NewGuid().ToString(); + var firstName = Faker.Name.First(); + var middleName = Faker.Name.Middle(); + var lastName = Faker.Name.Last(); + var dateOfBirth = new DateOnly(1990, 01, 01); + var email = Faker.Internet.Email(); + var nationalInsuranceNumber = Faker.Identification.UkNationalInsuranceNumber(); + var existingContact = await TestData.CreatePerson(p => p + .WithTrn(hasTrn: true) + .WithFirstName(firstName) + .WithMiddleName(middleName) + .WithLastName(lastName) + .WithDateOfBirth(dateOfBirth) + .WithEmail(email) + .WithNationalInsuranceNumber(nationalInsuranceNumber: nationalInsuranceNumber) + .WithQts(qtsDate1) + .WithQts(qtsDate2)); + + + var requestBody = CreateJsonContent(new { QTSDate = earliestQTLSDate }); + var request = new HttpRequestMessage(HttpMethod.Put, $"v3/persons/{existingContact.Trn}/qtls") + { + Content = requestBody + }; + + // Act + var response = await GetHttpClientWithApiKey().SendAsync(request); + var contact = XrmFakedContext.CreateQuery().FirstOrDefault(x => x.ContactId == existingContact.PersonId); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Equal(earliestQTLSDate, contact!.dfeta_QTSDate.ToDateOnlyWithDqtBstFix(isLocalTime: true)); + } + + [Theory] + [InlineData("01/01/1999", "02/02/2023", "02/02/2022", "02/02/2022")] //QTS After QTLS + [InlineData("01/01/2024", "02/02/2001", "02/02/2004", "02/02/2001")] //QTLS After QTS + public async Task Put_RemoveQTLS_SetsQTSDateToEarliestQTSRegistrationDate(string qtls, string qts1, string qts2, string expectedQTS) + { + // Arrange + var qtlsDate = DateOnly.Parse(qtls); + var qtsDate1 = DateOnly.Parse(qts1); + var qtsDate2 = DateOnly.Parse(qts2); + var expectedQTSDate = DateOnly.Parse(expectedQTS); + var requestId = Guid.NewGuid().ToString(); + var firstName = Faker.Name.First(); + var middleName = Faker.Name.Middle(); + var lastName = Faker.Name.Last(); + var dateOfBirth = new DateOnly(1990, 01, 01); + var email = Faker.Internet.Email(); + var nationalInsuranceNumber = Faker.Identification.UkNationalInsuranceNumber(); + var existingContact = await TestData.CreatePerson(p => p + .WithTrn(hasTrn: true) + .WithFirstName(firstName) + .WithMiddleName(middleName) + .WithLastName(lastName) + .WithDateOfBirth(dateOfBirth) + .WithEmail(email) + .WithNationalInsuranceNumber(nationalInsuranceNumber: nationalInsuranceNumber) + .WithQts(qtsDate1) + .WithQts(qtsDate2) + .WithQTLSDate(qtlsDate)); + + var requestBody = CreateJsonContent(new { QTSDate = default(DateOnly?) }); + var request = new HttpRequestMessage(HttpMethod.Put, $"v3/persons/{existingContact.Trn}/qtls") + { + Content = requestBody + }; + + // Act + var response = await GetHttpClientWithApiKey().SendAsync(request); + var contact = XrmFakedContext.CreateQuery().FirstOrDefault(x => x.ContactId == existingContact.PersonId); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Equal(expectedQTSDate, contact!.dfeta_QTSDate.ToDateOnlyWithDqtBstFix(isLocalTime: true)); + } + + [Theory] + [InlineData("01/01/2001", null, dfeta_InductionStatus.InProgress, null, null, "01/01/2001")] + [InlineData("01/01/2001", null, dfeta_InductionStatus.InductionExtended, null, null, "01/01/2001")] + [InlineData("01/01/2001", null, dfeta_InductionStatus.Fail, null, null, "01/01/2001")] + [InlineData(null, "01/01/2021", dfeta_InductionStatus.Fail, null, null, null)] + [InlineData("01/01/2001", "01/05/2003", dfeta_InductionStatus.FailedinWales, null, null, "01/01/2001")] + [InlineData(null, "01/01/1998", dfeta_InductionStatus.FailedinWales, null, null, null)] + [InlineData("01/01/2001", "02/02/1999", dfeta_InductionStatus.Exempt, dfeta_InductionExemptionReason.Exempt, null, "01/01/2001")] + public async Task Put_RejectQTLSUpdate_ReturnsAcceptedAndCreatesTask(string? existingQtls, string? incomingQtls, dfeta_InductionStatus inductionStatus, dfeta_InductionExemptionReason? reason, string? qts, string? expextedQTS) + { + // Arrange + var existingQtlsDate = string.IsNullOrEmpty(existingQtls) ? default(DateOnly?) : DateOnly.Parse(existingQtls); + var incomingQtlsDate = string.IsNullOrEmpty(incomingQtls) ? default(DateOnly?) : DateOnly.Parse(incomingQtls); + var qtsDate1 = string.IsNullOrEmpty(qts) ? default(DateOnly?) : DateOnly.Parse(qts); + var expextedQTSDate = string.IsNullOrEmpty(expextedQTS) ? default(DateOnly?) : DateOnly.Parse(expextedQTS); + var requestId = Guid.NewGuid().ToString(); + var firstName = Faker.Name.First(); + var middleName = Faker.Name.Middle(); + var lastName = Faker.Name.Last(); + var dateOfBirth = new DateOnly(1990, 01, 01); + var email = Faker.Internet.Email(); + var nationalInsuranceNumber = Faker.Identification.UkNationalInsuranceNumber(); + var existingContact = await TestData.CreatePerson(p => p + .WithTrn(hasTrn: true) + .WithFirstName(firstName) + .WithMiddleName(middleName) + .WithLastName(lastName) + .WithDateOfBirth(dateOfBirth) + .WithEmail(email) + .WithNationalInsuranceNumber(nationalInsuranceNumber: nationalInsuranceNumber) + .WithQts(qtsDate1) + .WithQTLSDate(existingQtlsDate) + .WithInduction(inductionStatus: inductionStatus, reason)); + + var requestBody = CreateJsonContent(new { QTSDate = incomingQtlsDate }); + var request = new HttpRequestMessage(HttpMethod.Put, $"v3/persons/{existingContact.Trn}/qtls") + { + Content = requestBody + }; + + // Act + var response = await GetHttpClientWithApiKey().SendAsync(request); + var task = XrmFakedContext.CreateQuery().FirstOrDefault(); + var contact = XrmFakedContext.CreateQuery().FirstOrDefault(x => x.ContactId == existingContact.PersonId); + + // Assert + Assert.NotNull(task); + Assert.Equal("Unable to set QTLSDate", task.Category); + Assert.Contains("Unable to set QTLSDate", task.Description); + Assert.Equal("Notification for SET QTLS data collections team", task.Subject); + Assert.Contains($"Unable to set QTLSDate", task.Description); + Assert.Equal(HttpStatusCode.Accepted, response.StatusCode); + Assert.Equal(expextedQTSDate, contact!.dfeta_QTSDate.ToDateOnlyWithDqtBstFix(isLocalTime: true)); + } + + [Theory] + //[InlineData(dfeta_InductionStatus.Exempt, dfeta_InductionExemptionReason.Exempt, "01/01/2021", dfeta_InductionStatus.Exempt, HttpStatusCode.OK)] + //[InlineData(dfeta_InductionStatus.Exempt, dfeta_InductionExemptionReason.Exempt, null, dfeta_InductionStatus.Exempt, HttpStatusCode.OK)] + //[InlineData(dfeta_InductionStatus.Fail, null, "01/01/2021", dfeta_InductionStatus.Fail, HttpStatusCode.Accepted)] + //[InlineData(dfeta_InductionStatus.Fail, null, null, dfeta_InductionStatus.Fail, HttpStatusCode.Accepted)] + //[InlineData(dfeta_InductionStatus.InProgress, null, "01/01/2021", dfeta_InductionStatus.Exempt, HttpStatusCode.Accepted)] + //[InlineData(dfeta_InductionStatus.InProgress, null, null, dfeta_InductionStatus.InProgress, HttpStatusCode.OK)] + //[InlineData(dfeta_InductionStatus.NotYetCompleted, null, "01/01/2021", dfeta_InductionStatus.Exempt, HttpStatusCode.OK)] + //[InlineData(dfeta_InductionStatus.NotYetCompleted, null, null, dfeta_InductionStatus.NotYetCompleted, HttpStatusCode.OK)] + [InlineData(dfeta_InductionStatus.InductionExtended, null, "01/01/2021", dfeta_InductionStatus.Exempt, HttpStatusCode.OK)] + [InlineData(dfeta_InductionStatus.InductionExtended, null, null, dfeta_InductionStatus.InductionExtended, HttpStatusCode.OK)] + //[InlineData(dfeta_InductionStatus.Pass, null, "01/01/2021", dfeta_InductionStatus.Pass, HttpStatusCode.OK)] + //[InlineData(dfeta_InductionStatus.Pass, null, null, dfeta_InductionStatus.Pass, HttpStatusCode.OK)] + //[InlineData(dfeta_InductionStatus.PassedinWales, null, "01/01/2021", dfeta_InductionStatus.PassedinWales, HttpStatusCode.OK)] + //[InlineData(dfeta_InductionStatus.PassedinWales, null, null, dfeta_InductionStatus.PassedinWales, HttpStatusCode.OK)] + //[InlineData(dfeta_InductionStatus.RequiredtoComplete, null, "01/01/2021", dfeta_InductionStatus.Exempt, HttpStatusCode.OK)] + //[InlineData(dfeta_InductionStatus.RequiredtoComplete, null, null, dfeta_InductionStatus.RequiredtoComplete, HttpStatusCode.OK)] + public async Task Put_ValidQTLSDate_SetsInductionStatus(dfeta_InductionStatus existingInductionStatus, dfeta_InductionExemptionReason? existingInductionExemptionReason, string? incomingQtls, dfeta_InductionStatus expectetInductionStatus, HttpStatusCode expectedHttpStatus) + { + // Arrange + var qtlsDate = !string.IsNullOrEmpty(incomingQtls) ? DateOnly.Parse(incomingQtls) : default(DateOnly?); + var requestId = Guid.NewGuid().ToString(); + var firstName = Faker.Name.First(); + var middleName = Faker.Name.Middle(); + var lastName = Faker.Name.Last(); + var dateOfBirth = new DateOnly(1990, 01, 01); + var email = Faker.Internet.Email(); + var nationalInsuranceNumber = Faker.Identification.UkNationalInsuranceNumber(); + var existingContact = await TestData.CreatePerson(p => p + .WithTrn(hasTrn: true) + .WithFirstName(firstName) + .WithMiddleName(middleName) + .WithLastName(lastName) + .WithDateOfBirth(dateOfBirth) + .WithEmail(email) + .WithNationalInsuranceNumber(nationalInsuranceNumber: nationalInsuranceNumber) + .WithInduction(inductionStatus: existingInductionStatus, inductionExemptionReason: existingInductionExemptionReason)); + + var requestBody = CreateJsonContent(new { QTSDate = qtlsDate }); + var request = new HttpRequestMessage(HttpMethod.Put, $"v3/persons/{existingContact.Trn}/qtls") + { + Content = requestBody + }; + + // Act + var response = await GetHttpClientWithApiKey().SendAsync(request); + var contact = XrmFakedContext.CreateQuery().FirstOrDefault(x => x.ContactId == existingContact.PersonId); + + // Assert + Assert.Equal(expectedHttpStatus, response.StatusCode); + Assert.Equal(expectetInductionStatus, contact!.dfeta_InductionStatus); + } + + [Theory] + [InlineData(null, "01/01/2021", dfeta_InductionStatus.Exempt, HttpStatusCode.OK)] + [InlineData("01/01/2021", null, null, HttpStatusCode.OK)] + public async Task Put_QTLSDate_SetsInductionWhenExistingInductionStatusEmpty(string? existingQtls, string? incomingQtls, dfeta_InductionStatus? expectetInductionStatus, HttpStatusCode expectedHttpStatus) + { + // Arrange + var existingqtlsDate = !string.IsNullOrEmpty(existingQtls) ? DateOnly.Parse(existingQtls) : default(DateOnly?); + var qtlsDate = !string.IsNullOrEmpty(incomingQtls) ? DateOnly.Parse(incomingQtls) : default(DateOnly?); + var requestId = Guid.NewGuid().ToString(); + var firstName = Faker.Name.First(); + var middleName = Faker.Name.Middle(); + var lastName = Faker.Name.Last(); + var dateOfBirth = new DateOnly(1990, 01, 01); + var email = Faker.Internet.Email(); + var nationalInsuranceNumber = Faker.Identification.UkNationalInsuranceNumber(); + var existingContact = await TestData.CreatePerson(p => p + .WithTrn(hasTrn: true) + .WithFirstName(firstName) + .WithMiddleName(middleName) + .WithLastName(lastName) + .WithDateOfBirth(dateOfBirth) + .WithEmail(email) + .WithNationalInsuranceNumber(nationalInsuranceNumber: nationalInsuranceNumber) + .WithQTLSDate(existingqtlsDate)); + + var requestBody = CreateJsonContent(new { QTSDate = qtlsDate }); + var request = new HttpRequestMessage(HttpMethod.Put, $"v3/persons/{existingContact.Trn}/qtls") + { + Content = requestBody + }; + + // Act + var response = await GetHttpClientWithApiKey().SendAsync(request); + var contact = XrmFakedContext.CreateQuery().FirstOrDefault(x => x.ContactId == existingContact.PersonId); + var task = XrmFakedContext.CreateQuery().FirstOrDefault(); + + // Assert + Assert.Null(task); + Assert.Equal(expectedHttpStatus, response.StatusCode); + Assert.Equal(expectetInductionStatus, contact!.dfeta_InductionStatus); + } +} diff --git a/TeachingRecordSystem/tests/TeachingRecordSystem.Core.Dqt.CrmIntegrationTests/QueryTests/CreateReviewTaskTests.cs b/TeachingRecordSystem/tests/TeachingRecordSystem.Core.Dqt.CrmIntegrationTests/QueryTests/CreateReviewTaskTests.cs new file mode 100644 index 0000000000..fa71a97075 --- /dev/null +++ b/TeachingRecordSystem/tests/TeachingRecordSystem.Core.Dqt.CrmIntegrationTests/QueryTests/CreateReviewTaskTests.cs @@ -0,0 +1,44 @@ +namespace TeachingRecordSystem.Core.Dqt.CrmIntegrationTests.QueryTests; +public class CreateReviewTaskTests : IAsyncLifetime +{ + private readonly CrmClientFixture.TestDataScope _dataScope; + private readonly CrmQueryDispatcher _crmQueryDispatcher; + + public CreateReviewTaskTests(CrmClientFixture crmClientFixture) + { + _dataScope = crmClientFixture.CreateTestDataScope(); + _crmQueryDispatcher = crmClientFixture.CreateQueryDispatcher(); + } + + public Task InitializeAsync() => Task.CompletedTask; + + public async Task DisposeAsync() => await _dataScope.DisposeAsync(); + + [Fact] + public async Task QueryExecutesSuccessfully() + { + // Arrange + var createPersonResult = await _dataScope.TestData.CreatePerson(); + var category = "Category"; + var description = "Description"; + var subject = "Subject"; + + // Act + var crmTaskId = await _crmQueryDispatcher.ExecuteQuery(new CreateReviewTaskQuery() + { + TeacherId = createPersonResult.PersonId, + Category = category, + Subject = subject, + Description = description + }); + + // Assert + using var ctx = new DqtCrmServiceContext(_dataScope.OrganizationService); + + var createdTask = ctx.TaskSet.SingleOrDefault(s => s.GetAttributeValue(Models.Task.Fields.Id) == crmTaskId); + Assert.NotNull(createdTask); + Assert.Equal(category, createdTask.Category); + Assert.Equal(subject, createdTask.Subject); + Assert.Equal(description, createdTask.Description); + } +} diff --git a/TeachingRecordSystem/tests/TeachingRecordSystem.Core.Dqt.CrmIntegrationTests/QueryTests/SetQTLSDateTests.cs b/TeachingRecordSystem/tests/TeachingRecordSystem.Core.Dqt.CrmIntegrationTests/QueryTests/SetQTLSDateTests.cs new file mode 100644 index 0000000000..34a5cc481c --- /dev/null +++ b/TeachingRecordSystem/tests/TeachingRecordSystem.Core.Dqt.CrmIntegrationTests/QueryTests/SetQTLSDateTests.cs @@ -0,0 +1,26 @@ +namespace TeachingRecordSystem.Core.Dqt.CrmIntegrationTests.QueryTests; +public class SetQTLSDateTests : IAsyncLifetime +{ + private readonly CrmClientFixture.TestDataScope _dataScope; + private readonly CrmQueryDispatcher _crmQueryDispatcher; + + public SetQTLSDateTests(CrmClientFixture crmClientFixture) + { + _dataScope = crmClientFixture.CreateTestDataScope(); + _crmQueryDispatcher = crmClientFixture.CreateQueryDispatcher(); + } + + public Task InitializeAsync() => Task.CompletedTask; + + public async Task DisposeAsync() => await _dataScope.DisposeAsync(); + + [Fact] + public async Task QueryExecutesSuccessfully() + { + // Arrange + var createPersonResult = await _dataScope.TestData.CreatePerson(); + + // Act + _ = await _crmQueryDispatcher.ExecuteQuery(new SetQTLSDateQuery(contactId: createPersonResult.ContactId, qtlsDate: new DateOnly(2021, 01, 01))); + } +} diff --git a/TeachingRecordSystem/tests/TeachingRecordSystem.TestCommon/Infrastructure/FakeXrmEasy/Plugins/QtsRegistrationUpdatedPlugin.cs b/TeachingRecordSystem/tests/TeachingRecordSystem.TestCommon/Infrastructure/FakeXrmEasy/Plugins/QtsRegistrationUpdatedPlugin.cs index d19a02901c..810a1e9a0f 100644 --- a/TeachingRecordSystem/tests/TeachingRecordSystem.TestCommon/Infrastructure/FakeXrmEasy/Plugins/QtsRegistrationUpdatedPlugin.cs +++ b/TeachingRecordSystem/tests/TeachingRecordSystem.TestCommon/Infrastructure/FakeXrmEasy/Plugins/QtsRegistrationUpdatedPlugin.cs @@ -7,6 +7,7 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Xrm.Sdk; using Microsoft.Xrm.Sdk.Messages; +using Microsoft.Xrm.Sdk.Query; using TeachingRecordSystem.Core.Dqt.Models; namespace TeachingRecordSystem.TestCommon.Infrastructure.FakeXrmEasy.Plugins; @@ -27,13 +28,24 @@ internal static void Register(IXrmFakedContext context) dfeta_qtsregistration.Fields.dfeta_QTSDate, dfeta_qtsregistration.Fields.dfeta_EYTSDate }); + + var preImageContact = new PluginImageDefinition( + "PostContactImage", + ProcessingStepImageType.PostImage, + new string[] + { + Contact.Fields.ContactId, + Contact.Fields.dfeta_qtlsdate + }); + context.RegisterPluginStep( new PluginStepDefinition() { EntityLogicalName = dfeta_qtsregistration.EntityLogicalName, MessageName = "Delete", Stage = ProcessingStepStage.Postoperation, - ImagesDefinitions = new List() { preImageDefinition } + ImagesDefinitions = new List() { preImageDefinition }, + Rank = 1 }); context.RegisterPluginStep( new PluginStepDefinition() @@ -41,7 +53,39 @@ internal static void Register(IXrmFakedContext context) EntityLogicalName = dfeta_qtsregistration.EntityLogicalName, MessageName = "Update", Stage = ProcessingStepStage.Postoperation, - ImagesDefinitions = new List() { preImageDefinition } + ImagesDefinitions = new List() { preImageDefinition }, + Rank = 5, + FilteringAttributes = new string[] + { + dfeta_qtsregistration.Fields.dfeta_PersonId, + dfeta_qtsregistration.Fields.dfeta_QTSDate, + dfeta_qtsregistration.Fields.dfeta_EYTSDate, + } + }); + context.RegisterPluginStep( + new PluginStepDefinition() + { + EntityLogicalName = Contact.EntityLogicalName, + MessageName = "Update", + Stage = ProcessingStepStage.Postoperation, + ImagesDefinitions = new List() { preImageContact }, + FilteringAttributes = new string[] + { + Contact.Fields.dfeta_qtlsdate + } + }); + + context.RegisterPluginStep( + new PluginStepDefinition() + { + EntityLogicalName = Contact.EntityLogicalName, + MessageName = "Create", + Stage = ProcessingStepStage.Postoperation, + ImagesDefinitions = new List() { preImageContact }, + FilteringAttributes = new string[] + { + Contact.Fields.dfeta_qtlsdate + } }); } @@ -50,7 +94,17 @@ public void Execute(IServiceProvider serviceProvider) var context = serviceProvider.GetRequiredService(); var orgService = serviceProvider.GetRequiredService(); - if (context.PreEntityImages.TryGetValue("preImage", out var preImage)) + if (context.PostEntityImages.ContainsKey("PostContactImage")) + { + //when triggered from contact update + Entity entity = (Entity)context.InputParameters["Target"]; + var qtsDates = this.GetContactQTSDates(orgService, entity.Id).ToList(); + var qtlsDate = entity.GetAttributeValue(Contact.Fields.dfeta_qtlsdate); + qtsDates.Add(qtlsDate); + var qtsDate = qtsDates.Where(x => x != null).OrderBy(date => date).FirstOrDefault(); + UpdatePersonQtsDate(orgService, entity.Id, qtsDate); + } + else if (context.PreEntityImages.TryGetValue("preImage", out var preImage)) { if (preImage.TryGetAttributeValue(dfeta_qtsregistration.Fields.dfeta_PersonId, out var personIdEntityReference)) { @@ -76,7 +130,12 @@ public void Execute(IServiceProvider serviceProvider) if (qtsDate.HasValue) { - UpdatePersonQtsDate(orgService, personId, qtsDate); + //qtsdate is the earliest of all qtsregistrations & contact.qtlsDate + var qtsDates = this.GetContactQTSDates(orgService, personId).ToList(); + var qtlsDate = this.GetContactQTLSDate(orgService, personId); + qtsDates.Add(qtlsDate); + var earliestQTSDate = qtsDates.Where(x => x != null).OrderBy(date => date).FirstOrDefault(); + UpdatePersonQtsDate(orgService, personId, earliestQTSDate); } if (eytsDate.HasValue) @@ -89,6 +148,39 @@ public void Execute(IServiceProvider serviceProvider) } } + public DateTime? GetContactQTLSDate(IOrganizationService orgService, Guid personId) + { + var entity = orgService.Retrieve(Contact.EntityLogicalName, personId, new ColumnSet(new string[] { + Contact.Fields.dfeta_qtlsdate + })); + + if (entity != null) + { + var qtlsDate = entity.GetAttributeValue(Contact.Fields.dfeta_qtlsdate); + return qtlsDate; + } + return null; + } + + private DateTime?[] GetContactQTSDates(IOrganizationService orgService, Guid? personId) + { + QueryExpression query = new QueryExpression(); + query.EntityName = dfeta_qtsregistration.EntityLogicalName; + query.ColumnSet = new ColumnSet(new string[] { + dfeta_qtsregistration.Fields.dfeta_QTSDate + }); + query.Criteria = new FilterExpression(LogicalOperator.And); + query.Criteria.AddCondition(dfeta_qtsregistration.Fields.dfeta_PersonId, ConditionOperator.Equal, personId); + query.Criteria.AddCondition(dfeta_qtsregistration.Fields.StateCode, ConditionOperator.Equal, 0/*Active*/); + + EntityCollection collection = orgService.RetrieveMultiple(query); + if (collection != null && collection.Entities != null && collection.Entities.Count > 0) + { + return collection.Entities.Select(x => x.Contains(dfeta_qtsregistration.Fields.dfeta_QTSDate) ? x[dfeta_qtsregistration.Fields.dfeta_QTSDate] as DateTime? : null).ToArray(); + } + return Array.Empty(); + } + private void UpdatePersonQtsDate(IOrganizationService orgService, Guid personId, DateTime? qtsDate) { orgService.Execute(new UpdateRequest() @@ -97,7 +189,7 @@ private void UpdatePersonQtsDate(IOrganizationService orgService, Guid personId, { Id = personId, dfeta_QTSDate = qtsDate - } + }, }); } diff --git a/TeachingRecordSystem/tests/TeachingRecordSystem.TestCommon/Infrastructure/FakeXrmEasy/Plugins/UpdateInductionStatusPlugin.cs b/TeachingRecordSystem/tests/TeachingRecordSystem.TestCommon/Infrastructure/FakeXrmEasy/Plugins/UpdateInductionStatusPlugin.cs new file mode 100644 index 0000000000..17de12c5f9 --- /dev/null +++ b/TeachingRecordSystem/tests/TeachingRecordSystem.TestCommon/Infrastructure/FakeXrmEasy/Plugins/UpdateInductionStatusPlugin.cs @@ -0,0 +1,336 @@ +using FakeXrmEasy.Abstractions; +using FakeXrmEasy.Abstractions.Plugins.Enums; +using FakeXrmEasy.Pipeline; +using FakeXrmEasy.Plugins.Definitions; +using FakeXrmEasy.Plugins.PluginImages; +using FakeXrmEasy.Plugins.PluginSteps; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Xrm.Sdk; +using Microsoft.Xrm.Sdk.Query; +using TeachingRecordSystem.Core.Dqt.Models; + +namespace TeachingRecordSystem.TestCommon.Infrastructure.FakeXrmEasy.Plugins; + +public class UpdateInductionStatusPlugin : IPlugin +{ + internal static void Register(IXrmFakedContext context) + { + var inductionStatusPostImage = new PluginImageDefinition( + "PostInductionImage", + ProcessingStepImageType.PostImage, + new string[] + { + dfeta_induction.Fields.dfeta_PersonId, + dfeta_induction.Fields.dfeta_InductionStatus, + }); + + var inductionStatusPreImage = new PluginImageDefinition( + "PreInductionImage", + ProcessingStepImageType.PreImage, + new string[] + { + dfeta_induction.Fields.dfeta_PersonId, + dfeta_induction.Fields.dfeta_InductionStatus, + }); + + var contactPostImage = new PluginImageDefinition( + "PostImageContactImage", + ProcessingStepImageType.PostImage, + new string[] + { + Contact.Fields.ContactId, + Contact.Fields.dfeta_qtlsdate, + }); + + context.RegisterPluginStep( + new PluginStepDefinition() + { + EntityLogicalName = dfeta_induction.EntityLogicalName, + MessageName = "Create", + Stage = ProcessingStepStage.Postoperation, + ImagesDefinitions = new List() { inductionStatusPostImage }, + FilteringAttributes = new string[] + { + dfeta_induction.Fields.dfeta_InductionStatus + } + }); + context.RegisterPluginStep( + new PluginStepDefinition() + { + EntityLogicalName = dfeta_induction.EntityLogicalName, + MessageName = "Delete", + Stage = ProcessingStepStage.Postoperation, + ImagesDefinitions = new List() { inductionStatusPreImage } + }); + + context.RegisterPluginStep( + new PluginStepDefinition() + { + EntityLogicalName = Contact.EntityLogicalName, + MessageName = "Update", + Stage = ProcessingStepStage.Postoperation, + ImagesDefinitions = new List() { contactPostImage }, + FilteringAttributes = new string[] + { + Contact.Fields.dfeta_qtlsdate + } + }); + + context.RegisterPluginStep( + new PluginStepDefinition() + { + EntityLogicalName = dfeta_induction.EntityLogicalName, + MessageName = "Update", + Stage = ProcessingStepStage.Postoperation, + ImagesDefinitions = new List() { inductionStatusPreImage }, + FilteringAttributes = new string[] + { + dfeta_induction.Fields.dfeta_InductionStatus, + dfeta_induction.Fields.StateCode + } + }); + } + + + public void Execute(IServiceProvider serviceProvider) + { + var context = serviceProvider.GetRequiredService(); + var orgService = serviceProvider.GetRequiredService(); + + if (context.PreEntityImages != null && context.PreEntityImages.Contains("PreInductionImage")) + { + Entity preImageEnt = context.PreEntityImages["PreInductionImage"]; + if (preImageEnt.Contains(dfeta_induction.Fields.dfeta_PersonId)) + { + if (preImageEnt[dfeta_induction.Fields.dfeta_PersonId] != null) + { + Guid personId = ((EntityReference)preImageEnt[dfeta_induction.Fields.dfeta_PersonId]).Id; + if (context.MessageName == "Delete") + { + //triggered from inductionstatus deleted - needs to recompute induction status + var qtlsDate = GetContactQTLSDate(orgService, personId); + OptionSetValue? derivedInductionStatus = CalculateInductionStation(qtlsDate, null); + if (derivedInductionStatus != null) + { + SetIndutionStatus(orgService, personId, derivedInductionStatus?.Value); + } + else + { + SetIndutionStatus(orgService, personId, null); + } + return; + } + + if (context.InputParameters.Contains("Target") && context.InputParameters["Target"] is Entity) + { + Entity entity = (Entity)context.InputParameters["Target"]; + if (entity.Attributes.Contains("statecode")) + { + var state = entity.GetAttributeValue("statecode"); + if (state?.Value == 1) + { + SetIndutionStatus(orgService, personId, null); + } + } + else + { + //triggered from update to InductionStatus.InductionStatus + var qtlsDate = GetContactQTLSDate(orgService, personId); + var existingInductionStatus = entity.GetAttributeValue(dfeta_induction.Fields.dfeta_InductionStatus); + if (existingInductionStatus != null) + { + var derivedInductionStatus = this.CalculateInductionStation(qtlsDate, existingInductionStatus); + SetIndutionStatus(orgService, personId, derivedInductionStatus?.Value); + } + } + return; + } + } + } + } + else if (context.PostEntityImages != null && context.PostEntityImages.Contains("PostInductionImage")) + { + //triggered on newly created inductionstatus + Entity postImageEnt = context.PostEntityImages["PostInductionImage"]; + Guid personId = ((EntityReference)postImageEnt[dfeta_induction.Fields.dfeta_PersonId]).Id; + var qtlsDate = GetContactQTLSDate(orgService, personId); + var existingInductionStatus = postImageEnt.GetAttributeValue(dfeta_induction.Fields.dfeta_InductionStatus); + var derivedInductionStatus = CalculateInductionStation(qtlsDate, existingInductionStatus); + SetIndutionStatus(orgService, personId, derivedInductionStatus?.Value); + return; + } + else if (context.PostEntityImages != null && context.PostEntityImages.Contains("PostImageContactImage")) + { + //Triggered on updates to contact.dfeta_qtlsdate + Entity postImageEnt = context.PostEntityImages["PostImageContactImage"]; + if (postImageEnt.Contains(Contact.Fields.Id)) + { + var personId = postImageEnt.GetAttributeValue(Contact.Fields.Id); + var qtlsDate = postImageEnt.GetAttributeValue(Contact.Fields.dfeta_qtlsdate); + var inductionStatus = this.GetContactInductionStatus(orgService, personId); + OptionSetValue? derivedInductionStatus = this.CalculateInductionStation(qtlsDate, inductionStatus); + if (derivedInductionStatus != null) + { + SetIndutionStatus(orgService, personId, derivedInductionStatus?.Value); + } + else + { + SetIndutionStatus(orgService, personId, null); + } + } + return; + } + + // Entity preImageEnt = context.PreEntityImages[preImageAlias]; + // if (preImageEnt.Contains(Induction.Attributes.PersonId)) + // { + // if (preImageEnt[Induction.Attributes.PersonId] != null) + // { + // Guid personId = ((EntityReference)preImageEnt[Induction.Attributes.PersonId]).Id; + // if (context.MessageName == "Delete") + // { + // //triggered from inductionstatus deleted - needs to recompute induction status + // var qtlsDate = this.GetContactQTLSDate(orgService, personId); + // OptionSetValue derivedInductionStatus = this.CalculateInductionStation(qtlsDate, null); + // if (derivedInductionStatus != null) + // Person.SetIndutionStatus(orgService, personId, derivedInductionStatus?.Value); + // else + // Person.SetIndutionStatus(orgService, personId, null); + // return; + // } + + // if (context.InputParameters.Contains("Target") && context.InputParameters["Target"] is Entity) + // { + // Entity entity = (Entity)context.InputParameters["Target"]; + // if (entity.Attributes.Contains("statecode")) + // { + // OptionSetValue state = entity.GetAttributeValue("statecode"); + // if (state.Value == 1) + // Person.SetIndutionStatus(orgService, personId, null); + // } + // else + // { + // //triggered from update to InductionStatus.InductionStatus + // var qtlsDate = this.GetContactQTLSDate(orgService, personId); + // var existingInductionStatus = entity.GetAttributeValue(Induction.Attributes.InductionStatus); + // if (existingInductionStatus != null) + // { + // OptionSetValue derivedInductionStatus = this.CalculateInductionStation(qtlsDate, existingInductionStatus); + // Person.SetIndutionStatus(orgService, personId, derivedInductionStatus.Value); + // } + // } + // return; + // } + // } + // } + //} + //else if (context.PostEntityImages != null && context.PostEntityImages.Contains(postImageInductionAlias)) + //{ + // //triggered on newly created inductionstatus + // Entity postImageEnt = context.PostEntityImages[postImageInductionAlias]; + // Guid personId = ((EntityReference)postImageEnt[Induction.Attributes.PersonId]).Id; + // var qtlsDate = this.GetContactQTLSDate(orgService, personId); + // var existingInductionStatus = postImageEnt.GetAttributeValue(Induction.Attributes.InductionStatus); + // if (existingInductionStatus != null) + // { + // OptionSetValue derivedInductionStatus = this.CalculateInductionStation(qtlsDate, existingInductionStatus); + // Person.SetIndutionStatus(orgService, personId, derivedInductionStatus.Value); + // } + // return; + //} + //else if (context.PostEntityImages != null && context.PostEntityImages.Contains(postImageContactAlias)) + //{ + // //Triggered on updates to contact.dfeta_qtlsdate + // Entity postImageEnt = context.PostEntityImages[postImageContactAlias]; + // if (postImageEnt.Contains(Person.Attributes.ID)) + // { + // var personId = postImageEnt.GetAttributeValue(Person.Attributes.ID); + // var qtlsDate = postImageEnt.GetAttributeValue(Person.Attributes.QTLSDate); + // var inductionStatus = this.GetContactInductionStatus(orgService, personId); + // OptionSetValue derivedInductionStatus = this.CalculateInductionStation(qtlsDate, inductionStatus); + // if (derivedInductionStatus != null) + // Person.SetIndutionStatus(orgService, personId, derivedInductionStatus?.Value); + // else + // Person.SetIndutionStatus(orgService, personId, null); + // } + // return; + //} + } + + private OptionSetValue? GetContactInductionStatus(IOrganizationService orgService, Guid personId) + { + QueryExpression query = new QueryExpression(); + query.EntityName = dfeta_induction.EntityLogicalName; + query.ColumnSet = new ColumnSet(new string[] { + dfeta_induction.Fields.dfeta_InductionStatus + }); + query.Criteria = new FilterExpression(LogicalOperator.And); + query.Criteria.AddCondition(dfeta_induction.Fields.dfeta_PersonId, ConditionOperator.Equal, personId); + query.Criteria.AddCondition(dfeta_induction.Fields.StateCode, ConditionOperator.Equal, 0/*Active*/); + + EntityCollection collection = orgService.RetrieveMultiple(query); + if (collection != null && collection.Entities != null && collection.Entities.Count > 0) + { + return collection.Entities.Select(x => x.GetAttributeValue(dfeta_induction.Fields.dfeta_InductionStatus)).FirstOrDefault(); + } + + return null; + } + + public DateTime? GetContactQTLSDate(IOrganizationService orgService, Guid personId) + { + var entity = orgService.Retrieve(Contact.EntityLogicalName, personId, new ColumnSet(new string[] { + Contact.Fields.dfeta_qtlsdate + })); + + if (entity != null) + { + var qtlsDate = entity.GetAttributeValue(Contact.Fields.dfeta_qtlsdate); + return qtlsDate; + } + return null; + } + public static void SetIndutionStatus(IOrganizationService orgService, Guid id, int? indutionStatus) + { + if (orgService == null) + { + throw new ArgumentNullException("orgService"); + } + + Entity person = new Entity(Contact.EntityLogicalName); + person.Id = id; + person.Attributes.Add(Contact.Fields.dfeta_InductionStatus, indutionStatus.HasValue ? new OptionSetValue(indutionStatus.Value) : null); + orgService.Update(person); + } + + private OptionSetValue? CalculateInductionStation(DateTime? qtlsDate, OptionSetValue? value) + { + if (value != null) + { + switch (value?.Value) + { + case (int)dfeta_InductionStatus.Exempt: + case (int)dfeta_InductionStatus.FailedinWales when qtlsDate.HasValue: + case (int)dfeta_InductionStatus.InProgress when qtlsDate.HasValue: + case (int)dfeta_InductionStatus.InductionExtended when qtlsDate.HasValue: + case (int)dfeta_InductionStatus.NotYetCompleted when qtlsDate.HasValue: + case (int)dfeta_InductionStatus.RequiredtoComplete when qtlsDate.HasValue: + { + return new OptionSetValue((int)dfeta_InductionStatus.Exempt); + } + default: + { + return value; + } + } + } + else + { + if (qtlsDate.HasValue) + { + return new OptionSetValue((int)dfeta_InductionStatus.Exempt); + } + return null; + } + } +} diff --git a/TeachingRecordSystem/tests/TeachingRecordSystem.TestCommon/ServiceCollectionExtensions.cs b/TeachingRecordSystem/tests/TeachingRecordSystem.TestCommon/ServiceCollectionExtensions.cs index 6c0e807393..05e3f653ac 100644 --- a/TeachingRecordSystem/tests/TeachingRecordSystem.TestCommon/ServiceCollectionExtensions.cs +++ b/TeachingRecordSystem/tests/TeachingRecordSystem.TestCommon/ServiceCollectionExtensions.cs @@ -48,6 +48,8 @@ public static IServiceCollection AddFakeXrm(this IServiceCollection services) PersonNameChangedPlugin.Register(fakedXrmContext); CalculateActiveSanctionsPlugin.Register(fakedXrmContext); QtsRegistrationUpdatedPlugin.Register(fakedXrmContext); + UpdateInductionStatusPlugin.Register(fakedXrmContext); + // SeedCrmReferenceData must be registered before AddDefaultServiceClient is called // to ensure this task runs before the cache pre-warming task diff --git a/TeachingRecordSystem/tests/TeachingRecordSystem.TestCommon/TestData.CreatePerson.cs b/TeachingRecordSystem/tests/TeachingRecordSystem.TestCommon/TestData.CreatePerson.cs index 36addd5f9f..7fde95c6cf 100644 --- a/TeachingRecordSystem/tests/TeachingRecordSystem.TestCommon/TestData.CreatePerson.cs +++ b/TeachingRecordSystem/tests/TeachingRecordSystem.TestCommon/TestData.CreatePerson.cs @@ -37,6 +37,8 @@ public class CreatePersonBuilder private readonly List _qtsRegistrations = new(); private readonly List _sanctions = []; private readonly List _mqBuilders = []; + private DateOnly? _qtlsDate; + private readonly List _inductions = []; public Guid PersonId { get; } = Guid.NewGuid(); @@ -101,6 +103,17 @@ public CreatePersonBuilder WithEmail(string email) return this; } + public CreatePersonBuilder WithInduction(dfeta_InductionStatus inductionStatus, dfeta_InductionExemptionReason? inductionExemptionReason = null) + { + if (inductionStatus == dfeta_InductionStatus.Exempt && inductionExemptionReason == null) + { + throw new InvalidOperationException("WithInduction must provide InductionExemptionReason if InductionStatus is Exempt"); + } + + _inductions.Add(new(Guid.NewGuid(), inductionStatus, inductionExemptionReason)); + return this; + } + public CreatePersonBuilder WithMobileNumber(string mobileNumber) { if (_mobileNumber is not null && _mobileNumber != mobileNumber) @@ -196,6 +209,12 @@ public CreatePersonBuilder WithQts(DateOnly? qtsDate = null) return this; } + public CreatePersonBuilder WithQTLSDate(DateOnly? qtlsDate) + { + _qtlsDate = qtlsDate; + return this; + } + public CreatePersonBuilder WithQtsRegistration(DateOnly? qtsDate, string? teacherStatusValue, DateTime? createdDate, DateOnly? eytsDate, string? eytsTeacherStatus) { _qtsRegistrations.Add(new QtsRegistration(qtsDate, teacherStatusValue, createdDate, eytsDate, eytsTeacherStatus)); @@ -232,7 +251,8 @@ internal async Task Execute(TestData testData) dfeta_StatedLastName = lastName, BirthDate = dateOfBirth.ToDateTime(new TimeOnly()), dfeta_TRN = trn, - GenderCode = gender + GenderCode = gender, + dfeta_qtlsdate = _qtlsDate.ToDateTimeWithDqtBstFix(isLocalTime: false) }; if (_email is not null) @@ -250,6 +270,11 @@ internal async Task Execute(TestData testData) contact.dfeta_NINumber = _nationalInsuranceNumber ?? testData.GenerateNationalInsuranceNumber(); } + if (_qtlsDate is not null) + { + contact.dfeta_qtlsdate = _qtlsDate.ToDateTimeWithDqtBstFix(isLocalTime: true); + } + var txnRequestBuilder = RequestBuilder.CreateTransaction(testData.OrganizationService); txnRequestBuilder.AddRequest(new CreateRequest() { Target = contact }); @@ -303,6 +328,7 @@ internal async Task Execute(TestData testData) }); } + var eyts = _qtsRegistrations.Where(x => x.EytsStatusValue != null && x.EytsDate != null); IInnerRequestHandle? getEytsRegistationTask = null; foreach (var item in eyts) @@ -399,6 +425,20 @@ internal async Task Execute(TestData testData) } } + foreach (var induction in _inductions) + { + txnRequestBuilder.AddRequest(new CreateRequest() + { + Target = new dfeta_induction() + { + Id = induction.InductionId, + dfeta_PersonId = PersonId.ToEntityReference(Contact.EntityLogicalName), + dfeta_InductionStatus = induction.inductionStatus, + dfeta_InductionExemptionReason = induction.inductionExemptionReason, + } + }); + } + foreach (var sanction in _sanctions) { var sanctionCode = await testData.ReferenceDataCache.GetSanctionCodeByValue(sanction.SanctionCode); @@ -473,7 +513,8 @@ await testData.SyncConfiguration.SyncIfEnabled( QtsDate = getQtsRegistationTask != null ? getQtsRegistationTask.GetResponse().Entity.ToEntity().dfeta_QTSDate.ToDateOnlyWithDqtBstFix(true) : null, EytsDate = getEytsRegistationTask != null ? getEytsRegistationTask.GetResponse().Entity.ToEntity().dfeta_EYTSDate.ToDateOnlyWithDqtBstFix(true) : null, Sanctions = [.. _sanctions], - MandatoryQualifications = mqs + MandatoryQualifications = [.. mqs.Select(t => t)], + Inductions = [.. _inductions] }; } } @@ -707,8 +748,11 @@ public record CreatePersonResult public required DateOnly? EytsDate { get; init; } public required IReadOnlyCollection Sanctions { get; init; } public required IReadOnlyCollection MandatoryQualifications { get; init; } + public required IReadOnlyCollection Inductions { get; init; } } + public record Induction(Guid InductionId, dfeta_InductionStatus inductionStatus, dfeta_InductionExemptionReason? inductionExemptionReason); + public record Sanction(Guid SanctionId, string SanctionCode, DateOnly? StartDate, DateOnly? EndDate, DateOnly? ReviewDate, bool Spent, string? Details, string? DetailsLink, bool IsActive); public record MandatoryQualificationInfo( diff --git a/crm_attributes.json b/crm_attributes.json index 1e2100cf93..0dcac2345c 100644 --- a/crm_attributes.json +++ b/crm_attributes.json @@ -73,7 +73,8 @@ "dfeta_slugid", "dfeta_allowpiiupdatesfromregister", "dfeta_previouslastname", - "dfeta_AllowIDSignInWithProhibitions" + "dfeta_AllowIDSignInWithProhibitions", + "dfeta_qtlsdate" ], "dfeta_businesseventaudit":[ "createdon",