diff --git a/TeachingRecordSystem/src/TeachingRecordSystem.Api/V3/Core/Operations/CreateTrnRequest.cs b/TeachingRecordSystem/src/TeachingRecordSystem.Api/V3/Core/Operations/CreateTrnRequest.cs index f298d6ce0..8f011075a 100644 --- a/TeachingRecordSystem/src/TeachingRecordSystem.Api/V3/Core/Operations/CreateTrnRequest.cs +++ b/TeachingRecordSystem/src/TeachingRecordSystem.Api/V3/Core/Operations/CreateTrnRequest.cs @@ -23,6 +23,13 @@ public record CreateTrnRequestCommand public required string? NationalInsuranceNumber { get; init; } public required bool? IdentityVerified { get; init; } public required string? OneLoginUserSubject { get; init; } + public required Contact_GenderCode? GenderCode { get; set; } + public required string? AddressLine1 { get; set; } + public required string? AddressLine2 { get; set; } + public required string? AddressLine3 { get; set; } + public required string? City { get; set; } + public required string? Postcode { get; set; } + public required string? Country { get; set; } } public class CreateTrnRequestHandler( @@ -144,14 +151,20 @@ await crmQueryDispatcher.ExecuteQueryAsync(new CreateContactQuery() StatedMiddleName = command.MiddleName ?? "", StatedLastName = command.LastName, DateOfBirth = command.DateOfBirth, - Gender = Contact_GenderCode.Notavailable, + Gender = command.GenderCode ?? Contact_GenderCode.Notavailable, EmailAddress = emailAddress, NationalInsuranceNumber = NationalInsuranceNumberHelper.Normalize(command.NationalInsuranceNumber), PotentialDuplicates = potentialDuplicates.Select(d => (Duplicate: d, HasActiveAlert: resultsWithActiveAlerts.Contains(d.ContactId))).ToArray(), ApplicationUserName = currentApplicationUserName, Trn = trn, TrnRequestId = TrnRequestHelper.GetCrmTrnRequestId(currentApplicationUserId, command.RequestId), - OutboxMessages = outboxMessages + OutboxMessages = outboxMessages, + Address1Line1 = command.AddressLine1, + Address1Line2 = command.AddressLine2, + Address1Line3 = command.AddressLine3, + Address1City = command.City, + Address1PostalCode = command.Postcode, + Address1Country = command.Country }); var status = trn is not null ? TrnRequestStatus.Completed : TrnRequestStatus.Pending; diff --git a/TeachingRecordSystem/src/TeachingRecordSystem.Api/V3/Core/SharedModels/Gender.cs b/TeachingRecordSystem/src/TeachingRecordSystem.Api/V3/Core/SharedModels/Gender.cs new file mode 100644 index 000000000..8c8b40012 --- /dev/null +++ b/TeachingRecordSystem/src/TeachingRecordSystem.Api/V3/Core/SharedModels/Gender.cs @@ -0,0 +1,19 @@ +using TeachingRecordSystem.Core.Dqt.Models; + +namespace TeachingRecordSystem.Api.V3.Core.SharedModels; + +public enum Gender +{ + Male = 1, + Female = 2, + Other = 3 +} + +public static class GenderExtensions +{ + public static Contact_GenderCode ConvertToContact_GenderCode(this Gender input) => + input.ConvertToEnumByValue(); + + public static bool TryConvertToContact_GenderCode(this Gender input, out Contact_GenderCode result) => + input.TryConvertToEnumByValue(out result); +} diff --git a/TeachingRecordSystem/src/TeachingRecordSystem.Api/V3/V20240307/Controllers/TrnRequestsController.cs b/TeachingRecordSystem/src/TeachingRecordSystem.Api/V3/V20240307/Controllers/TrnRequestsController.cs index ec855e5c0..56449e88b 100644 --- a/TeachingRecordSystem/src/TeachingRecordSystem.Api/V3/V20240307/Controllers/TrnRequestsController.cs +++ b/TeachingRecordSystem/src/TeachingRecordSystem.Api/V3/V20240307/Controllers/TrnRequestsController.cs @@ -39,7 +39,14 @@ public async Task CreateTrnRequestAsync( EmailAddresses = request.Person.Email is string emailAddress ? [emailAddress] : [], NationalInsuranceNumber = request.Person.NationalInsuranceNumber, IdentityVerified = null, - OneLoginUserSubject = null + OneLoginUserSubject = null, + AddressLine1 = null, + AddressLine2 = null, + AddressLine3 = null, + City = null, + Postcode = null, + GenderCode = null, + Country = null }; var result = await handler.HandleAsync(command); diff --git a/TeachingRecordSystem/src/TeachingRecordSystem.Api/V3/V20240606/Controllers/TrnRequestsController.cs b/TeachingRecordSystem/src/TeachingRecordSystem.Api/V3/V20240606/Controllers/TrnRequestsController.cs index bc19c58e0..34a0b6bf4 100644 --- a/TeachingRecordSystem/src/TeachingRecordSystem.Api/V3/V20240606/Controllers/TrnRequestsController.cs +++ b/TeachingRecordSystem/src/TeachingRecordSystem.Api/V3/V20240606/Controllers/TrnRequestsController.cs @@ -39,7 +39,14 @@ public async Task CreateTrnRequestAsync( EmailAddresses = request.Person.EmailAddresses ?? [], NationalInsuranceNumber = request.Person.NationalInsuranceNumber, IdentityVerified = null, - OneLoginUserSubject = null + OneLoginUserSubject = null, + AddressLine1 = null, + AddressLine2 = null, + AddressLine3 = null, + City = null, + Postcode = null, + GenderCode = null, + Country = null }; var result = await handler.HandleAsync(command); diff --git a/TeachingRecordSystem/src/TeachingRecordSystem.Api/V3/VNext/Controllers/TrnRequestsController.cs b/TeachingRecordSystem/src/TeachingRecordSystem.Api/V3/VNext/Controllers/TrnRequestsController.cs index e4e140491..b0b96b915 100644 --- a/TeachingRecordSystem/src/TeachingRecordSystem.Api/V3/VNext/Controllers/TrnRequestsController.cs +++ b/TeachingRecordSystem/src/TeachingRecordSystem.Api/V3/VNext/Controllers/TrnRequestsController.cs @@ -39,7 +39,14 @@ public async Task CreateTrnRequestAsync( EmailAddresses = request.Person.EmailAddresses ?? [], NationalInsuranceNumber = request.Person.NationalInsuranceNumber, IdentityVerified = request.IdentityVerified, - OneLoginUserSubject = request.OneLoginUserSubject + OneLoginUserSubject = request.OneLoginUserSubject, + AddressLine1 = request.Person.Address?.AddressLine1, + AddressLine2 = request.Person.Address?.AddressLine2, + AddressLine3 = request.Person.Address?.AddressLine3, + GenderCode = request.Person.GenderCode.HasValue ? Core.SharedModels.GenderExtensions.ConvertToContact_GenderCode(request.Person.GenderCode!.Value) : null, + City = request.Person.Address?.City, + Postcode = request.Person.Address?.Postcode, + Country = request.Person.Address?.Country, }; var result = await handler.HandleAsync(command); diff --git a/TeachingRecordSystem/src/TeachingRecordSystem.Api/V3/VNext/Requests/CreateTrnRequestRequest.cs b/TeachingRecordSystem/src/TeachingRecordSystem.Api/V3/VNext/Requests/CreateTrnRequestRequest.cs index 337832ed2..e37c00ae8 100644 --- a/TeachingRecordSystem/src/TeachingRecordSystem.Api/V3/VNext/Requests/CreateTrnRequestRequest.cs +++ b/TeachingRecordSystem/src/TeachingRecordSystem.Api/V3/VNext/Requests/CreateTrnRequestRequest.cs @@ -1,8 +1,30 @@ + +using TeachingRecordSystem.Api.V3.Core.SharedModels; + namespace TeachingRecordSystem.Api.V3.VNext.Requests; -[GenerateVersionedDto(typeof(V20240606.Requests.CreateTrnRequestRequest))] +[GenerateVersionedDto(typeof(V20240606.Requests.CreateTrnRequestRequest), excludeMembers: "Person")] public partial record CreateTrnRequestRequest { public bool IdentityVerified { get; init; } public string? OneLoginUserSubject { get; init; } + public required CreateTrnRequestRequestPerson Person { get; init; } +} + +[GenerateVersionedDto(typeof(V20240606.Requests.CreateTrnRequestRequestPerson))] +public partial record CreateTrnRequestRequestPerson +{ + public CreateTrnRequestAddress? Address { get; init; } + public Gender? GenderCode { get; init; } } + +public class CreateTrnRequestAddress +{ + public string? AddressLine1 { get; init; } + public string? AddressLine2 { get; init; } + public string? AddressLine3 { get; init; } + public string? City { get; init; } + public string? Postcode { get; init; } + public string? Country { get; init; } +} + diff --git a/TeachingRecordSystem/src/TeachingRecordSystem.Api/V3/VNext/Validators/CreateTrnRequestRequestValidator.cs b/TeachingRecordSystem/src/TeachingRecordSystem.Api/V3/VNext/Validators/CreateTrnRequestRequestValidator.cs new file mode 100644 index 000000000..dc7ac903f --- /dev/null +++ b/TeachingRecordSystem/src/TeachingRecordSystem.Api/V3/VNext/Validators/CreateTrnRequestRequestValidator.cs @@ -0,0 +1,40 @@ +using FluentValidation; +using TeachingRecordSystem.Api.V3.VNext.Requests; +using TeachingRecordSystem.Core.Dqt.Models; + +namespace TeachingRecordSystem.Api.V3.VNext.Validators; + +public class CreateTrnRequestRequestValidator : AbstractValidator +{ + public CreateTrnRequestRequestValidator(IClock clock) + { + RuleFor(r => r.Person.Address!.AddressLine1) + .MaximumLength(AttributeConstraints.Contact.Address1_Line1MaxLength) + .When(r => r.Person.Address != null); + + RuleFor(r => r.Person.Address!.AddressLine2) + .MaximumLength(AttributeConstraints.Contact.Address1_Line2MaxLength) + .When(r => r.Person.Address != null); + + RuleFor(r => r.Person.Address!.AddressLine3) + .MaximumLength(AttributeConstraints.Contact.Address1_Line3MaxLength) + .When(r => r.Person.Address != null); + + RuleFor(r => r.Person.Address!.City) + .MaximumLength(AttributeConstraints.Contact.Address1_CityMaxLength) + .When(r => r.Person.Address != null); + + RuleFor(r => r.Person.Address!.Country) + .MaximumLength(AttributeConstraints.Contact.Address1_CountryMaxLength) + .When(r => r.Person.Address != null); + + RuleFor(r => r.Person.Address!.Postcode) + .MaximumLength(AttributeConstraints.Contact.Address1_PostalCodeLength) + .When(r => r.Person.Address != null); + + RuleFor(r => r.Person.GenderCode) + .IsInEnum(); + } +} + + diff --git a/TeachingRecordSystem/src/TeachingRecordSystem.Core/Dqt/Queries/CreateTeacherQuery.cs b/TeachingRecordSystem/src/TeachingRecordSystem.Core/Dqt/Queries/CreateTeacherQuery.cs index 2c70b6a39..2924f0acf 100644 --- a/TeachingRecordSystem/src/TeachingRecordSystem.Core/Dqt/Queries/CreateTeacherQuery.cs +++ b/TeachingRecordSystem/src/TeachingRecordSystem.Core/Dqt/Queries/CreateTeacherQuery.cs @@ -17,4 +17,10 @@ public class CreateContactQuery : ICrmQuery public required string? Trn { get; init; } public required string? TrnRequestId { get; init; } public required IEnumerable OutboxMessages { get; init; } + public required string? Address1Line1 { get; init; } + public required string? Address1Line2 { get; init; } + public required string? Address1Line3 { get; init; } + public required string? Address1City { get; init; } + public required string? Address1PostalCode { get; init; } + public required string? Address1Country { get; init; } } diff --git a/TeachingRecordSystem/src/TeachingRecordSystem.Core/Dqt/QueryHandlers/CreateContactHandler.cs b/TeachingRecordSystem/src/TeachingRecordSystem.Core/Dqt/QueryHandlers/CreateContactHandler.cs index 0d0b06333..ef2f894f6 100644 --- a/TeachingRecordSystem/src/TeachingRecordSystem.Core/Dqt/QueryHandlers/CreateContactHandler.cs +++ b/TeachingRecordSystem/src/TeachingRecordSystem.Core/Dqt/QueryHandlers/CreateContactHandler.cs @@ -31,7 +31,13 @@ public async Task ExecuteAsync(CreateContactQuery query, IOrganizationServ EMailAddress1 = query.EmailAddress, dfeta_AllowPiiUpdatesFromRegister = false, dfeta_TrnRequestID = query.TrnRequestId, - dfeta_TRN = query.Trn + dfeta_TRN = query.Trn, + Address1_Line1 = query.Address1Line1, + Address1_Line2 = query.Address1Line2, + Address1_Line3 = query.Address1Line3, + Address1_City = query.Address1City, + Address1_PostalCode = query.Address1PostalCode, + Address1_Country = query.Address1Country }; if (query.Trn is null) @@ -40,6 +46,12 @@ public async Task ExecuteAsync(CreateContactQuery query, IOrganizationServ contact.Attributes.Remove(Contact.Fields.dfeta_TRN); } + if (query.Address1City is null) + { + // CRM plug-in explodes if AddressCity is specified but is null + contact.Attributes.Remove(Contact.Fields.Address1_City); + } + requestBuilder.AddRequest(new CreateRequest() { Target = contact }); foreach (var (duplicate, hasActiveAlert) in query.PotentialDuplicates) diff --git a/TeachingRecordSystem/tests/TeachingRecordSystem.Api.Tests/V3/VNext/CreateTrnRequestTests.cs b/TeachingRecordSystem/tests/TeachingRecordSystem.Api.Tests/V3/VNext/CreateTrnRequestTests.cs index 3cf788f6c..b78b2c97b 100644 --- a/TeachingRecordSystem/tests/TeachingRecordSystem.Api.Tests/V3/VNext/CreateTrnRequestTests.cs +++ b/TeachingRecordSystem/tests/TeachingRecordSystem.Api.Tests/V3/VNext/CreateTrnRequestTests.cs @@ -1,3 +1,5 @@ +using TeachingRecordSystem.Api.V3.Core.SharedModels; +using TeachingRecordSystem.Api.V3.VNext.Requests; using TeachingRecordSystem.Core.Dqt.Queries; using TeachingRecordSystem.Core.Services.DqtOutbox; using TeachingRecordSystem.Core.Services.DqtOutbox.Messages; @@ -69,4 +71,133 @@ public async Task Post_CreatesOutboxMessageInCrm() Assert.Equal(dateOfBirth, message.DateOfBirth); }); } + + [Fact] + public async Task Post_ValidAddressFields_PopulatesContactAddressFields() + { + // Arrange + var requestId = Guid.NewGuid().ToString(); + var firstName = TestData.GenerateFirstName(); + var middleName = TestData.GenerateMiddleName(); + var lastName = TestData.GenerateLastName(); + var dateOfBirth = TestData.GenerateDateOfBirth(); + var email = TestData.GenerateUniqueEmail(); + var identityVerified = true; + var oneLoginUserSubject = TestData.CreateOneLoginUserSubject(); + var addressLine1 = Faker.Address.StreetName(); + var addressLine2 = Faker.Address.StreetName(); + var addressLine3 = Faker.Address.StreetName(); + var postcode = Faker.Address.UkPostCode(); + var country = Faker.Address.Country(); + var gender = Gender.Female; + var city = Faker.Address.City(); + + var request = new HttpRequestMessage(HttpMethod.Post, "v3/trn-requests") + { + Content = CreateJsonContent(new + { + requestId = requestId, + person = new + { + firstName = firstName, + middleName = middleName, + lastName = lastName, + dateOfBirth = dateOfBirth, + emailAddresses = new[] { email }, + address = new + { + addressLine1 = addressLine1, + addressLine2 = addressLine2, + addressLine3 = addressLine3, + city = city, + postcode = postcode, + country = country, + }, + genderCode = gender + }, + identityVerified = identityVerified, + oneLoginUserSubject = oneLoginUserSubject + }) + }; + + // Act + var response = await GetHttpClientWithApiKey().SendAsync(request); + + // Assert + await AssertEx.JsonResponseAsync(response, expectedStatusCode: StatusCodes.Status200OK); + + var (crmQuery, _) = CrmQueryDispatcherSpy.GetSingleQuery(); + Assert.Equal(addressLine1, crmQuery.Address1Line1); + Assert.Equal(addressLine2, crmQuery.Address1Line2); + Assert.Equal(addressLine3, crmQuery.Address1Line3); + Assert.Equal(city, crmQuery.Address1City); + Assert.Equal(postcode, crmQuery.Address1PostalCode); + Assert.Equal(country, crmQuery.Address1Country); + Assert.Equal(Contact_GenderCode.Female, crmQuery.Gender); + } + + [Fact] + public async Task Post_AddressFieldsExceedingMaxLengths_ReturnsBadRequest() + { + // Arrange + var requestId = Guid.NewGuid().ToString(); + var firstName = TestData.GenerateFirstName(); + var middleName = TestData.GenerateMiddleName(); + var lastName = TestData.GenerateLastName(); + var dateOfBirth = TestData.GenerateDateOfBirth(); + var email = TestData.GenerateUniqueEmail(); + var identityVerified = true; + var oneLoginUserSubject = TestData.CreateOneLoginUserSubject(); + var addressLine1 = new string('x', 255); + var addressLine2 = new string('x', 255); + var addressLine3 = new string('x', 255); + var postcode = new string('x', 255); + var country = new string('x', 255); + var gender = Gender.Female; + var city = new string('x', 255); + + var request = new HttpRequestMessage(HttpMethod.Post, "v3/trn-requests") + { + Content = CreateJsonContent(new + { + requestId = requestId, + person = new + { + firstName = firstName, + middleName = middleName, + lastName = lastName, + dateOfBirth = dateOfBirth, + emailAddresses = new[] { email }, + address = new + { + addressLine1 = addressLine1, + addressLine2 = addressLine2, + addressLine3 = addressLine3, + city = city, + postcode = postcode, + country = country, + }, + genderCode = gender + }, + identityVerified = identityVerified, + oneLoginUserSubject = oneLoginUserSubject + }) + }; + + // Act + var response = await GetHttpClientWithApiKey().SendAsync(request); + + // Assert + await AssertEx.JsonResponseHasValidationErrorsForPropertiesAsync( + response, + new Dictionary() + { + { $"{nameof(CreateTrnRequestRequest.Person)}.{nameof(CreateTrnRequestRequestPerson.Address)}.{nameof(CreateTrnRequestAddress.AddressLine1)}", $"The length of 'Person Address Address Line1' must be {AttributeConstraints.Contact.Address1_Line1MaxLength} characters or fewer. You entered 255 characters."}, + { $"{nameof(CreateTrnRequestRequest.Person)}.{nameof(CreateTrnRequestRequestPerson.Address)}.{nameof(CreateTrnRequestAddress.AddressLine2)}", $"The length of 'Person Address Address Line2' must be {AttributeConstraints.Contact.Address1_Line2MaxLength} characters or fewer. You entered 255 characters."}, + { $"{nameof(CreateTrnRequestRequest.Person)}.{nameof(CreateTrnRequestRequestPerson.Address)}.{nameof(CreateTrnRequestAddress.AddressLine3)}", $"The length of 'Person Address Address Line3' must be {AttributeConstraints.Contact.Address1_Line3MaxLength} characters or fewer. You entered 255 characters."}, + { $"{nameof(CreateTrnRequestRequest.Person)}.{nameof(CreateTrnRequestRequestPerson.Address)}.{nameof(CreateTrnRequestAddress.Postcode)}", $"The length of 'Person Address Postcode' must be {AttributeConstraints.Contact.Address1_PostalCodeLength} characters or fewer. You entered 255 characters."}, + { $"{nameof(CreateTrnRequestRequest.Person)}.{nameof(CreateTrnRequestRequestPerson.Address)}.{nameof(CreateTrnRequestAddress.City)}", $"The length of 'Person Address City' must be {AttributeConstraints.Contact.Address1_CityMaxLength} characters or fewer. You entered 255 characters."}, + { $"{nameof(CreateTrnRequestRequest.Person)}.{nameof(CreateTrnRequestRequestPerson.Address)}.{nameof(CreateTrnRequestAddress.Country)}", $"The length of 'Person Address Country' must be {AttributeConstraints.Contact.Address1_CountryMaxLength} characters or fewer. You entered 255 characters."}, + }); + } } diff --git a/TeachingRecordSystem/tests/TeachingRecordSystem.Core.Dqt.CrmIntegrationTests/QueryTests/CreateContactTests.cs b/TeachingRecordSystem/tests/TeachingRecordSystem.Core.Dqt.CrmIntegrationTests/QueryTests/CreateContactTests.cs index 76b2ce21f..218566639 100644 --- a/TeachingRecordSystem/tests/TeachingRecordSystem.Core.Dqt.CrmIntegrationTests/QueryTests/CreateContactTests.cs +++ b/TeachingRecordSystem/tests/TeachingRecordSystem.Core.Dqt.CrmIntegrationTests/QueryTests/CreateContactTests.cs @@ -26,6 +26,13 @@ public async Task QueryExecutesSuccessfully() var lastName = _dataScope.TestData.GenerateLastName(); var email = _dataScope.TestData.GenerateUniqueEmail(); var nino = _dataScope.TestData.GenerateNationalInsuranceNumber(); + var address1 = Faker.Address.StreetName(); + var address2 = Faker.Address.StreetName(); + var address3 = Faker.Address.StreetName(); + var city = Faker.Address.City().ToUpper(); + var country = "England"; + var postCode = Faker.Address.UkPostCode(); + var gender = Contact_GenderCode.Notavailable; var dateOfBirth = _dataScope.TestData.GenerateDateOfBirth(); var trn = await _dataScope.TestData.GenerateTrnAsync(); @@ -41,11 +48,17 @@ public async Task QueryExecutesSuccessfully() EmailAddress = email, NationalInsuranceNumber = nino, DateOfBirth = dateOfBirth, - Gender = Contact_GenderCode.Notavailable, + Gender = gender, Trn = trn, PotentialDuplicates = [], ApplicationUserName = "Tests", - OutboxMessages = [] + OutboxMessages = [], + Address1Line1 = address1, + Address1Line2 = address2, + Address1Line3 = address3, + Address1City = city, + Address1Country = country, + Address1PostalCode = postCode }; // Act @@ -63,6 +76,13 @@ public async Task QueryExecutesSuccessfully() Assert.False(contact.dfeta_AllowPiiUpdatesFromRegister); Assert.Equal(nino, contact.dfeta_NINumber); Assert.Equal(dateOfBirth, contact.BirthDate.ToDateOnlyWithDqtBstFix(true)); + Assert.Equal(address1, contact.Address1_Line1); + Assert.Equal(address2, contact.Address1_Line2); + Assert.Equal(address3, contact.Address1_Line3); + Assert.Equal(city, contact.Address1_City); + Assert.Equal(postCode, contact.Address1_PostalCode); + Assert.Equal(country, contact.Address1_Country); + Assert.Equal(gender, contact.GenderCode); } [Fact] @@ -126,7 +146,13 @@ await _dataScope.OrganizationService.CreateAsync(new Contact() HasActiveAlert: false) ], ApplicationUserName = "Tests", - OutboxMessages = [] + OutboxMessages = [], + Address1Line1 = null, + Address1Line2 = null, + Address1Line3 = null, + Address1City = null, + Address1Country = null, + Address1PostalCode = null }; var createdTeacherId2 = await _crmQueryDispatcher.ExecuteQueryAsync(query); using var ctx = new DqtCrmServiceContext(_dataScope.OrganizationService);