Skip to content

Commit 7027a22

Browse files
authored
Move contact creation and update to TrnRequestHelper (#2011)
This will allow us to share the logic between the SupportUi and API. It should also make it simpler to run from Hangfire jobs. I've removed the name normalization bits as we won't need it by the time this goes live.
1 parent 9c5bd2d commit 7027a22

File tree

15 files changed

+168
-74
lines changed

15 files changed

+168
-74
lines changed

TeachingRecordSystem/src/TeachingRecordSystem.Api/V3/Implementation/Operations/CreateTrnRequest.cs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using System.Text;
22
using Microsoft.Extensions.Options;
3+
using Optional;
34
using TeachingRecordSystem.Api.Infrastructure.Security;
45
using TeachingRecordSystem.Api.V3.Implementation.Dtos;
56
using TeachingRecordSystem.Core.DataStore.Postgres;
@@ -185,9 +186,9 @@ await crmQueryDispatcher.ExecuteQueryAsync(new CreateContactQuery()
185186
FirstName = firstName,
186187
MiddleName = middleName,
187188
LastName = command.LastName,
188-
StatedFirstName = command.FirstName,
189-
StatedMiddleName = command.MiddleName ?? "",
190-
StatedLastName = command.LastName,
189+
StatedFirstName = Option.Some(command.FirstName),
190+
StatedMiddleName = Option.Some(command.MiddleName ?? ""),
191+
StatedLastName = Option.Some(command.LastName),
191192
DateOfBirth = command.DateOfBirth,
192193
Gender = command.Gender?.ConvertToContact_GenderCode() ?? Contact_GenderCode.Notavailable,
193194
EmailAddress = emailAddress,

TeachingRecordSystem/src/TeachingRecordSystem.Core/Dqt/Queries/CreateContactQuery.cs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
using Optional;
12
using TeachingRecordSystem.Core.Services.DqtOutbox.Messages;
23

34
namespace TeachingRecordSystem.Core.Dqt.Queries;
@@ -8,9 +9,9 @@ public record CreateContactQuery : ICrmQuery<Guid>
89
public required string FirstName { get; init; }
910
public required string MiddleName { get; init; }
1011
public required string LastName { get; init; }
11-
public required string StatedFirstName { get; init; }
12-
public required string StatedMiddleName { get; init; }
13-
public required string StatedLastName { get; init; }
12+
public Option<string> StatedFirstName { get; init; }
13+
public Option<string> StatedMiddleName { get; init; }
14+
public Option<string> StatedLastName { get; init; }
1415
public required DateOnly DateOfBirth { get; init; }
1516
public required Contact_GenderCode Gender { get; init; }
1617
public required string? EmailAddress { get; init; }

TeachingRecordSystem/src/TeachingRecordSystem.Core/Dqt/Queries/UpdateContactQuery.cs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,15 @@
22

33
namespace TeachingRecordSystem.Core.Dqt.Queries;
44

5-
public class UpdateContactQuery : ICrmQuery<bool>
5+
public record UpdateContactQuery : ICrmQuery<bool>
66
{
77
public required Guid ContactId { get; init; }
88
public required Option<string> FirstName { get; init; }
99
public required Option<string> MiddleName { get; init; }
1010
public required Option<string> LastName { get; init; }
11-
public required Option<string> StatedFirstName { get; init; }
12-
public required Option<string> StatedMiddleName { get; init; }
13-
public required Option<string> StatedLastName { get; init; }
11+
public Option<string> StatedFirstName { get; init; }
12+
public Option<string> StatedMiddleName { get; init; }
13+
public Option<string> StatedLastName { get; init; }
1414
public required Option<DateOnly> DateOfBirth { get; init; }
1515
public required Option<Contact_GenderCode> Gender { get; init; }
1616
public required Option<string?> EmailAddress { get; init; }

TeachingRecordSystem/src/TeachingRecordSystem.Core/Dqt/QueryHandlers/CreateContactHandler.cs

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
using Microsoft.PowerPlatform.Dataverse.Client;
22
using Microsoft.Xrm.Sdk.Messages;
3+
using Optional;
4+
using Optional.Unsafe;
35
using TeachingRecordSystem.Core.Dqt.Queries;
46
using TeachingRecordSystem.Core.Services.DqtOutbox;
57

@@ -20,18 +22,28 @@ public async Task<Guid> ExecuteAsync(CreateContactQuery query, IOrganizationServ
2022
FirstName = query.FirstName,
2123
MiddleName = query.MiddleName,
2224
LastName = query.LastName,
23-
dfeta_StatedFirstName = query.StatedFirstName,
24-
dfeta_StatedMiddleName = query.StatedMiddleName,
25-
dfeta_StatedLastName = query.StatedLastName,
2625
BirthDate = query.DateOfBirth.ToDateTimeWithDqtBstFix(isLocalTime: false),
2726
GenderCode = query.Gender,
2827
dfeta_NINumber = query.NationalInsuranceNumber,
2928
EMailAddress1 = query.EmailAddress,
3029
dfeta_AllowPiiUpdatesFromRegister = query.AllowPiiUpdates,
3130
dfeta_TrnRequestID = query.TrnRequestId,
32-
dfeta_TRN = query.Trn,
31+
dfeta_TRN = query.Trn
3332
};
3433

34+
void SetAttributeIfSpecified<T>(Option<T> value, string attributeName)
35+
{
36+
if (value.HasValue)
37+
{
38+
object? attributeValue = value.ValueOrFailure();
39+
contact.Attributes.Add(attributeName, attributeValue);
40+
}
41+
}
42+
43+
SetAttributeIfSpecified(query.StatedFirstName, Contact.Fields.dfeta_StatedFirstName);
44+
SetAttributeIfSpecified(query.StatedMiddleName, Contact.Fields.dfeta_StatedMiddleName);
45+
SetAttributeIfSpecified(query.StatedLastName, Contact.Fields.dfeta_StatedLastName);
46+
3547
if (query.Trn is null)
3648
{
3749
// CRM plug-in explodes if TRN is specified but is null

TeachingRecordSystem/src/TeachingRecordSystem.Core/TrnRequestHelper.cs

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,20 @@
11
using System.Diagnostics.CodeAnalysis;
22
using Microsoft.Extensions.Options;
33
using Microsoft.Xrm.Sdk.Query;
4+
using Optional;
45
using TeachingRecordSystem.Core.DataStore.Postgres;
56
using TeachingRecordSystem.Core.DataStore.Postgres.Models;
67
using TeachingRecordSystem.Core.Dqt;
78
using TeachingRecordSystem.Core.Dqt.Queries;
89
using TeachingRecordSystem.Core.Services.GetAnIdentity.Api.Models;
910
using TeachingRecordSystem.Core.Services.GetAnIdentityApi;
11+
using TeachingRecordSystem.Core.Services.TrnGeneration;
1012

1113
namespace TeachingRecordSystem.Core;
1214

1315
public class TrnRequestHelper(
1416
TrsDbContext dbContext,
17+
ITrnGenerator trnGenerator,
1518
ICrmQueryDispatcher crmQueryDispatcher,
1619
IGetAnIdentityApiClient idApiClient,
1720
IOptions<AccessYourTeachingQualificationsOptions> aytqOptionsAccessor)
@@ -96,6 +99,70 @@ public string GetAccessYourTeachingQualificationsLink(string trnToken) =>
9699
return result;
97100
}
98101

102+
public async Task<Guid> CreateContactFromTrnRequestAsync(TrnRequestMetadata requestData)
103+
{
104+
var trn = await trnGenerator.GenerateTrnAsync();
105+
var newContactId = Guid.NewGuid();
106+
107+
await crmQueryDispatcher.ExecuteQueryAsync(new CreateContactQuery()
108+
{
109+
ContactId = newContactId,
110+
FirstName = requestData.FirstName!,
111+
MiddleName = requestData.MiddleName ?? string.Empty,
112+
LastName = requestData.LastName!,
113+
DateOfBirth = requestData.DateOfBirth,
114+
Gender = Contact_GenderCode.Notprovided, // TODO when we've sorted gender
115+
EmailAddress = requestData.EmailAddress,
116+
NationalInsuranceNumber = requestData.NationalInsuranceNumber,
117+
ReviewTasks = [],
118+
ApplicationUserName = requestData.ApplicationUser.Name,
119+
Trn = trn,
120+
TrnRequestId = GetCrmTrnRequestId(requestData.ApplicationUserId, requestData.RequestId),
121+
TrnRequestMetadataMessage = null,
122+
AllowPiiUpdates = false
123+
});
124+
125+
return newContactId;
126+
}
127+
128+
public async Task UpdateContactFromTrnRequestAsync(
129+
TrnRequestMetadata requestData,
130+
IReadOnlyCollection<PersonMatchedAttribute> attributesToUpdate)
131+
{
132+
if (requestData.ResolvedPersonId is not Guid contactId)
133+
{
134+
throw new InvalidOperationException("TRN request is not resolved.");
135+
}
136+
137+
var query = new UpdateContactQuery()
138+
{
139+
ContactId = contactId,
140+
FirstName = default,
141+
MiddleName = default,
142+
LastName = default,
143+
DateOfBirth = default,
144+
Gender = default,
145+
EmailAddress = default,
146+
NationalInsuranceNumber = default
147+
};
148+
149+
foreach (var attribute in attributesToUpdate)
150+
{
151+
query = attribute switch
152+
{
153+
PersonMatchedAttribute.FirstName => query with { FirstName = Option.Some(requestData.FirstName!) },
154+
PersonMatchedAttribute.MiddleName => query with { MiddleName = Option.Some(requestData.MiddleName!) },
155+
PersonMatchedAttribute.LastName => query with { LastName = Option.Some(requestData.LastName!) },
156+
PersonMatchedAttribute.DateOfBirth => query with { DateOfBirth = Option.Some(requestData.DateOfBirth!) },
157+
PersonMatchedAttribute.EmailAddress => query with { EmailAddress = Option.Some(requestData.EmailAddress) },
158+
PersonMatchedAttribute.NationalInsuranceNumber => query with { NationalInsuranceNumber = Option.Some(requestData.NationalInsuranceNumber) },
159+
_ => throw new NotImplementedException()
160+
};
161+
}
162+
163+
await crmQueryDispatcher.ExecuteQueryAsync(query);
164+
}
165+
99166
public static string GetCrmTrnRequestId(Guid currentApplicationUserId, string requestId) =>
100167
$"{currentApplicationUserId}::{requestId}";
101168
}

TeachingRecordSystem/src/TeachingRecordSystem.SupportUi/Pages/SupportTasks/ApiTrnRequests/Resolve/CheckAnswers.cshtml.cs

Lines changed: 5 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -2,20 +2,15 @@
22
using Microsoft.AspNetCore.Mvc;
33
using Microsoft.AspNetCore.Mvc.Filters;
44
using Microsoft.AspNetCore.Mvc.RazorPages;
5-
using Optional;
65
using TeachingRecordSystem.Core.DataStore.Postgres;
7-
using TeachingRecordSystem.Core.Dqt.Models;
8-
using TeachingRecordSystem.Core.Dqt.Queries;
9-
using TeachingRecordSystem.Core.Services.TrnGeneration;
106
using static TeachingRecordSystem.SupportUi.Pages.SupportTasks.ApiTrnRequests.Resolve.ResolveApiTrnRequestState;
117

128
namespace TeachingRecordSystem.SupportUi.Pages.SupportTasks.ApiTrnRequests.Resolve;
139

1410
[Journey(JourneyNames.ResolveApiTrnRequest), RequireJourneyInstance]
1511
public class CheckAnswers(
1612
TrsDbContext dbContext,
17-
ICrmQueryDispatcher crmQueryDispatcher,
18-
ITrnGenerator trnGenerator,
13+
TrnRequestHelper trnRequestHelper,
1914
TrsLinkGenerator linkGenerator) : PageModel
2015
{
2116
[FromRoute]
@@ -55,53 +50,17 @@ public async Task<IActionResult> OnPostAsync()
5550

5651
if (CreatingNewRecord)
5752
{
58-
var newContactId = Guid.NewGuid();
59-
requestData.ResolvedPersonId = newContactId;
60-
var trn = await trnGenerator.GenerateTrnAsync();
61-
62-
await crmQueryDispatcher.ExecuteQueryAsync(new CreateContactQuery()
63-
{
64-
ContactId = newContactId,
65-
// These three name fields need normalizing; we'll cover that when moving this into a background job
66-
FirstName = requestData.FirstName!,
67-
MiddleName = requestData.MiddleName ?? string.Empty,
68-
LastName = requestData.LastName!,
69-
StatedFirstName = requestData.FirstName!,
70-
StatedMiddleName = requestData.MiddleName ?? string.Empty,
71-
StatedLastName = requestData.LastName!,
72-
DateOfBirth = requestData.DateOfBirth,
73-
Gender = Contact_GenderCode.Notprovided, // TODO when we've sorted gender
74-
EmailAddress = requestData.EmailAddress,
75-
NationalInsuranceNumber = requestData.NationalInsuranceNumber,
76-
ReviewTasks = [],
77-
ApplicationUserName = requestData.ApplicationUser.Name,
78-
Trn = trn,
79-
TrnRequestId = TrnRequestHelper.GetCrmTrnRequestId(requestData.ApplicationUserId, requestData.RequestId),
80-
TrnRequestMetadataMessage = null, // We don't need to pass this as we've always got metadata in our DB
81-
AllowPiiUpdates = false
82-
});
53+
requestData.ResolvedPersonId = await trnRequestHelper.CreateContactFromTrnRequestAsync(requestData);
8354
}
8455
else
8556
{
8657
Debug.Assert(state.PersonId is not null);
8758
var existingContactId = state.PersonId!.Value;
8859
requestData.ResolvedPersonId = existingContactId;
8960

90-
await crmQueryDispatcher.ExecuteQueryAsync(new UpdateContactQuery()
91-
{
92-
ContactId = existingContactId,
93-
// These three name fields need normalizing; we'll cover that when moving this into a background job
94-
FirstName = state.FirstNameSource is PersonAttributeSource.TrnRequest ? Option.Some(requestData.FirstName!) : default,
95-
MiddleName = state.MiddleNameSource is PersonAttributeSource.TrnRequest ? Option.Some(requestData.MiddleName ?? string.Empty) : default,
96-
LastName = state.LastNameSource is PersonAttributeSource.TrnRequest ? Option.Some(requestData.LastName!) : default,
97-
StatedFirstName = state.FirstNameSource is PersonAttributeSource.TrnRequest ? Option.Some(requestData.FirstName!) : default,
98-
StatedMiddleName = state.MiddleNameSource is PersonAttributeSource.TrnRequest ? Option.Some(requestData.MiddleName ?? string.Empty) : default,
99-
StatedLastName = state.LastNameSource is PersonAttributeSource.TrnRequest ? Option.Some(requestData.LastName!) : default,
100-
DateOfBirth = state.DateOfBirthSource is PersonAttributeSource.TrnRequest ? Option.Some(requestData.DateOfBirth) : default,
101-
Gender = default, // TODO when we've sorted gender
102-
EmailAddress = state.EmailAddressSource is PersonAttributeSource.TrnRequest ? Option.Some(requestData.EmailAddress) : default,
103-
NationalInsuranceNumber = state.NationalInsuranceNumberSource is PersonAttributeSource.TrnRequest ? Option.Some(requestData.NationalInsuranceNumber) : default
104-
});
61+
await trnRequestHelper.UpdateContactFromTrnRequestAsync(
62+
requestData,
63+
state.GetAttributesToUpdate());
10564
}
10665

10766
supportTask.Status = SupportTaskStatus.Closed;

TeachingRecordSystem/src/TeachingRecordSystem.SupportUi/Pages/SupportTasks/ApiTrnRequests/Resolve/ResolveApiTrnRequestState.cs

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,49 @@ IEnumerable<PersonMatchedAttribute> Impl()
6767
}
6868
}
6969

70+
public IReadOnlyCollection<PersonMatchedAttribute> GetAttributesToUpdate()
71+
{
72+
if (!PersonAttributeSourcesSet)
73+
{
74+
throw new InvalidOperationException("Attribute sources not set.");
75+
}
76+
77+
return Impl().AsReadOnly();
78+
79+
IEnumerable<PersonMatchedAttribute> Impl()
80+
{
81+
if (FirstNameSource is PersonAttributeSource.TrnRequest)
82+
{
83+
yield return PersonMatchedAttribute.FirstName;
84+
}
85+
86+
if (MiddleNameSource is PersonAttributeSource.TrnRequest)
87+
{
88+
yield return PersonMatchedAttribute.MiddleName;
89+
}
90+
91+
if (LastNameSource is PersonAttributeSource.TrnRequest)
92+
{
93+
yield return PersonMatchedAttribute.LastName;
94+
}
95+
96+
if (DateOfBirthSource is PersonAttributeSource.TrnRequest)
97+
{
98+
yield return PersonMatchedAttribute.DateOfBirth;
99+
}
100+
101+
if (EmailAddressSource is PersonAttributeSource.TrnRequest)
102+
{
103+
yield return PersonMatchedAttribute.EmailAddress;
104+
}
105+
106+
if (NationalInsuranceNumberSource is PersonAttributeSource.TrnRequest)
107+
{
108+
yield return PersonMatchedAttribute.NationalInsuranceNumber;
109+
}
110+
}
111+
}
112+
70113
public enum PersonAttributeSource
71114
{
72115
ExistingRecord = 0,

TeachingRecordSystem/tests/TeachingRecordSystem.Api.UnitTests/V3/CreateTrnRequestTests.cs

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
using FakeXrmEasy.Abstractions;
33
using Microsoft.Extensions.Configuration;
44
using Microsoft.Extensions.DependencyInjection;
5+
using Optional.Unsafe;
56
using TeachingRecordSystem.Api.V3.Implementation.Dtos;
67
using TeachingRecordSystem.Api.V3.Implementation.Operations;
78
using TeachingRecordSystem.Core.Dqt.Models;
@@ -62,9 +63,9 @@ public Task HandleAsync_WithMultipleFirstNames_NormalizesFirstAndMiddleNames() =
6263
// Assert
6364
var (createContactQuery, _) = CrmQueryDispatcherSpy.GetSingleQuery<CreateContactQuery, Guid>();
6465
Assert.Equal(firstName1, createContactQuery.FirstName);
65-
Assert.Equal(firstName, createContactQuery.StatedFirstName);
66+
Assert.Equal(firstName, createContactQuery.StatedFirstName.ValueOrFailure());
6667
Assert.Equal($"{firstName2} {middleName}", createContactQuery.MiddleName);
67-
Assert.Equal(middleName, createContactQuery.StatedMiddleName);
68+
Assert.Equal(middleName, createContactQuery.StatedMiddleName.ValueOrFailure());
6869
});
6970

7071
[Fact]
@@ -530,9 +531,9 @@ private void AssertContactMatchesCommand(
530531
{
531532
var (applicationUserId, _) = CurrentUserProvider.GetCurrentApplicationUser();
532533

533-
Assert.Equal(command.FirstName, query.StatedFirstName);
534-
Assert.Equal(command.MiddleName, query.StatedMiddleName);
535-
Assert.Equal(command.LastName, query.StatedLastName);
534+
Assert.Equal(command.FirstName, query.StatedFirstName.ValueOrFailure());
535+
Assert.Equal(command.MiddleName, query.StatedMiddleName.ValueOrFailure());
536+
Assert.Equal(command.LastName, query.StatedLastName.ValueOrFailure());
536537
Assert.Equal(command.DateOfBirth, query.DateOfBirth);
537538
Assert.Equal(command.Gender?.ConvertToContact_GenderCode(), query.Gender);
538539
Assert.Equal(command.NationalInsuranceNumber, query.NationalInsuranceNumber);

TeachingRecordSystem/tests/TeachingRecordSystem.AuthorizeAccess.EndToEndTests/HostFixture.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
using TeachingRecordSystem.Core.DataStore.Postgres;
1111
using TeachingRecordSystem.Core.Services.Files;
1212
using TeachingRecordSystem.Core.Services.GetAnIdentityApi;
13+
using TeachingRecordSystem.Core.Services.TrnGeneration;
1314
using TeachingRecordSystem.Core.Services.TrsDataSync;
1415
using TeachingRecordSystem.TestCommon.Infrastructure;
1516
using TeachingRecordSystem.UiTestCommon.Infrastructure.FormFlow;
@@ -92,6 +93,7 @@ private Host<Program> CreateHost() =>
9293
services.AddFakeXrm();
9394
services.AddSingleton<FakeTrnGenerator>();
9495
services.AddSingleton<TrsDataSyncHelper>();
96+
services.AddSingleton<ITrnGenerator>(sp => sp.GetRequiredService<FakeTrnGenerator>());
9597
services.AddSingleton<IAuditRepository, TestableAuditRepository>();
9698
services.AddSingleton<IUserInstanceStateProvider, InMemoryInstanceStateProvider>();
9799
services.AddSingleton(GetMockFileService());

TeachingRecordSystem/tests/TeachingRecordSystem.AuthorizeAccess.Tests/HostFixture.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
using TeachingRecordSystem.AuthorizeAccess.Tests.Infrastructure.Security;
88
using TeachingRecordSystem.Core.Services.Files;
99
using TeachingRecordSystem.Core.Services.GetAnIdentityApi;
10+
using TeachingRecordSystem.Core.Services.TrnGeneration;
1011
using TeachingRecordSystem.Core.Services.TrsDataSync;
1112
using TeachingRecordSystem.TestCommon.Infrastructure;
1213
using TeachingRecordSystem.UiTestCommon.Infrastructure.FormFlow;
@@ -62,6 +63,7 @@ protected override void ConfigureWebHost(IWebHostBuilder builder)
6263
services.AddFakeXrm();
6364
services.AddSingleton<IUserInstanceStateProvider, InMemoryInstanceStateProvider>();
6465
services.AddSingleton<FakeTrnGenerator>();
66+
services.AddSingleton<ITrnGenerator>(sp => sp.GetRequiredService<FakeTrnGenerator>());
6567
services.AddSingleton<IAuditRepository, TestableAuditRepository>();
6668
services.AddSingleton<TrsDataSyncHelper>();
6769
services.AddSingleton(GetMockFileService());

0 commit comments

Comments
 (0)