Skip to content

Commit

Permalink
WIP: bulk person API
Browse files Browse the repository at this point in the history
  • Loading branch information
gunndabad committed Aug 19, 2024
1 parent ced2453 commit 6a92524
Show file tree
Hide file tree
Showing 26 changed files with 503 additions and 128 deletions.
2 changes: 2 additions & 0 deletions TeachingRecordSystem/src/TeachingRecordSystem.Api/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
using TeachingRecordSystem.Api.Infrastructure.Security;
using TeachingRecordSystem.Api.Validation;
using TeachingRecordSystem.Core.Dqt;
using TeachingRecordSystem.Core.Dqt.Models;
using TeachingRecordSystem.Core.Infrastructure;
using TeachingRecordSystem.Core.Services.Certificates;
using TeachingRecordSystem.Core.Services.GetAnIdentityApi;
Expand Down Expand Up @@ -219,6 +220,7 @@ public static void Main(string[] args)
services.AddMemoryCache();
services.AddSingleton<AddTrnToSentryScopeResourceFilter>();
services.AddTransient<TrnRequestHelper>();
services.AddSingleton<PreviousNameHelper>();

builder.Services.AddOptions<EvidenceFilesOptions>()
.Bind(builder.Configuration.GetSection("EvidenceFiles"))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,15 +20,18 @@ public record FindPersonByLastNameAndDateOfBirthResultItem
public required string LastName { get; init; }
public required IReadOnlyCollection<SanctionInfo> Sanctions { get; init; }
public required IReadOnlyCollection<NameInfo> PreviousNames { get; init; }
public required InductionStatusInfo? InductionStatus { get; init; }
public required QtsInfo? Qts { get; init; }
}

public class FindPersonByLastNameAndDateOfBirthHandler(ICrmQueryDispatcher crmQueryDispatcher, IConfiguration configuration)
public class FindPersonByLastNameAndDateOfBirthHandler(
ICrmQueryDispatcher crmQueryDispatcher,
PreviousNameHelper previousNameHelper,
ReferenceDataCache referenceDataCache)
{
private readonly TimeSpan _concurrentNameChangeWindow = TimeSpan.FromSeconds(configuration.GetValue("ConcurrentNameChangeWindowSeconds", 5));

public async Task<FindPersonByLastNameAndDateOfBirthResult> Handle(FindPersonByLastNameAndDateOfBirthCommand command)
{
var contacts = await crmQueryDispatcher.ExecuteQuery(
var matched = await crmQueryDispatcher.ExecuteQuery(
new GetActiveContactsByLastNameAndDateOfBirthQuery(
command.LastName!,
command.DateOfBirth!.Value,
Expand All @@ -40,9 +43,10 @@ public async Task<FindPersonByLastNameAndDateOfBirthResult> Handle(FindPersonByL
Contact.Fields.LastName,
Contact.Fields.dfeta_StatedFirstName,
Contact.Fields.dfeta_StatedMiddleName,
Contact.Fields.dfeta_StatedLastName)));
Contact.Fields.dfeta_StatedLastName,
Contact.Fields.dfeta_InductionStatus)));

var contactsById = contacts.ToDictionary(r => r.Id, r => r);
var contactsById = matched.ToDictionary(r => r.Id, r => r);

var sanctions = await crmQueryDispatcher.ExecuteQuery(
new GetSanctionsByContactIdsQuery(
Expand All @@ -53,35 +57,56 @@ public async Task<FindPersonByLastNameAndDateOfBirthResult> Handle(FindPersonByL
var previousNames = (await crmQueryDispatcher.ExecuteQuery(new GetPreviousNamesByContactIdsQuery(contactsById.Keys)))
.ToDictionary(
kvp => kvp.Key,
kvp => PreviousNameHelper.GetFullPreviousNames(kvp.Value, contactsById[kvp.Key], _concurrentNameChangeWindow));
kvp => previousNameHelper.GetFullPreviousNames(kvp.Value, contactsById[kvp.Key]));

var qtsRegistrations = await crmQueryDispatcher.ExecuteQuery(
new GetActiveQtsRegistrationsByContactIdsQuery(
contactsById.Keys,
new ColumnSet(
dfeta_qtsregistration.Fields.CreatedOn,
dfeta_qtsregistration.Fields.dfeta_EarlyYearsStatusId,
dfeta_qtsregistration.Fields.dfeta_EYTSDate,
dfeta_qtsregistration.Fields.dfeta_QTSDate,
dfeta_qtsregistration.Fields.dfeta_PersonId,
dfeta_qtsregistration.Fields.dfeta_TeacherStatusId)));

return new FindPersonByLastNameAndDateOfBirthResult(
Total: contacts.Length,
Items: contacts.Select(r => new FindPersonByLastNameAndDateOfBirthResultItem()
{
Trn = r.dfeta_TRN,
DateOfBirth = r.BirthDate!.Value.ToDateOnlyWithDqtBstFix(isLocalTime: false),
FirstName = r.ResolveFirstName(),
MiddleName = r.ResolveMiddleName(),
LastName = r.ResolveLastName(),
Sanctions = sanctions[r.Id]
.Where(s => Constants.ExposableSanctionCodes.Contains(s.SanctionCode))
.Select(s => new SanctionInfo()
{
Code = s.SanctionCode,
StartDate = s.Sanction.dfeta_StartDate?.ToDateOnlyWithDqtBstFix(isLocalTime: true)
})
.AsReadOnly(),
PreviousNames = previousNames[r.Id]
.Select(name => new NameInfo()
{
FirstName = name.FirstName,
MiddleName = name.MiddleName,
LastName = name.LastName
})
.AsReadOnly()
})
.OrderBy(c => c.Trn)
.AsReadOnly());
Total: matched.Length,
Items: await matched
.ToAsyncEnumerable()
.SelectAwait(async r => new FindPersonByLastNameAndDateOfBirthResultItem()
{
Trn = r.dfeta_TRN,
DateOfBirth = r.BirthDate!.Value.ToDateOnlyWithDqtBstFix(isLocalTime: false),
FirstName = r.ResolveFirstName(),
MiddleName = r.ResolveMiddleName(),
LastName = r.ResolveLastName(),
Sanctions = sanctions[r.Id]
.Where(s => Constants.ExposableSanctionCodes.Contains(s.SanctionCode))
.Select(s => new SanctionInfo()
{
Code = s.SanctionCode,
StartDate = s.Sanction.dfeta_StartDate?.ToDateOnlyWithDqtBstFix(isLocalTime: true)
})
.AsReadOnly(),
PreviousNames = previousNames[r.Id]
.Select(name => new NameInfo()
{
FirstName = name.FirstName,
MiddleName = name.MiddleName,
LastName = name.LastName
})
.AsReadOnly(),
InductionStatus = r.dfeta_InductionStatus?.ConvertToInductionStatus() is InductionStatus inductionStatus ?
new InductionStatusInfo()
{
Status = inductionStatus,
StatusDescription = inductionStatus.GetDescription()
} :
null,
Qts = await QtsInfo.Create(qtsRegistrations[r.Id].OrderBy(qr => qr.CreatedOn).FirstOrDefault(), referenceDataCache)
})
.OrderBy(c => c.Trn)
.ToListAsync());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
using System.Collections.Immutable;
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 FindPersonsByTrnAndDateOfBirthCommand(IEnumerable<(string Trn, DateOnly DateOfBirth)> Persons);

public record FindPersonsByTrnAndDateOfBirthResult(int Total, IReadOnlyCollection<FindPersonsByTrnAndDateOfBirthResultItem> Items);

public record FindPersonsByTrnAndDateOfBirthResultItem
{
public required string Trn { get; init; }
public required DateOnly DateOfBirth { get; init; }
public required string FirstName { get; init; }
public required string MiddleName { get; init; }
public required string LastName { get; init; }
public required IReadOnlyCollection<SanctionInfo> Sanctions { get; init; }
public required IReadOnlyCollection<NameInfo> PreviousNames { get; init; }
public required InductionStatusInfo? InductionStatus { get; init; }
public required QtsInfo? Qts { get; init; }
}

public class FindPersonsByTrnAndDateOfBirthHandler(
ICrmQueryDispatcher crmQueryDispatcher,
PreviousNameHelper previousNameHelper,
ReferenceDataCache referenceDataCache)
{
public async Task<FindPersonsByTrnAndDateOfBirthResult> Handle(FindPersonsByTrnAndDateOfBirthCommand command)
{
var contacts = await crmQueryDispatcher.ExecuteQuery(
new GetActiveContactsByTrnsQuery(
command.Persons.Select(p => p.Trn),
new ColumnSet(
Contact.Fields.dfeta_TRN,
Contact.Fields.BirthDate,
Contact.Fields.FirstName,
Contact.Fields.MiddleName,
Contact.Fields.LastName,
Contact.Fields.dfeta_StatedFirstName,
Contact.Fields.dfeta_StatedMiddleName,
Contact.Fields.dfeta_StatedLastName,
Contact.Fields.dfeta_InductionStatus)));

// Remove any results where the request DOB doesn't match the contact's DOB
// (we can't easily do this in the query itself).
var matched = contacts
.Where(kvp => kvp.Value is not null)
.Where(kvp => command.Persons.Any(p => p.Trn == kvp.Key && p.DateOfBirth == kvp.Value!.BirthDate?.ToDateOnlyWithDqtBstFix(isLocalTime: false)))
.Select(kvp => kvp.Value!)
.ToArray();

var contactsById = matched.ToDictionary(c => c.Id, c => c);

var sanctions = await crmQueryDispatcher.ExecuteQuery(
new GetSanctionsByContactIdsQuery(
contactsById.Keys,
ActiveOnly: true,
new()));

var previousNames = (await crmQueryDispatcher.ExecuteQuery(new GetPreviousNamesByContactIdsQuery(contactsById.Keys)))
.ToDictionary(
kvp => kvp.Key,
kvp => previousNameHelper.GetFullPreviousNames(kvp.Value, contactsById[kvp.Key]));

var qtsRegistrations = await crmQueryDispatcher.ExecuteQuery(
new GetActiveQtsRegistrationsByContactIdsQuery(
contactsById.Keys,
new ColumnSet(
dfeta_qtsregistration.Fields.CreatedOn,
dfeta_qtsregistration.Fields.dfeta_EarlyYearsStatusId,
dfeta_qtsregistration.Fields.dfeta_EYTSDate,
dfeta_qtsregistration.Fields.dfeta_QTSDate,
dfeta_qtsregistration.Fields.dfeta_PersonId,
dfeta_qtsregistration.Fields.dfeta_TeacherStatusId)));

return new FindPersonsByTrnAndDateOfBirthResult(
Total: matched.Length,
Items: await matched
.ToAsyncEnumerable()
.SelectAwait(async r => new FindPersonsByTrnAndDateOfBirthResultItem()
{
Trn = r.dfeta_TRN,
DateOfBirth = r.BirthDate!.Value.ToDateOnlyWithDqtBstFix(isLocalTime: false),
FirstName = r.ResolveFirstName(),
MiddleName = r.ResolveMiddleName(),
LastName = r.ResolveLastName(),
Sanctions = sanctions[r.Id]
.Where(s => Constants.ExposableSanctionCodes.Contains(s.SanctionCode))
.Select(s => new SanctionInfo()
{
Code = s.SanctionCode,
StartDate = s.Sanction.dfeta_StartDate?.ToDateOnlyWithDqtBstFix(isLocalTime: true)
})
.AsReadOnly(),
PreviousNames = previousNames[r.Id]
.Select(name => new NameInfo()
{
FirstName = name.FirstName,
MiddleName = name.MiddleName,
LastName = name.LastName
})
.AsReadOnly(),
InductionStatus = r.dfeta_InductionStatus?.ConvertToInductionStatus() is InductionStatus inductionStatus ?
new InductionStatusInfo()
{
Status = inductionStatus,
StatusDescription = inductionStatus.GetDescription()
} :
null,
Qts = await QtsInfo.Create(qtsRegistrations[r.Id].OrderBy(qr => qr.CreatedOn).FirstOrDefault(), referenceDataCache)
})
.OrderBy(c => c.Trn)
.ToListAsync());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ public record GetPersonResult
public required Option<bool> PendingNameChange { get; init; }
public required Option<bool> PendingDateOfBirthChange { get; init; }
public required string? EmailAddress { get; set; }
public required GetPersonResultQts? Qts { get; init; }
public required QtsInfo? Qts { get; init; }
public required GetPersonResultEyts? Eyts { get; init; }
public required Option<GetPersonResultInduction?> Induction { get; init; }
public required Option<IReadOnlyCollection<GetPersonResultInitialTeacherTraining>> InitialTeacherTraining { get; init; }
Expand All @@ -52,13 +52,6 @@ public record GetPersonResult
public required Option<bool> AllowIdSignInWithProhibitions { get; init; }
}

public record GetPersonResultQts
{
public required DateOnly? Awarded { get; init; }
public required string CertificateUrl { get; init; }
public required string? StatusDescription { get; init; }
}

public record GetPersonResultEyts
{
public required DateOnly? Awarded { get; init; }
Expand Down Expand Up @@ -161,12 +154,10 @@ public class GetPersonHandler(
ICrmQueryDispatcher crmQueryDispatcher,
ReferenceDataCache referenceDataCache,
IDataverseAdapter dataverseAdapter,
IConfiguration configuration)
PreviousNameHelper previousNameHelper)
{
public async Task<GetPersonResult?> Handle(GetPersonCommand command)
{
var concurrentNameChangeWindow = TimeSpan.FromSeconds(configuration.GetValue<int>("ConcurrentNameChangeWindowSeconds", 5));

var contactDetail = await crmQueryDispatcher.ExecuteQuery(
new GetActiveContactDetailByTrnQuery(
command.Trn,
Expand Down Expand Up @@ -330,7 +321,7 @@ async Task<SanctionResult[]> GetSanctions()
GetSanctions() :
null;

IEnumerable<NameInfo>? previousNames = PreviousNameHelper.GetFullPreviousNames(contactDetail.PreviousNames, contactDetail.Contact, concurrentNameChangeWindow)
IEnumerable<NameInfo>? previousNames = previousNameHelper.GetFullPreviousNames(contactDetail.PreviousNames, contactDetail.Contact)
.Select(name => new NameInfo()
{
FirstName = name.FirstName,
Expand All @@ -356,9 +347,10 @@ async Task<SanctionResult[]> GetSanctions()

var qts = qtsRegistrations.OrderByDescending(x => x.CreatedOn).FirstOrDefault(qts => qts.dfeta_QTSDate is not null);
var eyts = qtsRegistrations.OrderByDescending(x => x.CreatedOn).FirstOrDefault(qts => qts.dfeta_EYTSDate is not null);
var eytsTeacherStatus = eyts != null ? await dataverseAdapter.GetEarlyYearsStatus(eyts!.dfeta_EarlyYearsStatusId.Id) : null;
var allTeacherStatuses = await referenceDataCache.GetTeacherStatuses();
var qtsStatus = qts != null ? allTeacherStatuses.Single(x => x.Id == qts.dfeta_TeacherStatusId.Id) : null;
var allEarlyYearsStatuses = await referenceDataCache.GetEytsStatuses();
var eytsStatus = eyts is not null ? allEarlyYearsStatuses.Single(x => x.Id == eyts.dfeta_EarlyYearsStatusId.Id) : null;
var qtsStatus = qts is not null ? allTeacherStatuses.Single(x => x.Id == qts.dfeta_TeacherStatusId.Id) : null;

var allowIdSignInWithProhibitions = command.Include.HasFlag(GetPersonCommandIncludes.AllowIdSignInWithProhibitions) ?
Option.Some(contact.dfeta_AllowIDSignInWithProhibitions == true) :
Expand All @@ -374,8 +366,8 @@ async Task<SanctionResult[]> GetSanctions()
NationalInsuranceNumber = contact.dfeta_NINumber,
PendingNameChange = command.Include.HasFlag(GetPersonCommandIncludes.PendingDetailChanges) ? Option.Some((await getPendingDetailChangesTask!).PendingNameRequest) : default,
PendingDateOfBirthChange = command.Include.HasFlag(GetPersonCommandIncludes.PendingDetailChanges) ? Option.Some((await getPendingDetailChangesTask!).PendingDateOfBirthRequest) : default,
Qts = MapQts(qts?.dfeta_QTSDate?.ToDateOnlyWithDqtBstFix(isLocalTime: true), qtsStatus != null ? GetQtsStatusDescription(qtsStatus!.dfeta_Value!, qtsStatus.dfeta_name) : null),
Eyts = MapEyts(eyts?.dfeta_EYTSDate?.ToDateOnlyWithDqtBstFix(isLocalTime: true), eytsTeacherStatus != null ? GetEytsStatusDescription(eytsTeacherStatus!.dfeta_Value!) : null),
Qts = await QtsInfo.Create(qts, referenceDataCache),
Eyts = MapEyts(eyts?.dfeta_EYTSDate?.ToDateOnlyWithDqtBstFix(isLocalTime: true), eytsStatus != null ? GetEytsStatusDescription(eytsStatus!.dfeta_Value!) : null),
EmailAddress = contact.EMailAddress1,
Induction = command.Include.HasFlag(GetPersonCommandIncludes.Induction) ?
Option.Some(MapInduction(await getInductionTask!, contact)) :
Expand Down Expand Up @@ -443,39 +435,6 @@ async Task<SanctionResult[]> GetSanctions()
_ => throw new ArgumentException($"Unregonized EYTS status: '{value}'.", nameof(value))
};

private static string GetQtsStatusDescription(string value, string statusDescription) => value switch
{
"28" => "Qualified",
"50" => "Qualified",
"67" => "Qualified",
"68" => "Qualified",
"69" => "Qualified",
"71" => "Qualified",
"87" => "Qualified",
"90" => "Qualified",
"100" => "Qualified",
"103" => "Qualified",
"104" => "Qualified",
"206" => "Qualified",
"211" => "Trainee teacher",
"212" => "Assessment only route candidate",
"213" => "Qualified",
"214" => "Partial qualified teacher status",
"223" => "Qualified",
_ when statusDescription.StartsWith("Qualified teacher", StringComparison.InvariantCultureIgnoreCase) => "Qualified",
_ => throw new ArgumentException($"Unregonized QTS status: '{value}'.", nameof(value))
};

private static GetPersonResultQts? MapQts(DateOnly? qtsDate, string? statusDescription) =>
statusDescription is not null ?
new GetPersonResultQts()
{
Awarded = qtsDate,
CertificateUrl = "/v3/certificates/qts",
StatusDescription = statusDescription
} :
null;

private static GetPersonResultEyts? MapEyts(DateOnly? eytsDate, string? statusDescription) =>
statusDescription != null ?
new GetPersonResultEyts()
Expand All @@ -486,7 +445,6 @@ statusDescription is not null ?
} :
null;


private static GetPersonResultInduction? MapInduction((dfeta_induction Induction, dfeta_inductionperiod[] Inductionperiods) data, TeachingRecordSystem.Core.Dqt.Models.Contact contact)
{
var inductionStatus = contact.dfeta_InductionStatus?.ConvertToInductionStatus();
Expand Down
Loading

0 comments on commit 6a92524

Please sign in to comment.