diff --git a/TeachingRecordSystem/src/TeachingRecordSystem.Api/V3/Core/Operations/CreateTrnRequest.cs b/TeachingRecordSystem/src/TeachingRecordSystem.Api/V3/Core/Operations/CreateTrnRequest.cs index 1cec0f238..fbb864170 100644 --- a/TeachingRecordSystem/src/TeachingRecordSystem.Api/V3/Core/Operations/CreateTrnRequest.cs +++ b/TeachingRecordSystem/src/TeachingRecordSystem.Api/V3/Core/Operations/CreateTrnRequest.cs @@ -1,7 +1,9 @@ using TeachingRecordSystem.Api.Infrastructure.Security; using TeachingRecordSystem.Api.V3.Core.SharedModels; using TeachingRecordSystem.Api.Validation; +using TeachingRecordSystem.Core.DataStore.Postgres; using TeachingRecordSystem.Core.Dqt; +using TeachingRecordSystem.Core.Dqt.Models; using TeachingRecordSystem.Core.Dqt.Queries; using TeachingRecordSystem.Core.Services.NameSynonyms; using TeachingRecordSystem.Core.Services.TrnGenerationApi; @@ -20,6 +22,7 @@ public record CreateTrnRequestCommand } public class CreateTrnRequestHandler( + TrsDbContext dbContext, ICrmQueryDispatcher crmQueryDispatcher, TrnRequestHelper trnRequestHelper, ICurrentClientProvider currentClientProvider, @@ -31,37 +34,76 @@ public async Task Handle(CreateTrnRequestCommand command) var currentClientId = currentClientProvider.GetCurrentClientId(); var trnRequest = await trnRequestHelper.GetTrnRequestInfo(currentClientId, command.RequestId); - if (trnRequest != null) + if (trnRequest is not null) { throw new ErrorException(ErrorRegistry.CannotResubmitRequest()); } - string? trn = null; - + // Normalize names; DQT matching process requires a single-word first name :-| var firstAndMiddleNames = $"{command.FirstName} {command.MiddleName}".Split(' ', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); var firstName = firstAndMiddleNames.First(); var middleName = string.Join(' ', firstAndMiddleNames.Skip(1)); var firstNameSynonyms = (await nameSynonymProvider.GetAllNameSynonyms()).GetValueOrDefault(firstName, []); - var potentialDuplicates = await crmQueryDispatcher.ExecuteQuery( + var normalizedNino = NationalInsuranceNumberHelper.Normalize(command.NationalInsuranceNumber); + + // Check workforce data for NINO matches + var workforceDataMatches = normalizedNino is not null ? + await dbContext.TpsEmployments + .Where(e => e.NationalInsuranceNumber == normalizedNino) + .Join(dbContext.Persons, e => e.PersonId, p => p.PersonId, (e, p) => p.DqtContactId!.Value) + .ToArrayAsync() : + []; + + var potentialDuplicates = (await crmQueryDispatcher.ExecuteQuery( new FindPotentialDuplicateContactsQuery() { FirstNames = firstNameSynonyms.Append(firstName), MiddleName = middleName, LastName = command.LastName, DateOfBirth = command.DateOfBirth, - EmailAddresses = command.EmailAddresses - }); + EmailAddresses = command.EmailAddresses, + NationalInsuranceNumber = normalizedNino, + MatchedOnNationalInsuranceNumberContactIds = workforceDataMatches + })).ToList(); + + // If any record has matched on NINO & DOB treat that as a definite match and return the existing record's details + var matchedOnNinoAndDob = potentialDuplicates + .FirstOrDefault(d => d.MatchedAttributes.Contains(Contact.Fields.dfeta_NINumber) && d.MatchedAttributes.Contains(Contact.Fields.BirthDate)); + if (matchedOnNinoAndDob is not null) + { + // FUTURE: consider whether we should be updating any missing attributes here - if (potentialDuplicates.Length == 0) + var hasStatedNames = !string.IsNullOrEmpty(matchedOnNinoAndDob.StatedFirstName) && + !string.IsNullOrEmpty(matchedOnNinoAndDob.StatedLastName); + + return new TrnRequestInfo() + { + RequestId = command.RequestId, + Person = new TrnRequestInfoPerson() + { + FirstName = hasStatedNames ? matchedOnNinoAndDob.StatedFirstName! : matchedOnNinoAndDob.FirstName, + MiddleName = hasStatedNames ? matchedOnNinoAndDob.StatedMiddleName ?? "" : matchedOnNinoAndDob.MiddleName, + LastName = hasStatedNames ? matchedOnNinoAndDob.StatedLastName! : matchedOnNinoAndDob.LastName, + EmailAddress = matchedOnNinoAndDob.EmailAddress, + DateOfBirth = matchedOnNinoAndDob.DateOfBirth!.Value, + NationalInsuranceNumber = NationalInsuranceNumberHelper.Normalize(matchedOnNinoAndDob.NationalInsuranceNumber) + }, + Trn = matchedOnNinoAndDob.Trn, + Status = TrnRequestStatus.Completed + }; + } + + string? trn = null; + if (potentialDuplicates.Count == 0) { trn = await trnGenerationApiClient.GenerateTrn(); } var emailAddress = command.EmailAddresses?.FirstOrDefault(); - var contactId = await crmQueryDispatcher.ExecuteQuery(new CreateContactQuery() + await crmQueryDispatcher.ExecuteQuery(new CreateContactQuery() { FirstName = firstName, MiddleName = middleName, diff --git a/TeachingRecordSystem/src/TeachingRecordSystem.Core/Dqt/Queries/CreateTeacherQuery.cs b/TeachingRecordSystem/src/TeachingRecordSystem.Core/Dqt/Queries/CreateTeacherQuery.cs index 1509037fb..0c609d208 100644 --- a/TeachingRecordSystem/src/TeachingRecordSystem.Core/Dqt/Queries/CreateTeacherQuery.cs +++ b/TeachingRecordSystem/src/TeachingRecordSystem.Core/Dqt/Queries/CreateTeacherQuery.cs @@ -11,7 +11,7 @@ public class CreateContactQuery : ICrmQuery public required DateOnly DateOfBirth { get; init; } public required string? EmailAddress { get; init; } public required string? NationalInsuranceNumber { get; init; } - public required FindPotentialDuplicateContactsResult[] PotentialDuplicates { get; init; } + public required IReadOnlyCollection PotentialDuplicates { get; init; } public required string? Trn { get; init; } public required string? TrnRequestId { get; init; } } diff --git a/TeachingRecordSystem/src/TeachingRecordSystem.Core/Dqt/Queries/FindPotentialDuplicateContactsQuery.cs b/TeachingRecordSystem/src/TeachingRecordSystem.Core/Dqt/Queries/FindPotentialDuplicateContactsQuery.cs index 47eb3e295..fbb93bd1d 100644 --- a/TeachingRecordSystem/src/TeachingRecordSystem.Core/Dqt/Queries/FindPotentialDuplicateContactsQuery.cs +++ b/TeachingRecordSystem/src/TeachingRecordSystem.Core/Dqt/Queries/FindPotentialDuplicateContactsQuery.cs @@ -7,18 +7,27 @@ public record FindPotentialDuplicateContactsQuery : ICrmQuery EmailAddresses { get; init; } + public required string? NationalInsuranceNumber { get; init; } + + // A collection of contact IDs that have already been identified to match on NINO (through workforce data) + public required Guid[] MatchedOnNationalInsuranceNumberContactIds { get; init; } } public record FindPotentialDuplicateContactsResult { public required Guid ContactId { get; init; } - public required string[] MatchedAttributes { get; init; } + public required string Trn { get; init; } + public required IReadOnlyCollection MatchedAttributes { get; init; } public required bool HasActiveSanctions { get; init; } public required bool HasQtsDate { get; init; } public required bool HasEytsDate { get; init; } public required string FirstName { get; init; } public required string MiddleName { get; init; } public required string LastName { get; init; } + public required string? StatedFirstName { get; init; } + public required string? StatedMiddleName { get; init; } + public required string? StatedLastName { get; init; } public required DateOnly? DateOfBirth { get; init; } + public required string? NationalInsuranceNumber { get; init; } public required string? EmailAddress { get; init; } } diff --git a/TeachingRecordSystem/src/TeachingRecordSystem.Core/Dqt/QueryHandlers/CreateContactHandler.cs b/TeachingRecordSystem/src/TeachingRecordSystem.Core/Dqt/QueryHandlers/CreateContactHandler.cs index 1963664c1..95be1fc92 100644 --- a/TeachingRecordSystem/src/TeachingRecordSystem.Core/Dqt/QueryHandlers/CreateContactHandler.cs +++ b/TeachingRecordSystem/src/TeachingRecordSystem.Core/Dqt/QueryHandlers/CreateContactHandler.cs @@ -14,6 +14,8 @@ public async Task Execute(CreateContactQuery query, IOrganizationServiceAs var requestBuilder = RequestBuilder.CreateTransaction(organizationService); + Debug.Assert(query.Trn is null || query.PotentialDuplicates.Count == 0); + var contact = new Contact() { Id = contactId, @@ -27,28 +29,16 @@ public async Task Execute(CreateContactQuery query, IOrganizationServiceAs dfeta_NINumber = query.NationalInsuranceNumber, EMailAddress1 = query.EmailAddress, dfeta_AllowPiiUpdatesFromRegister = false, - dfeta_TrnRequestID = query.TrnRequestId + dfeta_TrnRequestID = query.TrnRequestId, + dfeta_TRN = query.Trn }; - // only set trn if there is not any potential duplicate matches on the query - if (query.PotentialDuplicates.Length == 0) - { - contact.dfeta_TRN = query.Trn; - } - requestBuilder.AddRequest(new CreateRequest() { Target = contact }); - if (query.PotentialDuplicates.Length == 0) + foreach (var duplicate in query.PotentialDuplicates) { - FlagBadData(requestBuilder, query, contactId); - } - else - { - foreach (var duplicate in query.PotentialDuplicates) - { - var task = CreateDuplicateReviewTaskEntity(duplicate, query, contactId); - requestBuilder.AddRequest(new CreateRequest() { Target = task }); - } + var task = CreateDuplicateReviewTaskEntity(duplicate, contactId); + requestBuilder.AddRequest(new CreateRequest() { Target = task }); } await requestBuilder.Execute(); @@ -56,22 +46,7 @@ public async Task Execute(CreateContactQuery query, IOrganizationServiceAs return contactId; } - private void FlagBadData(RequestBuilder requestBuilder, CreateContactQuery createTeacherRequest, Guid contactId) - { - var firstNameContainsDigit = createTeacherRequest.FirstName.Any(Char.IsDigit); - var middleNameContainsDigit = createTeacherRequest.MiddleName?.Any(Char.IsDigit) ?? false; - var lastNameContainsDigit = createTeacherRequest.LastName.Any(Char.IsDigit); - - if (firstNameContainsDigit || middleNameContainsDigit || lastNameContainsDigit) - { - requestBuilder.AddRequest(new CreateRequest() - { - Target = CreateNameWithDigitsReviewTaskEntity(firstNameContainsDigit, middleNameContainsDigit, lastNameContainsDigit, contactId) - }); - } - } - - private CrmTask CreateDuplicateReviewTaskEntity(FindPotentialDuplicateContactsResult duplicate, CreateContactQuery createTeacherRequest, Guid contactId) + private CrmTask CreateDuplicateReviewTaskEntity(FindPotentialDuplicateContactsResult duplicate, Guid contactId) { var description = GetDescription(); @@ -92,7 +67,7 @@ string GetDescription() sb.AppendLine("Potential duplicate"); sb.AppendLine("Matched on"); - foreach (var matchedAttribute in duplicate.MatchedAttributes ?? Array.Empty()) + foreach (var matchedAttribute in duplicate.MatchedAttributes) { sb.AppendLine(matchedAttribute switch { @@ -100,12 +75,12 @@ string GetDescription() Contact.Fields.MiddleName => $" - Middle name: '{duplicate.MiddleName}'", Contact.Fields.LastName => $" - Last name: '{duplicate.LastName}'", Contact.Fields.BirthDate => $" - Date of birth: '{duplicate.DateOfBirth:dd/MM/yyyy}'", + Contact.Fields.dfeta_NINumber => $" - National Insurance number: '{duplicate.NationalInsuranceNumber}'", Contact.Fields.EMailAddress1 => $" - Email address: '{duplicate.EmailAddress}'", _ => throw new Exception($"Unknown matched field: '{matchedAttribute}'.") }); } - Debug.Assert(!duplicate.HasEytsDate || !duplicate.HasQtsDate); var additionalFlags = new List(); if (duplicate.HasActiveSanctions) @@ -117,7 +92,8 @@ string GetDescription() { additionalFlags.Add("QTS date"); } - else if (duplicate.HasEytsDate) + + if (duplicate.HasEytsDate) { additionalFlags.Add("EYTS date"); } @@ -130,50 +106,4 @@ string GetDescription() return sb.ToString(); } } - - private CrmTask CreateNameWithDigitsReviewTaskEntity( - bool firstNameContainsDigit, - bool middleNameContainsDigit, - bool lastNameContainsDigit, - Guid teacherId) - { - var description = GetDescription(); - - return new CrmTask() - { - RegardingObjectId = teacherId.ToEntityReference(Contact.EntityLogicalName), - Category = "DMSImportTrn", - Subject = "Notification for QTS Unit Team", - Description = description - }; - - string GetDescription() - { - var badFields = new List(); - - if (firstNameContainsDigit) - { - badFields.Add("first name"); - } - - if (middleNameContainsDigit) - { - badFields.Add("middle name"); - } - - if (lastNameContainsDigit) - { - badFields.Add("last name"); - } - - Debug.Assert(badFields.Count > 0); - - var description = badFields.ToCommaSeparatedString(finalValuesConjunction: "and") - + $" contain{(badFields.Count == 1 ? "s" : "")} a digit"; - - description = description[0..1].ToUpper() + description[1..]; - - return description; - } - } } diff --git a/TeachingRecordSystem/src/TeachingRecordSystem.Core/Dqt/QueryHandlers/FindPotentialDuplicateContactsHandler.cs b/TeachingRecordSystem/src/TeachingRecordSystem.Core/Dqt/QueryHandlers/FindPotentialDuplicateContactsHandler.cs index e9f9f1552..593c6b1d7 100644 --- a/TeachingRecordSystem/src/TeachingRecordSystem.Core/Dqt/QueryHandlers/FindPotentialDuplicateContactsHandler.cs +++ b/TeachingRecordSystem/src/TeachingRecordSystem.Core/Dqt/QueryHandlers/FindPotentialDuplicateContactsHandler.cs @@ -9,27 +9,42 @@ public class FindPotentialDuplicateContactsHandler : ICrmQueryHandler Execute(FindPotentialDuplicateContactsQuery findQuery, IOrganizationServiceAsync organizationService) { - var emails = findQuery.EmailAddresses.ToArray(); + // Find an existing active record with a TRN that matches on: + // * at least 3 of FirstName, MiddleName, LastName and BirthDate *OR* + // * email address *OR* + // * NINO. var filter = new FilterExpression(LogicalOperator.And); filter.AddCondition(Contact.Fields.StateCode, ConditionOperator.Equal, (int)ContactState.Active); + filter.AddCondition(Contact.Fields.dfeta_TRN, ConditionOperator.NotNull); var childFilters = new FilterExpression(LogicalOperator.Or); - if (TryGetMatchCombinationsFilter(out var matchCombinationsFilter)) + if (TryGetAtLeastThreeMatchesFilter(out var matchCombinationsFilter)) { childFilters.AddFilter(matchCombinationsFilter); } + var emails = findQuery.EmailAddresses.ToArray(); if (emails.Length > 0) { childFilters.AddCondition(Contact.Fields.EMailAddress1, ConditionOperator.In, emails); } + if (!string.IsNullOrEmpty(findQuery.NationalInsuranceNumber)) + { + childFilters.AddCondition(Contact.Fields.dfeta_NINumber, ConditionOperator.Equal, findQuery.NationalInsuranceNumber); + } + + if (findQuery.MatchedOnNationalInsuranceNumberContactIds.Length > 0) + { + childFilters.AddCondition(Contact.PrimaryIdAttribute, ConditionOperator.In, findQuery.MatchedOnNationalInsuranceNumberContactIds.Cast().ToArray()); + } + if (childFilters.Filters.Count == 0) { // Not enough data in the input to match on - return Array.Empty(); + return []; } filter.AddFilter(childFilters); @@ -46,11 +61,16 @@ public async Task Execute(FindPotentialD Contact.Fields.FirstName, Contact.Fields.MiddleName, Contact.Fields.LastName, + Contact.Fields.dfeta_StatedFirstName, + Contact.Fields.dfeta_StatedMiddleName, + Contact.Fields.dfeta_StatedLastName, Contact.Fields.dfeta_PreviousLastName, Contact.Fields.BirthDate, Contact.Fields.dfeta_HUSID, Contact.Fields.dfeta_SlugId, Contact.Fields.EMailAddress1, + Contact.Fields.dfeta_NINumber, + Contact.Fields.dfeta_TRN } }, Criteria = filter @@ -58,7 +78,8 @@ public async Task Execute(FindPotentialD var queryResult = await organizationService.RetrieveMultipleAsync(query); - var results = queryResult.Entities.Select(entity => entity.ToEntity()) + var results = queryResult.Entities + .Select(entity => entity.ToEntity()) .Select(match => { var attributeMatches = new[] @@ -86,6 +107,11 @@ public async Task Execute(FindPotentialD ( Attribute: Contact.Fields.EMailAddress1, Matches: findQuery.EmailAddresses.Contains(match.EMailAddress1, StringComparer.OrdinalIgnoreCase) + ), + ( + Attribute: Contact.Fields.dfeta_NINumber, + Matches: (!string.IsNullOrEmpty(findQuery.NationalInsuranceNumber) && findQuery.NationalInsuranceNumber == match.dfeta_NINumber) || + findQuery.MatchedOnNationalInsuranceNumberContactIds.Contains(match.Id) ) }; @@ -94,6 +120,7 @@ public async Task Execute(FindPotentialD return new FindPotentialDuplicateContactsResult() { ContactId = match.Id, + Trn = match.dfeta_TRN, MatchedAttributes = matchedAttributeNames, HasActiveSanctions = match.dfeta_ActiveSanctions == true, HasQtsDate = match.dfeta_QTSDate.HasValue, @@ -101,7 +128,11 @@ public async Task Execute(FindPotentialD FirstName = match.FirstName, MiddleName = match.MiddleName ?? "", LastName = match.LastName, + StatedFirstName = match.dfeta_StatedFirstName, + StatedMiddleName = match.dfeta_StatedMiddleName, + StatedLastName = match.dfeta_StatedLastName, DateOfBirth = match.BirthDate.ToDateOnlyWithDqtBstFix(isLocalTime: false), + NationalInsuranceNumber = !string.IsNullOrEmpty(match.dfeta_NINumber) ? match.dfeta_NINumber : null, EmailAddress = match.EMailAddress1 }; }) @@ -109,10 +140,8 @@ public async Task Execute(FindPotentialD return results; - bool TryGetMatchCombinationsFilter(out FilterExpression? filter) + bool TryGetAtLeastThreeMatchesFilter(out FilterExpression? filter) { - // Find an existing active record that matches on at least 3 of FirstName, MiddleName, LastName & BirthDate - var fields = new[] { (FieldName: Contact.Fields.FirstName, Value: findQuery.FirstNames), diff --git a/TeachingRecordSystem/src/TeachingRecordSystem.Core/Services/NameSynonyms/INameSynonymProvider.cs b/TeachingRecordSystem/src/TeachingRecordSystem.Core/Services/NameSynonyms/INameSynonymProvider.cs index 2cfd9653a..72fa6a124 100644 --- a/TeachingRecordSystem/src/TeachingRecordSystem.Core/Services/NameSynonyms/INameSynonymProvider.cs +++ b/TeachingRecordSystem/src/TeachingRecordSystem.Core/Services/NameSynonyms/INameSynonymProvider.cs @@ -1,4 +1,3 @@ - namespace TeachingRecordSystem.Core.Services.NameSynonyms; public interface INameSynonymProvider diff --git a/TeachingRecordSystem/tests/TeachingRecordSystem.Api.Tests/V3/V20240307/CreateTrnRequestTests.cs b/TeachingRecordSystem/tests/TeachingRecordSystem.Api.Tests/V3/V20240307/CreateTrnRequestTests.cs index 02b5cd7b2..e547cf393 100644 --- a/TeachingRecordSystem/tests/TeachingRecordSystem.Api.Tests/V3/V20240307/CreateTrnRequestTests.cs +++ b/TeachingRecordSystem/tests/TeachingRecordSystem.Api.Tests/V3/V20240307/CreateTrnRequestTests.cs @@ -174,6 +174,7 @@ public async Task Post_RequestWithExistingRequestInCrm_ReturnsConflict() var nationalInsuranceNumber = Faker.Identification.UkNationalInsuranceNumber(); var existingContact = await TestData.CreatePerson(p => p + .WithTrn() .WithFirstName(firstName) .WithMiddleName(middleName) .WithLastName(lastName) @@ -232,6 +233,7 @@ public async Task Post_RequestWithExistingRequestInDb_ReturnsConflict() var nationalInsuranceNumber = Faker.Identification.UkNationalInsuranceNumber(); var existingContact = await TestData.CreatePerson(p => p + .WithTrn() .WithFirstName(firstName) .WithMiddleName(middleName) .WithLastName(lastName) @@ -349,6 +351,7 @@ public async Task Post_RequestWithInvalidNino_ReturnsError() var invalidNino = "IvalidNi"; var existingContact = await TestData.CreatePerson(p => p + .WithTrn() .WithFirstName(firstName) .WithMiddleName(middleName) .WithLastName(lastName) @@ -468,7 +471,7 @@ public async Task Post_RequestWithoutEmail_ReturnsOk() } [Fact] - public async Task Post_PotentialDuplicateContact_CreatesContactWithoutTrnAndReturnsPendingStatus() + public async Task Post_PotentialDuplicateContactOnNamesAndDateOfBirth_CreatesContactWithoutTrnAndReturnsPendingStatus() { // Arrange var requestId = Guid.NewGuid().ToString(); @@ -480,6 +483,7 @@ public async Task Post_PotentialDuplicateContact_CreatesContactWithoutTrnAndRetu var nationalInsuranceNumber = Faker.Identification.UkNationalInsuranceNumber(); await TestData.CreatePerson(p => p + .WithTrn() .WithFirstName(firstName) .WithMiddleName(middleName) .WithLastName(lastName) @@ -533,6 +537,290 @@ await AssertEx.JsonResponseEquals( expectedStatusCode: 200); } + [Fact] + public async Task Post_PotentialDuplicateContactMatchedOnDqtNinoOnly_CreatesContactWithoutTrnAndReturnsPendingStatus() + { + // 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(); + + await TestData.CreatePerson(p => p + .WithTrn() + .WithFirstName(TestData.GenerateChangedFirstName(firstName)) + .WithMiddleName(TestData.GenerateChangedMiddleName(middleName)) + .WithLastName(TestData.GenerateChangedLastName(lastName)) + .WithDateOfBirth(TestData.GenerateChangedDateOfBirth(dateOfBirth)) + .WithNationalInsuranceNumber(nationalInsuranceNumber)); + + var requestBody = CreateJsonContent(CreateDummyRequest() with + { + RequestId = requestId, + Person = new() + { + FirstName = firstName, + MiddleName = middleName, + LastName = lastName, + DateOfBirth = dateOfBirth, + Email = email, + NationalInsuranceNumber = nationalInsuranceNumber, + } + }); + + var request = new HttpRequestMessage(HttpMethod.Post, "v3/trn-requests") + { + Content = requestBody + }; + + // Act + var response = await GetHttpClientWithApiKey().SendAsync(request); + + // Assert + var (_, createdContactId) = CrmQueryDispatcherSpy.GetSingleQuery(); + var contact = XrmFakedContext.CreateQuery().SingleOrDefault(c => c.Id == createdContactId); + Assert.NotNull(contact); + Assert.Null(contact.dfeta_TRN); + + await AssertEx.JsonResponseEquals( + response, + expected: new + { + requestId, + person = new + { + firstName, + middleName, + lastName, + email, + dateOfBirth, + nationalInsuranceNumber, + }, + trn = (string?)null, + status = "Pending" + }, + expectedStatusCode: 200); + } + + [Fact] + public async Task Post_PotentialDuplicateContactMatchedOnWorkforceDataNinoOnly_CreatesContactWithoutTrnAndReturnsPendingStatus() + { + // 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 person = await TestData.CreatePerson(p => p + .WithTrn() + .WithFirstName(TestData.GenerateChangedFirstName(firstName)) + .WithMiddleName(TestData.GenerateChangedMiddleName(middleName)) + .WithLastName(TestData.GenerateChangedLastName(lastName)) + .WithDateOfBirth(TestData.GenerateChangedDateOfBirth(dateOfBirth))); + + var establishment = await TestData.CreateEstablishment(localAuthorityCode: "321"); + await TestData.CreateTpsEmployment( + person, + establishment, + startDate: new DateOnly(2024, 1, 1), + lastKnownEmployedDate: new DateOnly(2024, 10, 1), + EmploymentType.FullTime, + lastExtractDate: new DateOnly(2024, 10, 1), + nationalInsuranceNumber: nationalInsuranceNumber); + + var requestBody = CreateJsonContent(CreateDummyRequest() with + { + RequestId = requestId, + Person = new() + { + FirstName = firstName, + MiddleName = middleName, + LastName = lastName, + DateOfBirth = dateOfBirth, + Email = email, + NationalInsuranceNumber = nationalInsuranceNumber, + } + }); + + var request = new HttpRequestMessage(HttpMethod.Post, "v3/trn-requests") + { + Content = requestBody + }; + + // Act + var response = await GetHttpClientWithApiKey().SendAsync(request); + + // Assert + var (_, createdContactId) = CrmQueryDispatcherSpy.GetSingleQuery(); + var contact = XrmFakedContext.CreateQuery().SingleOrDefault(c => c.Id == createdContactId); + Assert.NotNull(contact); + Assert.Null(contact.dfeta_TRN); + + await AssertEx.JsonResponseEquals( + response, + expected: new + { + requestId, + person = new + { + firstName, + middleName, + lastName, + email, + dateOfBirth, + nationalInsuranceNumber, + }, + trn = (string?)null, + status = "Pending" + }, + expectedStatusCode: 200); + } + + [Fact] + public async Task Post_DuplicateContactMatchedOnDqtNinoAndDateOfBirth_ReturnsExistingTrnAndDoesNotCreateNewContact() + { + // 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 person = await TestData.CreatePerson(p => p + .WithTrn() + .WithFirstName(TestData.GenerateChangedFirstName(firstName)) + .WithMiddleName(TestData.GenerateChangedMiddleName(middleName)) + .WithLastName(TestData.GenerateChangedLastName(lastName)) + .WithDateOfBirth(dateOfBirth) + .WithNationalInsuranceNumber(nationalInsuranceNumber)); + + var requestBody = CreateJsonContent(CreateDummyRequest() with + { + RequestId = requestId, + Person = new() + { + FirstName = firstName, + MiddleName = middleName, + LastName = lastName, + DateOfBirth = dateOfBirth, + Email = email, + NationalInsuranceNumber = nationalInsuranceNumber, + } + }); + + var request = new HttpRequestMessage(HttpMethod.Post, "v3/trn-requests") + { + Content = requestBody + }; + + // Act + var response = await GetHttpClientWithApiKey().SendAsync(request); + + // Assert + Assert.Empty(CrmQueryDispatcherSpy.GetAllQueries()); + + await AssertEx.JsonResponseEquals( + response, + expected: new + { + requestId, + person = new + { + firstName = person.FirstName, + middleName = person.MiddleName, + lastName = person.LastName, + email = person.Email, + dateOfBirth = person.DateOfBirth, + nationalInsuranceNumber = person.NationalInsuranceNumber, + }, + trn = person.Trn, + status = "Completed" + }, + expectedStatusCode: 200); + } + + [Fact] + public async Task Post_DuplicateContactMatchedOnWorkforceDataNinoAndDqtDateOfBirth_ReturnsExistingTrnAndDoesNotCreateNewContact() + { + // 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 person = await TestData.CreatePerson(p => p + .WithTrn() + .WithFirstName(TestData.GenerateChangedFirstName(firstName)) + .WithMiddleName(TestData.GenerateChangedMiddleName(middleName)) + .WithLastName(TestData.GenerateChangedLastName(lastName)) + .WithDateOfBirth(dateOfBirth)); + + var establishment = await TestData.CreateEstablishment(localAuthorityCode: "321"); + await TestData.CreateTpsEmployment( + person, + establishment, + startDate: new DateOnly(2024, 1, 1), + lastKnownEmployedDate: new DateOnly(2024, 10, 1), + EmploymentType.FullTime, + lastExtractDate: new DateOnly(2024, 10, 1), + nationalInsuranceNumber: nationalInsuranceNumber); + + var requestBody = CreateJsonContent(CreateDummyRequest() with + { + RequestId = requestId, + Person = new() + { + FirstName = firstName, + MiddleName = middleName, + LastName = lastName, + DateOfBirth = dateOfBirth, + Email = email, + NationalInsuranceNumber = nationalInsuranceNumber, + } + }); + + var request = new HttpRequestMessage(HttpMethod.Post, "v3/trn-requests") + { + Content = requestBody + }; + + // Act + var response = await GetHttpClientWithApiKey().SendAsync(request); + + // Assert + Assert.Empty(CrmQueryDispatcherSpy.GetAllQueries()); + + await AssertEx.JsonResponseEquals( + response, + expected: new + { + requestId, + person = new + { + firstName = person.FirstName, + middleName = person.MiddleName, + lastName = person.LastName, + email = person.Email, + dateOfBirth = person.DateOfBirth, + nationalInsuranceNumber = person.NationalInsuranceNumber, + }, + trn = person.Trn, + status = "Completed" + }, + expectedStatusCode: 200); + } + private static CreateTrnRequestRequest CreateDummyRequest() => new() { RequestId = Guid.NewGuid().ToString(), diff --git a/TeachingRecordSystem/tests/TeachingRecordSystem.Api.Tests/V3/V20240606/CreateTrnRequestTests.cs b/TeachingRecordSystem/tests/TeachingRecordSystem.Api.Tests/V3/V20240606/CreateTrnRequestTests.cs index 866e7004e..e16e938bc 100644 --- a/TeachingRecordSystem/tests/TeachingRecordSystem.Api.Tests/V3/V20240606/CreateTrnRequestTests.cs +++ b/TeachingRecordSystem/tests/TeachingRecordSystem.Api.Tests/V3/V20240606/CreateTrnRequestTests.cs @@ -26,6 +26,7 @@ public async Task Post_WithMultipleEmailAddresses_MatchesByEmail() var email2 = Faker.Internet.Email(); await TestData.CreatePerson(p => p + .WithTrn() .WithFirstName(firstName) .WithMiddleName(middleName) .WithLastName(lastName) diff --git a/TeachingRecordSystem/tests/TeachingRecordSystem.Core.Dqt.CrmIntegrationTests/QueryTests/CreateContactTests.cs b/TeachingRecordSystem/tests/TeachingRecordSystem.Core.Dqt.CrmIntegrationTests/QueryTests/CreateContactTests.cs index 6c6080162..c79a83c3a 100644 --- a/TeachingRecordSystem/tests/TeachingRecordSystem.Core.Dqt.CrmIntegrationTests/QueryTests/CreateContactTests.cs +++ b/TeachingRecordSystem/tests/TeachingRecordSystem.Core.Dqt.CrmIntegrationTests/QueryTests/CreateContactTests.cs @@ -155,6 +155,7 @@ public async Task QueryWithMatchedDuplicateContactName_ExecutesSuccessfully_Crea new FindPotentialDuplicateContactsResult() { ContactId = createdTeacherId1, + Trn = trn1, MatchedAttributes = [Contact.Fields.FirstName, Contact.Fields.MiddleName, Contact.Fields.LastName], HasActiveSanctions = false, HasEytsDate = false, @@ -162,8 +163,12 @@ public async Task QueryWithMatchedDuplicateContactName_ExecutesSuccessfully_Crea FirstName = firstName, MiddleName = middleName, LastName = lastName, + StatedFirstName = firstName, + StatedMiddleName = middleName, + StatedLastName = lastName, DateOfBirth = dob, - EmailAddress = email + EmailAddress = email, + NationalInsuranceNumber = ni } ] }; diff --git a/TeachingRecordSystem/tests/TeachingRecordSystem.Core.Dqt.CrmIntegrationTests/QueryTests/FindPotentialDuplicateContactsTests.cs b/TeachingRecordSystem/tests/TeachingRecordSystem.Core.Dqt.CrmIntegrationTests/QueryTests/FindPotentialDuplicateContactsTests.cs index 45ae5bc2a..6baf4d02f 100644 --- a/TeachingRecordSystem/tests/TeachingRecordSystem.Core.Dqt.CrmIntegrationTests/QueryTests/FindPotentialDuplicateContactsTests.cs +++ b/TeachingRecordSystem/tests/TeachingRecordSystem.Core.Dqt.CrmIntegrationTests/QueryTests/FindPotentialDuplicateContactsTests.cs @@ -38,7 +38,9 @@ public async Task MatchOnNameAndDob_ReturnsMatch() MiddleName = middleName, LastName = lastName, DateOfBirth = dob.ToDateOnlyWithDqtBstFix(isLocalTime: false), - EmailAddresses = [] + EmailAddresses = [], + NationalInsuranceNumber = null, + MatchedOnNationalInsuranceNumberContactIds = [] }; // Act @@ -61,7 +63,7 @@ public async Task MatchOnEmail_ReturnsMatch() // Arrange var email = $"{Guid.NewGuid()}@test.com"; - var person = await _dataScope.TestData.CreatePerson(p => p.WithEmail(email)); + var person = await _dataScope.TestData.CreatePerson(p => p.WithTrn().WithEmail(email)); var query = new FindPotentialDuplicateContactsQuery() { @@ -69,7 +71,9 @@ public async Task MatchOnEmail_ReturnsMatch() MiddleName = "", LastName = Faker.Name.Last(), DateOfBirth = DateOnly.FromDateTime(Faker.Identification.DateOfBirth()), - EmailAddresses = [email] + EmailAddresses = [email], + NationalInsuranceNumber = null, + MatchedOnNationalInsuranceNumberContactIds = [] }; // Act