Skip to content

Commit 0616d6e

Browse files
committed
Added belt and braces to block person merge if there are any professional statuses ,MQs or Induction Status other than None
1 parent a3878d9 commit 0616d6e

7 files changed

Lines changed: 243 additions & 10 deletions

File tree

TeachingRecordSystem/src/TeachingRecordSystem.SupportUi/Extensions.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,7 @@ public static IServiceCollection AddSupportUiServices(this IServiceCollection se
9595
.AddTransient<RequireClosedAlertFilter>()
9696
.AddTransient<RequireOpenAlertFilter>()
9797
.AddTransient<RedirectWithPersonIdFilter>()
98+
.AddTransient<CheckPersonCanBeMergedFilter>()
9899
.AddSingleton<ITagHelperInitializer<FormTagHelper>, FormTagHelperInitializer>()
99100
.AddSingleton<ITagHelperInitializer<TextInputTagHelper>, TextInputTagHelperInitializer>()
100101
.AddScoped<SupportUiFormContext>()
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
using Microsoft.AspNetCore.Http.Features;
2+
using Microsoft.AspNetCore.Mvc;
3+
using Microsoft.AspNetCore.Mvc.Filters;
4+
using TeachingRecordSystem.Core.DataStore.Postgres;
5+
6+
namespace TeachingRecordSystem.SupportUi.Infrastructure.Filters;
7+
8+
public class CheckPersonCanBeMergedFilter(TrsDbContext dbContext) : IAsyncResourceFilter
9+
{
10+
public async Task OnResourceExecutionAsync(ResourceExecutingContext context, ResourceExecutionDelegate next)
11+
{
12+
var currentPerson = context.HttpContext.Features.GetRequiredFeature<CurrentPersonFeature>();
13+
var person = await dbContext.Persons
14+
.SingleAsync(p => p.PersonId == currentPerson.PersonId);
15+
16+
var hasOpenAlert = await dbContext.Alerts
17+
.AnyAsync(a => a.PersonId == currentPerson.PersonId && a.EndDate == null);
18+
19+
var hasMandatoryQualification = await dbContext.MandatoryQualifications
20+
.AnyAsync(mq => mq.PersonId == currentPerson.PersonId);
21+
22+
var hasProfessionalStatus =
23+
person.EytsDate.HasValue ||
24+
person.HasEyps ||
25+
person.PqtsDate.HasValue ||
26+
person.QtsDate.HasValue ||
27+
person.QtlsStatus != QtlsStatus.None;
28+
29+
if (hasOpenAlert ||
30+
hasMandatoryQualification ||
31+
hasProfessionalStatus ||
32+
person.InductionStatus != InductionStatus.None)
33+
{
34+
context.Result = new BadRequestResult();
35+
return;
36+
}
37+
38+
await next();
39+
}
40+
}

TeachingRecordSystem/src/TeachingRecordSystem.SupportUi/Pages/Persons/MergePerson/Conventions.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
using Microsoft.AspNetCore.Authorization;
2+
using Microsoft.AspNetCore.Mvc;
23
using Microsoft.AspNetCore.Mvc.RazorPages;
4+
using TeachingRecordSystem.SupportUi.Infrastructure.Filters;
35
using TeachingRecordSystem.SupportUi.Infrastructure.Security;
46

57
namespace TeachingRecordSystem.SupportUi.Pages.Persons.MergePerson;
@@ -12,6 +14,8 @@ public void Configure(RazorPagesOptions options)
1214
this.GetFolderPathFromNamespace(),
1315
model =>
1416
{
17+
model.Filters.Add(new CheckPersonExistsFilterFactory());
18+
model.Filters.Add(new ServiceFilterAttribute<CheckPersonCanBeMergedFilter>() { Order = -150 }); // After Check Person but before FormFlow
1519
model.EndpointMetadata.Add(new AuthorizeAttribute()
1620
{
1721
Policy = AuthorizationPolicies.PersonDataEdit

TeachingRecordSystem/src/TeachingRecordSystem.SupportUi/Pages/Persons/MergePerson/Index.cshtml.cs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,5 +11,8 @@ public class IndexModel(SupportUiLinkGenerator linkGenerator) : PageModel
1111
[FromRoute]
1212
public Guid PersonId { get; set; }
1313

14-
public IActionResult OnGet() => Redirect(linkGenerator.Persons.MergePerson.EnterTrn(PersonId, JourneyInstance!.InstanceId));
14+
public IActionResult OnGet()
15+
{
16+
return Redirect(linkGenerator.Persons.MergePerson.EnterTrn(PersonId, JourneyInstance!.InstanceId));
17+
}
1518
}

TeachingRecordSystem/src/TeachingRecordSystem.SupportUi/Pages/Persons/PersonDetail/Index.cshtml.cs

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,6 @@ namespace TeachingRecordSystem.SupportUi.Pages.Persons.PersonDetail;
1010
[AllowDeactivatedPerson]
1111
public class IndexModel(TrsDbContext dbContext, IAuthorizationService authorizationService) : PageModel
1212
{
13-
private static readonly InductionStatus[] _invalidInductionStatusesForMerge = [InductionStatus.InProgress, InductionStatus.Passed, InductionStatus.Failed];
14-
1513
[FromRoute]
1614
public Guid PersonId { get; set; }
1715

@@ -57,8 +55,7 @@ public async Task OnGetAsync()
5755
!HasOpenAlert &&
5856
!hasMandatoryQualification &&
5957
Person!.IsActive &&
60-
(PersonProfessionalStatus is not PersonProfessionalStatusInfo professionalStatus ||
61-
!_invalidInductionStatusesForMerge.Contains(professionalStatus.InductionStatus));
58+
PersonProfessionalStatus is not PersonProfessionalStatusInfo professionalStatus;
6259

6360
// Person cannot be reactivated if they were deactivated as part of a merge
6461
// where they were merged into another Person (i.e. they were the secondary
Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
namespace TeachingRecordSystem.SupportUi.Tests.PageTests.Persons.MergePerson;
2+
3+
public class IndexTests(HostFixture hostFixture) : TestBase(hostFixture)
4+
{
5+
[Fact]
6+
public async Task Get_RedirectsToEnterTrn()
7+
{
8+
// Arrange
9+
var person = await TestData.CreatePersonAsync();
10+
11+
var request = new HttpRequestMessage(HttpMethod.Get, $"/persons/{person.PersonId}/merge");
12+
13+
// Act
14+
var response = await HttpClient.SendAsync(request); // Initializes journey
15+
response = await response.FollowRedirectAsync(HttpClient);
16+
17+
// Assert
18+
Assert.Equal(StatusCodes.Status302Found, (int)response.StatusCode);
19+
Assert.StartsWith($"/persons/{person.PersonId}/merge/enter-trn", response.Headers.Location?.OriginalString);
20+
}
21+
22+
[Fact]
23+
public async Task Get_WithPersonIdForNonExistentPerson_ReturnsNotFound()
24+
{
25+
// Arrange
26+
var nonExistentPersonId = Guid.NewGuid();
27+
var request = new HttpRequestMessage(HttpMethod.Get, $"/persons/{nonExistentPersonId}/merge");
28+
29+
// Act
30+
var response = await HttpClient.SendAsync(request);
31+
32+
// Assert
33+
Assert.Equal(StatusCodes.Status404NotFound, (int)response.StatusCode);
34+
}
35+
36+
[Fact]
37+
public async Task Get_PersonHasOpenAlert_ReturnsBadRequest()
38+
{
39+
// Arrange
40+
var person = await TestData.CreatePersonAsync(p => p
41+
.WithAlert(a => a.WithEndDate(null)));
42+
var request = new HttpRequestMessage(HttpMethod.Get, $"/persons/{person.PersonId}/merge");
43+
44+
// Act
45+
var response = await HttpClient.SendAsync(request);
46+
47+
// Assert
48+
Assert.Equal(StatusCodes.Status400BadRequest, (int)response.StatusCode);
49+
}
50+
51+
[Fact]
52+
public async Task Get_PersonHasMandatoryQualification_ReturnsBadRequest()
53+
{
54+
// Arrange
55+
var person = await TestData.CreatePersonAsync(p => p
56+
.WithMandatoryQualification());
57+
58+
var request = new HttpRequestMessage(HttpMethod.Get, $"/persons/{person.PersonId}/merge");
59+
60+
// Act
61+
var response = await HttpClient.SendAsync(request);
62+
63+
// Assert
64+
Assert.Equal(StatusCodes.Status400BadRequest, (int)response.StatusCode);
65+
}
66+
67+
[Theory]
68+
[InlineData(InductionStatus.InProgress, false)]
69+
[InlineData(InductionStatus.Passed, false)]
70+
[InlineData(InductionStatus.Failed, false)]
71+
[InlineData(InductionStatus.None, true)]
72+
[InlineData(InductionStatus.Exempt, false)]
73+
[InlineData(InductionStatus.FailedInWales, false)]
74+
[InlineData(InductionStatus.RequiredToComplete, false)]
75+
public async Task Get_PersonWithInductionStatus_ReturnsExpectedResult(InductionStatus status, bool expectIsValid)
76+
{
77+
// Arrange
78+
var person = await TestData.CreatePersonAsync(p => p
79+
.WithInductionStatus(i => i
80+
.WithStatus(status)
81+
.WithStartDate(new DateOnly(2024, 1, 1))
82+
.WithCompletedDate(new DateOnly(2024, 1, 1))));
83+
84+
var request = new HttpRequestMessage(HttpMethod.Get, $"/persons/{person.PersonId}/merge");
85+
86+
// Act
87+
var response = await HttpClient.SendAsync(request);
88+
89+
// Assert
90+
if (expectIsValid)
91+
{
92+
response = await response.FollowRedirectAsync(HttpClient);
93+
Assert.Equal(StatusCodes.Status302Found, (int)response.StatusCode);
94+
Assert.StartsWith($"/persons/{person.PersonId}/merge/enter-trn", response.Headers.Location?.OriginalString);
95+
}
96+
else
97+
{
98+
Assert.Equal(StatusCodes.Status400BadRequest, (int)response.StatusCode);
99+
}
100+
}
101+
102+
[Fact]
103+
public async Task Get_PersonHasQts_ReturnsBadRequest()
104+
{
105+
// Arrange
106+
var awardDate = Clock.Today;
107+
var person = await TestData.CreatePersonAsync(p => p
108+
.WithQts(awardDate));
109+
110+
var request = new HttpRequestMessage(HttpMethod.Get, $"/persons/{person.PersonId}/merge");
111+
112+
// Act
113+
var response = await HttpClient.SendAsync(request);
114+
115+
// Assert
116+
Assert.Equal(StatusCodes.Status400BadRequest, (int)response.StatusCode);
117+
}
118+
119+
[Fact]
120+
public async Task Get_PersonHasQtls_ReturnsBadRequest()
121+
{
122+
// Arrange
123+
var awardDate = Clock.Today;
124+
var person = await TestData.CreatePersonAsync(p => p
125+
.WithQtls(awardDate));
126+
127+
var request = new HttpRequestMessage(HttpMethod.Get, $"/persons/{person.PersonId}/merge");
128+
129+
// Act
130+
var response = await HttpClient.SendAsync(request);
131+
132+
// Assert
133+
Assert.Equal(StatusCodes.Status400BadRequest, (int)response.StatusCode);
134+
}
135+
136+
[Fact]
137+
public async Task Get_PersonHasEyts_ReturnsBadRequest()
138+
{
139+
// Arrange
140+
var awardDate = Clock.Today;
141+
var person = await TestData.CreatePersonAsync(p => p
142+
.WithHoldsRouteToProfessionalStatus(ProfessionalStatusType.EarlyYearsTeacherStatus, awardDate));
143+
144+
var request = new HttpRequestMessage(HttpMethod.Get, $"/persons/{person.PersonId}/merge");
145+
146+
// Act
147+
var response = await HttpClient.SendAsync(request);
148+
149+
// Assert
150+
Assert.Equal(StatusCodes.Status400BadRequest, (int)response.StatusCode);
151+
}
152+
153+
[Fact]
154+
public async Task Get_PersonHasEyps_ReturnsBadRequest()
155+
{
156+
// Arrange
157+
var person = await TestData.CreatePersonAsync(p => p
158+
.WithHoldsRouteToProfessionalStatus(ProfessionalStatusType.EarlyYearsProfessionalStatus));
159+
160+
var request = new HttpRequestMessage(HttpMethod.Get, $"/persons/{person.PersonId}/merge");
161+
162+
// Act
163+
var response = await HttpClient.SendAsync(request);
164+
165+
// Assert
166+
Assert.Equal(StatusCodes.Status400BadRequest, (int)response.StatusCode);
167+
}
168+
169+
[Fact]
170+
public async Task Get_PersonHasPqts_ReturnsBadRequest()
171+
{
172+
// Arrange
173+
var awardDate = Clock.Today;
174+
var person = await TestData.CreatePersonAsync(p => p
175+
.WithHoldsRouteToProfessionalStatus(ProfessionalStatusType.PartialQualifiedTeacherStatus, awardDate));
176+
177+
var request = new HttpRequestMessage(HttpMethod.Get, $"/persons/{person.PersonId}/merge");
178+
179+
// Act
180+
var response = await HttpClient.SendAsync(request);
181+
182+
// Assert
183+
Assert.Equal(StatusCodes.Status400BadRequest, (int)response.StatusCode);
184+
}
185+
}

TeachingRecordSystem/tests/TeachingRecordSystem.SupportUi.Tests/PageTests/Persons/PersonDetail/IndexTests.cs

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -246,6 +246,7 @@ public async Task Get_PersonHasQts_ShowsDetails()
246246
Assert.Equal("No", doc.GetSummaryListValueByKey("Early years professional status (EYPS)"));
247247
Assert.Equal("No", doc.GetSummaryListValueByKey("Partial qualified teacher status (PQTS)"));
248248
Assert.Null(doc.GetSummaryListValueByKey("PQTS held since"));
249+
Assert.Null(doc.GetElementByTestId("merge-button"));
249250
}
250251

251252
[Fact]
@@ -275,6 +276,7 @@ public async Task Get_PersonHasQtls_ShowsDetails()
275276
Assert.Equal("No", doc.GetSummaryListValueByKey("Early years professional status (EYPS)"));
276277
Assert.Equal("No", doc.GetSummaryListValueByKey("Partial qualified teacher status (PQTS)"));
277278
Assert.Null(doc.GetSummaryListValueByKey("PQTS held since"));
279+
Assert.Null(doc.GetElementByTestId("merge-button"));
278280
}
279281

280282
[Fact]
@@ -304,14 +306,13 @@ public async Task Get_PersonHasEyts_ShowsDetails()
304306
Assert.Equal("No", doc.GetSummaryListValueByKey("Early years professional status (EYPS)"));
305307
Assert.Equal("No", doc.GetSummaryListValueByKey("Partial qualified teacher status (PQTS)"));
306308
Assert.Null(doc.GetSummaryListValueByKey("PQTS held since"));
309+
Assert.Null(doc.GetElementByTestId("merge-button"));
307310
}
308311

309312
[Fact]
310313
public async Task Get_PersonHasEyps_ShowsDetails()
311314
{
312315
// Arrange
313-
var awardDate = Clock.Today;
314-
315316
var person = await TestData.CreatePersonAsync(p => p
316317

317318
.WithHoldsRouteToProfessionalStatus(ProfessionalStatusType.EarlyYearsProfessionalStatus));
@@ -333,6 +334,7 @@ public async Task Get_PersonHasEyps_ShowsDetails()
333334
Assert.Equal("Holds", doc.GetSummaryListValueByKey("Early years professional status (EYPS)"));
334335
Assert.Equal("No", doc.GetSummaryListValueByKey("Partial qualified teacher status (PQTS)"));
335336
Assert.Null(doc.GetSummaryListValueByKey("PQTS held since"));
337+
Assert.Null(doc.GetElementByTestId("merge-button"));
336338
}
337339

338340
[Fact]
@@ -362,6 +364,7 @@ public async Task Get_PersonHasPqts_ShowsDetails()
362364
Assert.Equal("No", doc.GetSummaryListValueByKey("Early years professional status (EYPS)"));
363365
Assert.Equal("Holds", doc.GetSummaryListValueByKey("Partial qualified teacher status (PQTS)"));
364366
Assert.Equal(awardDate.ToString(UiDefaults.DateOnlyDisplayFormat), doc.GetSummaryListValueByKey("PQTS held since"));
367+
Assert.Null(doc.GetElementByTestId("merge-button"));
365368
}
366369

367370
[Theory]
@@ -500,9 +503,9 @@ public async Task Get_PersonHasMandatoryQualification_DoesNotShowMergeButton()
500503
[InlineData(InductionStatus.Passed, false)]
501504
[InlineData(InductionStatus.Failed, false)]
502505
[InlineData(InductionStatus.None, true)]
503-
[InlineData(InductionStatus.Exempt, true)]
504-
[InlineData(InductionStatus.FailedInWales, true)]
505-
[InlineData(InductionStatus.RequiredToComplete, true)]
506+
[InlineData(InductionStatus.Exempt, false)]
507+
[InlineData(InductionStatus.FailedInWales, false)]
508+
[InlineData(InductionStatus.RequiredToComplete, false)]
506509
public async Task Get_PersonWithInductionStatus_ShowsMergeButtonAsExpected(InductionStatus status, bool expectMergeButtonToBeShown)
507510
{
508511
// Arrange

0 commit comments

Comments
 (0)