Skip to content

Commit

Permalink
Consider NINO when looking for duplicates (#1657)
Browse files Browse the repository at this point in the history
  • Loading branch information
gunndabad authored Nov 6, 2024
1 parent d4b9e67 commit c2a910f
Show file tree
Hide file tree
Showing 10 changed files with 412 additions and 105 deletions.
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -20,6 +22,7 @@ public record CreateTrnRequestCommand
}

public class CreateTrnRequestHandler(
TrsDbContext dbContext,
ICrmQueryDispatcher crmQueryDispatcher,
TrnRequestHelper trnRequestHelper,
ICurrentClientProvider currentClientProvider,
Expand All @@ -31,37 +34,76 @@ public async Task<TrnRequestInfo> 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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ public class CreateContactQuery : ICrmQuery<Guid>
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<FindPotentialDuplicateContactsResult> PotentialDuplicates { get; init; }
public required string? Trn { get; init; }
public required string? TrnRequestId { get; init; }
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,18 +7,27 @@ public record FindPotentialDuplicateContactsQuery : ICrmQuery<FindPotentialDupli
public required string LastName { get; init; }
public required DateOnly DateOfBirth { get; init; }
public required IEnumerable<string> 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<string> 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; }
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ public async Task<Guid> 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,
Expand All @@ -27,51 +29,24 @@ public async Task<Guid> 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();

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();

Expand All @@ -92,20 +67,20 @@ string GetDescription()
sb.AppendLine("Potential duplicate");
sb.AppendLine("Matched on");

foreach (var matchedAttribute in duplicate.MatchedAttributes ?? Array.Empty<string>())
foreach (var matchedAttribute in duplicate.MatchedAttributes)
{
sb.AppendLine(matchedAttribute switch
{
Contact.Fields.FirstName => $" - First name: '{duplicate.FirstName}'",
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<string>();

if (duplicate.HasActiveSanctions)
Expand All @@ -117,7 +92,8 @@ string GetDescription()
{
additionalFlags.Add("QTS date");
}
else if (duplicate.HasEytsDate)

if (duplicate.HasEytsDate)
{
additionalFlags.Add("EYTS date");
}
Expand All @@ -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<string>();

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;
}
}
}
Loading

0 comments on commit c2a910f

Please sign in to comment.