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 14, 2024
1 parent 2aef5e5 commit de1a4b6
Show file tree
Hide file tree
Showing 18 changed files with 255 additions and 37 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 @@ -22,10 +22,8 @@ public record FindPersonByLastNameAndDateOfBirthResultItem
public required IReadOnlyCollection<NameInfo> PreviousNames { get; init; }
}

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

public async Task<FindPersonByLastNameAndDateOfBirthResult> Handle(FindPersonByLastNameAndDateOfBirthCommand command)
{
var contacts = await crmQueryDispatcher.ExecuteQuery(
Expand Down Expand Up @@ -53,7 +51,7 @@ 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]));

return new FindPersonByLastNameAndDateOfBirthResult(
Total: contacts.Length,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
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 class FindPersonsByTrnAndDateOfBirthHandler(ICrmQueryDispatcher crmQueryDispatcher, PreviousNameHelper previousNameHelper)
{
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)));

// 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]));

return new FindPersonsByTrnAndDateOfBirthResult(
Total: matched.Length,
Items: matched.Select(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()
})
.OrderBy(c => c.Trn)
.AsReadOnly());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -161,12 +161,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 +328,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 Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@

namespace TeachingRecordSystem.Api.V3.V20240101.Responses;

[AutoMap(typeof(FindPersonByLastNameAndDateOfBirthResult))]
public record FindTeachersResponse
{
public required int Total { get; init; }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@

namespace TeachingRecordSystem.Api.V3.V20240606.Responses;

[AutoMap(typeof(FindPersonByLastNameAndDateOfBirthResult))]
public record FindPersonResponse
{
public required int Total { get; init; }
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Swashbuckle.AspNetCore.Annotations;
using TeachingRecordSystem.Api.Infrastructure.Security;
using TeachingRecordSystem.Api.V3.Core.Operations;
using TeachingRecordSystem.Api.V3.V20240814.Requests;
using TeachingRecordSystem.Api.V3.V20240814.Responses;

namespace TeachingRecordSystem.Api.V3.V20240814.Controllers;

[Route("persons")]
public class PersonsController(IMapper mapper) : ControllerBase
{
[HttpPost("find")]
[SwaggerOperation(
OperationId = "FindPersons",
Summary = "Find persons",
Description = "Finds persons matching the specified criteria.")]
[ProducesResponseType(typeof(FindPersonsResponse), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)]
[Authorize(Policy = AuthorizationPolicies.GetPerson)]
public async Task<IActionResult> FindTeachers(
[FromBody] FindPersonsRequest request,
[FromServices] FindPersonsByTrnAndDateOfBirthHandler handler)
{
var command = new FindPersonsByTrnAndDateOfBirthCommand(request.Persons.Select(p => (p.Trn, p.DateOfBirth)));
var result = await handler.Handle(command);
var response = mapper.Map<FindPersonsResponse>(result);
return Ok(response);
}

[HttpGet("")]
[SwaggerOperation(
OperationId = "FindPerson",
Summary = "Find person",
Description = "Finds a person matching the specified criteria.")]
[ProducesResponseType(typeof(FindPersonResponse), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)]
[Authorize(Policy = AuthorizationPolicies.GetPerson)]
public async Task<IActionResult> FindTeachers(
FindPersonRequest request,
[FromServices] FindPersonByLastNameAndDateOfBirthHandler handler)
{
var command = new FindPersonByLastNameAndDateOfBirthCommand(request.LastName!, request.DateOfBirth!.Value);
var result = await handler.Handle(command);

var response = new FindPersonResponse()
{
Total = result.Total,
Query = request,
Results = result.Items.Select(mapper.Map<FindPersonResponseResult>).AsReadOnly()
};

return Ok(response);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
using Microsoft.AspNetCore.Mvc;

namespace TeachingRecordSystem.Api.V3.V20240814.Requests;

public record FindPersonRequest
{
[FromQuery(Name = "findBy")]
public FindPersonFindBy FindBy { get; init; }
[FromQuery(Name = "lastName")]
public string? LastName { get; init; }
[FromQuery(Name = "dateOfBirth")]
public DateOnly? DateOfBirth { get; init; }
}

public enum FindPersonFindBy
{
LastNameAndDateOfBirth = 1
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
namespace TeachingRecordSystem.Api.V3.VNext.Requests;
namespace TeachingRecordSystem.Api.V3.V20240814.Requests;

public record FindPersonsRequest
{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
using TeachingRecordSystem.Api.V3.Core.Operations;
using TeachingRecordSystem.Api.V3.V20240101.ApiModels;
using TeachingRecordSystem.Api.V3.V20240814.Requests;

namespace TeachingRecordSystem.Api.V3.V20240814.Responses;

public record FindPersonResponse
{
public required int Total { get; init; }
public required FindPersonRequest Query { get; init; }
public required IReadOnlyCollection<FindPersonResponseResult> Results { get; init; }
}

[AutoMap(typeof(FindPersonByLastNameAndDateOfBirthResultItem))]
public record FindPersonResponseResult
{
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; }
}
Original file line number Diff line number Diff line change
@@ -1,13 +1,18 @@
using AutoMapper.Configuration.Annotations;
using TeachingRecordSystem.Api.V3.Core.Operations;
using TeachingRecordSystem.Api.V3.V20240101.ApiModels;

namespace TeachingRecordSystem.Api.V3.VNext.Responses;
namespace TeachingRecordSystem.Api.V3.V20240814.Responses;

[AutoMap(typeof(FindPersonsByTrnAndDateOfBirthResult))]
public record FindPersonsResponse
{
public required int Total { get; init; }
[SourceMember(nameof(FindPersonsByTrnAndDateOfBirthResult.Items))]
public required IReadOnlyCollection<FindPersonsResponseResult> Results { get; init; }
}

[AutoMap(typeof(FindPersonsByTrnAndDateOfBirthResultItem))]
public record FindPersonsResponseResult
{
public required string Trn { get; init; }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
using TeachingRecordSystem.Api.V3.Core.Operations;
using TeachingRecordSystem.Api.V3.VNext.ApiModels;
using TeachingRecordSystem.Api.V3.VNext.Requests;
using TeachingRecordSystem.Api.V3.VNext.Responses;

namespace TeachingRecordSystem.Api.V3.VNext.Controllers;

Expand Down Expand Up @@ -47,17 +46,4 @@ public async Task<IActionResult> GetQtls(
var response = mapper.Map<QtlsInfo?>(result);
return response is not null ? Ok(response) : NotFound();
}

[HttpPost("find")]
[SwaggerOperation(
OperationId = "FindPersons",
Summary = "Find persons",
Description = "Finds persons matching the specified criteria.")]
[ProducesResponseType(typeof(FindPersonsResponse), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)]
[Authorize(Policy = AuthorizationPolicies.GetPerson)]
public Task<IActionResult> FindTeachers([FromBody] FindPersonsRequest request)
{
throw new NotImplementedException();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ public static class VersionRegistry
V3MinorVersions.V20240412,
V3MinorVersions.V20240416,
V3MinorVersions.V20240606,
V3MinorVersions.V20240814,
V3MinorVersions.VNext,
];

Expand Down Expand Up @@ -54,6 +55,7 @@ public static class V3MinorVersions
public const string V20240412 = "20240412";
public const string V20240416 = "20240416";
public const string V20240606 = "20240606";
public const string V20240814 = "20240814";
public const string VNext = VNextVersion;
}
}
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
namespace TeachingRecordSystem.Core.Dqt.Models;
using Microsoft.Extensions.Configuration;
using FullName = (string FirstName, string MiddleName, string LastName);

public static class PreviousNameHelper
namespace TeachingRecordSystem.Core.Dqt.Models;

public class PreviousNameHelper(IConfiguration configuration)
{
public static FullName[] GetFullPreviousNames(
IEnumerable<dfeta_previousname> previousNames,
Contact contact,
TimeSpan concurrentNameChangeWindow)
public FullName[] GetFullPreviousNames(IEnumerable<dfeta_previousname> previousNames, Contact contact)
{
var concurrentNameChangeWindow = TimeSpan.FromSeconds(configuration.GetValue("ConcurrentNameChangeWindowSeconds", 5));

var result = new List<FullName>();

var currentFirstName = contact.FirstName!;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
using Microsoft.Xrm.Sdk.Query;

namespace TeachingRecordSystem.Core.Dqt.Queries;

public record GetActiveContactsByTrnsQuery(IEnumerable<string> Trns, ColumnSet ColumnSet) :
ICrmQuery<IDictionary<string, Contact?>>;
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
using AngleSharp.Common;
using Microsoft.PowerPlatform.Dataverse.Client;
using Microsoft.Xrm.Sdk.Query;
using TeachingRecordSystem.Core.Dqt.Queries;

namespace TeachingRecordSystem.Core.Dqt.QueryHandlers;

public class GetActiveContactsByTrnsHandler :
ICrmQueryHandler<GetActiveContactsByTrnsQuery, IDictionary<string, Contact?>>
{
public async Task<IDictionary<string, Contact?>> Execute(
GetActiveContactsByTrnsQuery query,
IOrganizationServiceAsync organizationService)
{
var queryExpression = new QueryExpression(Contact.EntityLogicalName)
{
ColumnSet = query.ColumnSet,
Criteria = new FilterExpression(LogicalOperator.And)
{
Conditions =
{
new ConditionExpression(Contact.Fields.StateCode, ConditionOperator.Equal, (int)ContactState.Active),
new ConditionExpression(Contact.Fields.dfeta_TRN, ConditionOperator.In, query.Trns.ToArray())
}
}
};

var response = await organizationService.RetrieveMultipleAsync(queryExpression);
var contacts = response.Entities.Select(e => e.ToEntity<Contact>()).ToDictionary(c => c.dfeta_TRN, c => c);

return query.Trns.ToDictionary(trn => trn, trn => contacts.GetValueOrDefault(trn));
}
}
Loading

0 comments on commit de1a4b6

Please sign in to comment.