diff --git a/TeachingRecordSystem/src/TeachingRecordSystem.Core/DataStore/Postgres/Models/Event.cs b/TeachingRecordSystem/src/TeachingRecordSystem.Core/DataStore/Postgres/Models/Event.cs
index a27151b34..afe835ad9 100644
--- a/TeachingRecordSystem/src/TeachingRecordSystem.Core/DataStore/Postgres/Models/Event.cs
+++ b/TeachingRecordSystem/src/TeachingRecordSystem.Core/DataStore/Postgres/Models/Event.cs
@@ -1,4 +1,3 @@
-using System.Text.Json;
using TeachingRecordSystem.Core.Events;
namespace TeachingRecordSystem.Core.DataStore.Postgres.Models;
@@ -15,7 +14,7 @@ public class Event
public static Event FromEventBase(EventBase @event, DateTime? inserted)
{
var eventName = @event.GetEventName();
- var payload = JsonSerializer.Serialize(@event, inputType: @event.GetType(), EventBase.JsonSerializerOptions);
+ var payload = @event.Serialize();
return new Event()
{
@@ -29,10 +28,6 @@ public static Event FromEventBase(EventBase @event, DateTime? inserted)
public EventBase ToEventBase()
{
- var eventTypeName = $"{typeof(EventBase).Namespace}.{EventName}";
- var eventType = Type.GetType(eventTypeName) ??
- throw new Exception($"Could not find event type '{eventTypeName}'.");
-
- return (EventBase)JsonSerializer.Deserialize(Payload, eventType, EventBase.JsonSerializerOptions)!;
+ return EventBase.Deserialize(Payload, EventName);
}
}
diff --git a/TeachingRecordSystem/src/TeachingRecordSystem.Core/Events/EventBase.cs b/TeachingRecordSystem/src/TeachingRecordSystem.Core/Events/EventBase.cs
index fe5453f87..4633eb9ab 100644
--- a/TeachingRecordSystem/src/TeachingRecordSystem.Core/Events/EventBase.cs
+++ b/TeachingRecordSystem/src/TeachingRecordSystem.Core/Events/EventBase.cs
@@ -12,4 +12,15 @@ public abstract record EventBase
public required RaisedByUserInfo RaisedBy { get; init; }
public string GetEventName() => GetType().Name;
+
+ public string Serialize() => JsonSerializer.Serialize(this, inputType: GetType(), JsonSerializerOptions);
+
+ public static EventBase Deserialize(string payload, string eventName)
+ {
+ var eventTypeName = $"{typeof(EventBase).Namespace}.{eventName}";
+ var eventType = Type.GetType(eventTypeName) ??
+ throw new Exception($"Could not find event type '{eventTypeName}'.");
+
+ return (EventBase)JsonSerializer.Deserialize(payload, eventType, JsonSerializerOptions)!;
+ }
}
diff --git a/TeachingRecordSystem/src/TeachingRecordSystem.SupportUi/Pages/ChangeRequests/Index.cshtml b/TeachingRecordSystem/src/TeachingRecordSystem.SupportUi/Pages/ChangeRequests/Index.cshtml
index 027c6469f..e76dcab1d 100644
--- a/TeachingRecordSystem/src/TeachingRecordSystem.SupportUi/Pages/ChangeRequests/Index.cshtml
+++ b/TeachingRecordSystem/src/TeachingRecordSystem.SupportUi/Pages/ChangeRequests/Index.cshtml
@@ -29,7 +29,7 @@
@changeRequestInfo.RequestReference |
@changeRequestInfo.Customer |
@changeRequestInfo.ChangeType |
- @changeRequestInfo.CreatedOn.ToString("dd/MM/yyyy") |
+ @changeRequestInfo.CreatedOn.ToString("d MMMM yyyy") |
}
}
diff --git a/TeachingRecordSystem/src/TeachingRecordSystem.SupportUi/Pages/Persons/PersonDetail/ChangeLog.cshtml b/TeachingRecordSystem/src/TeachingRecordSystem.SupportUi/Pages/Persons/PersonDetail/ChangeLog.cshtml
index 13ed2ccf9..b42108ed0 100644
--- a/TeachingRecordSystem/src/TeachingRecordSystem.SupportUi/Pages/Persons/PersonDetail/ChangeLog.cshtml
+++ b/TeachingRecordSystem/src/TeachingRecordSystem.SupportUi/Pages/Persons/PersonDetail/ChangeLog.cshtml
@@ -1,6 +1,7 @@
@page "/persons/{personId}/changelog"
@using TeachingRecordSystem.Core.Events
@using TeachingRecordSystem.SupportUi.Pages.Common;
+@using TeachingRecordSystem.SupportUi.Pages.Persons.PersonDetail.Timeline.Events
@model TeachingRecordSystem.SupportUi.Pages.Persons.PersonDetail.ChangeLogModel
@{
Layout = "Layout";
@@ -23,7 +24,7 @@ else
{
var viewName = timelineItem.ItemType switch
{
- TimelineItemType.Event => $"./Timeline/Events/{((EventBase)timelineItem.ItemModel).GetEventName()}.cshtml",
+ TimelineItemType.Event => $"./Timeline/Events/{((TimelineEvent)timelineItem.ItemModel).Event.GetEventName()}.cshtml",
_ => $"./Timeline/{timelineItem.ItemType}.cshtml"
};
diff --git a/TeachingRecordSystem/src/TeachingRecordSystem.SupportUi/Pages/Persons/PersonDetail/ChangeLog.cshtml.cs b/TeachingRecordSystem/src/TeachingRecordSystem.SupportUi/Pages/Persons/PersonDetail/ChangeLog.cshtml.cs
index b250f0f07..de89d6cb5 100644
--- a/TeachingRecordSystem/src/TeachingRecordSystem.SupportUi/Pages/Persons/PersonDetail/ChangeLog.cshtml.cs
+++ b/TeachingRecordSystem/src/TeachingRecordSystem.SupportUi/Pages/Persons/PersonDetail/ChangeLog.cshtml.cs
@@ -1,11 +1,16 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
+using Microsoft.EntityFrameworkCore;
+using TeachingRecordSystem.Core.DataStore.Postgres;
+using TeachingRecordSystem.Core.DataStore.Postgres.Models;
using TeachingRecordSystem.Core.Dqt.Models;
using TeachingRecordSystem.Core.Dqt.Queries;
+using TeachingRecordSystem.Core.Events;
+using TeachingRecordSystem.SupportUi.Pages.Persons.PersonDetail.Timeline.Events;
namespace TeachingRecordSystem.SupportUi.Pages.Persons.PersonDetail;
-public class ChangeLogModel(ICrmQueryDispatcher crmQueryDispatcher) : PageModel
+public class ChangeLogModel(ICrmQueryDispatcher crmQueryDispatcher, IDbContextFactory dbContextFactory) : PageModel
{
[FromRoute]
public Guid PersonId { get; set; }
@@ -40,6 +45,44 @@ public async Task OnGet()
var notesResult = await crmQueryDispatcher.ExecuteQuery(new GetNotesByContactIdQuery(PersonId));
+ using var dbContext = await dbContextFactory.CreateDbContextAsync();
+ var personIdString = PersonId.ToString();
+ var eventsWithUser = await dbContext.Database
+ .SqlQuery($"""
+ SELECT
+ e.event_name,
+ e.payload as event_payload,
+ u.user_id as trs_user_user_id,
+ u.active as trs_user_active,
+ u.user_type as trs_user_user_type,
+ u.name as trs_user_name,
+ u.email as trs_user_email,
+ u.azure_ad_user_id as trs_user_azure_ad_user_id,
+ u.roles as trs_user_roles,
+ u.dqt_user_id as trs_user_dqt_user_id,
+ CASE
+ WHEN e.payload #>> Array['RaisedBy','DqtUserId'] is not null THEN
+ (e.payload #>> Array['RaisedBy','DqtUserId'])::uuid
+ ELSE
+ null
+ END as dqt_user_id,
+ e.payload #>> Array['RaisedBy','DqtUserName'] as dqt_user_name
+ FROM
+ events as e
+ LEFT JOIN
+ users as u ON
+ CASE
+ WHEN e.payload #>> Array['RaisedBy','DqtUserId'] is null THEN
+ (e.payload ->> 'RaisedBy')::uuid
+ ELSE
+ null
+ END = u.user_id
+ WHERE
+ e.payload ->> 'PersonId' = {personIdString}
+ AND e.event_name = 'MandatoryQualificationDeletedEvent'
+ """)
+ .ToListAsync();
+
TimelineItems = notesResult
.Annotations.Select(n => (TimelineItem)new TimelineItem(
TimelineItemType.Annotation,
@@ -53,9 +96,70 @@ public async Task OnGet()
TimelineItemType.Task,
t.ModifiedOn.WithDqtBstFix(isLocalTime: true)!.Value,
t)))
+ .Concat(eventsWithUser.Select(MapTimelineEvent))
.OrderByDescending(i => i.Timestamp)
.ToArray();
return Page();
}
+
+ private TimelineItem MapTimelineEvent(EventWithUser eventWithUser)
+ {
+ var @event = EventBase.Deserialize(eventWithUser.EventPayload, eventWithUser.EventName);
+ User? user = null;
+ if (eventWithUser.TrsUserUserId is not null)
+ {
+ user = new User
+ {
+ UserId = eventWithUser.TrsUserUserId.Value,
+ Active = eventWithUser.TrsUserActive!.Value,
+ UserType = eventWithUser.TrsUserUserType!.Value,
+ Name = eventWithUser.TrsUserName!,
+ Email = eventWithUser.TrsUserEmail,
+ AzureAdUserId = eventWithUser.TrsUserAzureAdUserId,
+ Roles = eventWithUser.TrsUserRoles!,
+ DqtUserId = eventWithUser.TrsUserDqtUserId
+ };
+ }
+
+ DqtUser? dqtUser = null;
+ if (eventWithUser.DqtUserId is not null)
+ {
+ dqtUser = new DqtUser
+ {
+ UserId = eventWithUser.DqtUserId.Value,
+ Name = eventWithUser.DqtUserName!
+ };
+ }
+
+ RaisedByUser raiseByUser = new()
+ {
+ User = user,
+ DqtUser = dqtUser
+ };
+
+ var timelineEventType = typeof(TimelineEvent<>).MakeGenericType(@event.GetType()!);
+ var timelineEvent = (TimelineEvent)Activator.CreateInstance(timelineEventType, @event, raiseByUser)!;
+ var timelineItemType = typeof(TimelineItem<>).MakeGenericType(timelineEventType);
+ return (TimelineItem)Activator.CreateInstance(timelineItemType, TimelineItemType.Event, timelineEvent.Event.CreatedUtc, timelineEvent)!;
+ }
+
+ ///
+ /// Flattened out record to allow Event, TRS User and DQT User to be returned in a single SQL query
+ ///
+ private record EventWithUser
+ {
+ public required string EventName { get; init; }
+ public required string EventPayload { get; init; }
+ public required Guid? TrsUserUserId { get; init; }
+ public required bool? TrsUserActive { get; set; }
+ public required UserType? TrsUserUserType { get; init; }
+ public required string? TrsUserName { get; set; }
+ public required string? TrsUserEmail { get; set; }
+ public required string? TrsUserAzureAdUserId { get; set; }
+ public required string[]? TrsUserRoles { get; set; }
+ public required Guid? TrsUserDqtUserId { get; set; }
+ public required Guid? DqtUserId { get; set; }
+ public required string? DqtUserName { get; set; }
+ }
}
diff --git a/TeachingRecordSystem/src/TeachingRecordSystem.SupportUi/Pages/Persons/PersonDetail/Timeline/Events/DqtUser.cs b/TeachingRecordSystem/src/TeachingRecordSystem.SupportUi/Pages/Persons/PersonDetail/Timeline/Events/DqtUser.cs
new file mode 100644
index 000000000..7ed51439d
--- /dev/null
+++ b/TeachingRecordSystem/src/TeachingRecordSystem.SupportUi/Pages/Persons/PersonDetail/Timeline/Events/DqtUser.cs
@@ -0,0 +1,7 @@
+namespace TeachingRecordSystem.SupportUi.Pages.Persons.PersonDetail.Timeline.Events;
+
+public record DqtUser
+{
+ public required Guid? UserId { get; set; }
+ public required string? Name { get; set; }
+}
diff --git a/TeachingRecordSystem/src/TeachingRecordSystem.SupportUi/Pages/Persons/PersonDetail/Timeline/Events/MandatoryQualificationDeletedEvent.cshtml b/TeachingRecordSystem/src/TeachingRecordSystem.SupportUi/Pages/Persons/PersonDetail/Timeline/Events/MandatoryQualificationDeletedEvent.cshtml
new file mode 100644
index 000000000..78e18839f
--- /dev/null
+++ b/TeachingRecordSystem/src/TeachingRecordSystem.SupportUi/Pages/Persons/PersonDetail/Timeline/Events/MandatoryQualificationDeletedEvent.cshtml
@@ -0,0 +1,62 @@
+@using TeachingRecordSystem.Core.Events
+@using TeachingRecordSystem.Core.Services.Files
+@inject IFileService FileService
+@model TimelineItem>
+@{
+ var deletedEvent = Model.ItemModel.Event;
+ var mandatoryQualification = deletedEvent.MandatoryQualification;
+ var evidenceFileUrl = deletedEvent.EvidenceFile is not null ? await FileService.GetFileUrl(deletedEvent.EvidenceFile!.FileId, TimeSpan.FromMinutes(15)) : null;
+ var raisedByUser = Model.ItemModel.RaisedByUser;
+ var raisedBy = deletedEvent.RaisedBy.IsDqtUser ? (raisedByUser.DqtUser?.Name ?? "Unknown") : (raisedByUser.User?.Name) ?? "Unknown";
+}
+
+
+
+
+ By @raisedBy on
+
+
+
+
+
+ Reason for deleting
+ @(!string.IsNullOrEmpty(deletedEvent.DeletionReason) ? deletedEvent.DeletionReason : "None")
+
+
+ More detail about the reason for deleting
+
+
+
+ Training provider
+ @(mandatoryQualification.Provider is not null ? mandatoryQualification.Provider.Name : "None" )
+
+
+ Specialism
+ @(mandatoryQualification.Specialism.HasValue ? mandatoryQualification.Specialism.Value.GetTitle() : "None")
+
+
+ Start date
+ @(mandatoryQualification.StartDate.HasValue ? mandatoryQualification.StartDate.Value.ToString("d MMMM yyyy") : "None")
+
+
+ Status
+ @(mandatoryQualification.Status.HasValue ? mandatoryQualification.Status.Value.GetTitle() : "None")
+
+
+ End date
+ @(mandatoryQualification.EndDate.HasValue ? mandatoryQualification.EndDate.Value.ToString("d MMMM yyyy") : "None")
+
+
+
+ @if (evidenceFileUrl is not null)
+ {
+
+ @($"{deletedEvent.EvidenceFile!.Name} (opens in new tab)")
+
+ }
+
+
diff --git a/TeachingRecordSystem/src/TeachingRecordSystem.SupportUi/Pages/Persons/PersonDetail/Timeline/Events/RaisedByUser.cs b/TeachingRecordSystem/src/TeachingRecordSystem.SupportUi/Pages/Persons/PersonDetail/Timeline/Events/RaisedByUser.cs
new file mode 100644
index 000000000..ebdc9295f
--- /dev/null
+++ b/TeachingRecordSystem/src/TeachingRecordSystem.SupportUi/Pages/Persons/PersonDetail/Timeline/Events/RaisedByUser.cs
@@ -0,0 +1,9 @@
+using TeachingRecordSystem.Core.DataStore.Postgres.Models;
+
+namespace TeachingRecordSystem.SupportUi.Pages.Persons.PersonDetail.Timeline.Events;
+
+public record RaisedByUser
+{
+ public required User? User { get; set; }
+ public required DqtUser? DqtUser { get; set; }
+}
diff --git a/TeachingRecordSystem/src/TeachingRecordSystem.SupportUi/Pages/Persons/PersonDetail/Timeline/Events/TimelineEvent.cs b/TeachingRecordSystem/src/TeachingRecordSystem.SupportUi/Pages/Persons/PersonDetail/Timeline/Events/TimelineEvent.cs
new file mode 100644
index 000000000..7595ea71f
--- /dev/null
+++ b/TeachingRecordSystem/src/TeachingRecordSystem.SupportUi/Pages/Persons/PersonDetail/Timeline/Events/TimelineEvent.cs
@@ -0,0 +1,10 @@
+using TeachingRecordSystem.Core.Events;
+
+namespace TeachingRecordSystem.SupportUi.Pages.Persons.PersonDetail.Timeline.Events;
+
+public record TimelineEvent(EventBase Event, RaisedByUser RaisedByUser);
+
+public record TimelineEvent(TEvent Event, RaisedByUser RaisedByUser) : TimelineEvent(Event, RaisedByUser) where TEvent : EventBase
+{
+ public new TEvent Event => (TEvent)base.Event;
+}
diff --git a/TeachingRecordSystem/tests/TeachingRecordSystem.SupportUi.Tests/PageTests/ChangeRequests/IndexTests.cs b/TeachingRecordSystem/tests/TeachingRecordSystem.SupportUi.Tests/PageTests/ChangeRequests/IndexTests.cs
index 810e81a55..c5e9046ae 100644
--- a/TeachingRecordSystem/tests/TeachingRecordSystem.SupportUi.Tests/PageTests/ChangeRequests/IndexTests.cs
+++ b/TeachingRecordSystem/tests/TeachingRecordSystem.SupportUi.Tests/PageTests/ChangeRequests/IndexTests.cs
@@ -49,13 +49,13 @@ public async Task Get_ValidRequest_RendersExpectedContent()
Assert.Equal(createIncident1Result.TicketNumber, tableRow1.GetElementByTestId($"request-reference-{createIncident1Result.TicketNumber}")!.TextContent);
Assert.Equal($"{createPerson1Result.FirstName} {createPerson1Result.LastName}", tableRow1.GetElementByTestId($"name-{createIncident1Result.TicketNumber}")!.TextContent);
Assert.Equal(createIncident1Result.SubjectTitle, tableRow1.GetElementByTestId($"change-type-{createIncident1Result.TicketNumber}")!.TextContent);
- Assert.Equal(createIncident1Result.CreatedOn.ToString("dd/MM/yyyy"), tableRow1.GetElementByTestId($"created-on-{createIncident1Result.TicketNumber}")!.TextContent);
+ Assert.Equal(createIncident1Result.CreatedOn.ToString("d MMMM yyyy"), tableRow1.GetElementByTestId($"created-on-{createIncident1Result.TicketNumber}")!.TextContent);
var tableRow2 = doc.GetElementByTestId($"change-request-{createIncident2Result.TicketNumber}");
Assert.NotNull(tableRow2);
Assert.Equal(createIncident2Result.TicketNumber, tableRow2.GetElementByTestId($"request-reference-{createIncident2Result.TicketNumber}")!.TextContent);
Assert.Equal($"{createPerson2Result.FirstName} {createPerson2Result.LastName}", tableRow2.GetElementByTestId($"name-{createIncident2Result.TicketNumber}")!.TextContent);
Assert.Equal(createIncident2Result.SubjectTitle, tableRow2.GetElementByTestId($"change-type-{createIncident2Result.TicketNumber}")!.TextContent);
- Assert.Equal(createIncident2Result.CreatedOn.ToString("dd/MM/yyyy"), tableRow2.GetElementByTestId($"created-on-{createIncident2Result.TicketNumber}")!.TextContent);
+ Assert.Equal(createIncident2Result.CreatedOn.ToString("d MMMM yyyy"), tableRow2.GetElementByTestId($"created-on-{createIncident2Result.TicketNumber}")!.TextContent);
}
[Fact]
diff --git a/TeachingRecordSystem/tests/TeachingRecordSystem.SupportUi.Tests/PageTests/Persons/PersonDetail/ChangeLogTests.cs b/TeachingRecordSystem/tests/TeachingRecordSystem.SupportUi.Tests/PageTests/Persons/PersonDetail/ChangeLogTests.cs
index f86ab2906..5038d9aef 100644
--- a/TeachingRecordSystem/tests/TeachingRecordSystem.SupportUi.Tests/PageTests/Persons/PersonDetail/ChangeLogTests.cs
+++ b/TeachingRecordSystem/tests/TeachingRecordSystem.SupportUi.Tests/PageTests/Persons/PersonDetail/ChangeLogTests.cs
@@ -1,3 +1,6 @@
+using TeachingRecordSystem.Core.Events;
+using TeachingRecordSystem.Core.Events.Models;
+
namespace TeachingRecordSystem.SupportUi.Tests.PageTests.Persons.PersonDetail;
public class ChangeLogTests : TestBase
@@ -23,7 +26,7 @@ public async Task Get_WithPersonIdForNonExistentPerson_ReturnsNotFound()
}
[Fact]
- public async Task Get_WithPersonIdForPersonWithNoNotes_DisplaysNoChanges()
+ public async Task Get_WithPersonIdForPersonWithNoChanges_DisplaysNoChanges()
{
// Arrange
var person = await TestData.CreatePerson();
@@ -42,15 +45,85 @@ public async Task Get_WithPersonIdForPersonWithNoNotes_DisplaysNoChanges()
}
[Fact]
- public async Task Get_WithPersonIdForPersonWithNotes_DisplaysChangesInDescendingOrder()
+ public async Task Get_WithPersonIdForPersonWithNotesChanges_DisplaysChangesAsExpected()
{
// Arrange
var person = await TestData.CreatePerson();
await TestData.CreateNote(b => b.WithPersonId(person.ContactId).WithSubject("Note 1 Subject").WithDescription("Note 1 Description"));
await TestData.CreateNote(b => b.WithPersonId(person.ContactId).WithSubject("Note 2 Subject").WithDescription("Note 2 Description"));
+
+ var request = new HttpRequestMessage(HttpMethod.Get, $"/persons/{person.ContactId}/changelog");
+
+ // Act
+ var response = await HttpClient.SendAsync(request);
+
+ // Assert
+ Assert.Equal(StatusCodes.Status200OK, (int)response.StatusCode);
+
+ var doc = await response.GetDocument();
+ var changes = doc.GetAllElementsByTestId("timeline-item");
+ Assert.NotEmpty(changes);
+ Assert.Equal(2, changes.Count);
+ Assert.Equal("Note modified", changes[0].GetElementByTestId("timeline-item-title")!.TextContent.Trim());
+ Assert.Equal("by Test User", changes[0].GetElementByTestId("timeline-item-user")!.TextContent.Trim());
+ Assert.Null(changes[0].GetElementByTestId("timeline-item-status"));
+ Assert.NotNull(changes[0].GetElementByTestId("timeline-item-time"));
+ Assert.Equal("Note 2 Subject", changes[0].GetElementByTestId("timeline-item-summary")!.TextContent.Trim());
+ Assert.Equal("Note 2 Description", changes[0].GetElementByTestId("timeline-item-description")!.TextContent.Trim());
+ Assert.Equal("Note modified", changes[1].GetElementByTestId("timeline-item-title")!.TextContent.Trim());
+ Assert.Equal("by Test User", changes[1].GetElementByTestId("timeline-item-user")!.TextContent.Trim());
+ Assert.Null(changes[1].GetElementByTestId("timeline-item-status"));
+ Assert.NotNull(changes[1].GetElementByTestId("timeline-item-time"));
+ Assert.Equal("Note 1 Subject", changes[1].GetElementByTestId("timeline-item-summary")!.TextContent.Trim());
+ Assert.Equal("Note 1 Description", changes[1].GetElementByTestId("timeline-item-description")!.TextContent.Trim());
+ }
+
+ [Fact]
+ public async Task Get_WithPersonIdForPersonWithTaskChanges_DisplaysChangesAsExpected()
+ {
+ // Arrange
+ var person = await TestData.CreatePerson();
await TestData.CreateCrmTask(b => b.WithPersonId(person.ContactId).WithSubject("Task 1 Subject").WithDescription("Task 1 Description"));
await TestData.CreateCrmTask(b => b.WithPersonId(person.ContactId).WithSubject("Task 2 Subject").WithDescription("Task 2 Description").WithDueDate(Clock.UtcNow.AddDays(-2)));
await TestData.CreateCrmTask(b => b.WithPersonId(person.ContactId).WithSubject("Task 3 Subject").WithDescription("Task 3 Description").WithCompletedStatus());
+
+ var request = new HttpRequestMessage(HttpMethod.Get, $"/persons/{person.ContactId}/changelog");
+
+ // Act
+ var response = await HttpClient.SendAsync(request);
+
+ // Assert
+ Assert.Equal(StatusCodes.Status200OK, (int)response.StatusCode);
+
+ var doc = await response.GetDocument();
+ var changes = doc.GetAllElementsByTestId("timeline-item");
+ Assert.NotEmpty(changes);
+ Assert.Equal(3, changes.Count);
+ Assert.Equal("Task completed", changes[0].GetElementByTestId("timeline-item-title")!.TextContent.Trim());
+ Assert.Equal("by Test User", changes[0].GetElementByTestId("timeline-item-user")!.TextContent.Trim());
+ Assert.Equal("Closed", changes[0].GetElementByTestId("timeline-item-status")!.TextContent.Trim());
+ Assert.NotNull(changes[0].GetElementByTestId("timeline-item-time"));
+ Assert.Equal("Task 3 Subject", changes[0].GetElementByTestId("timeline-item-summary")!.TextContent.Trim());
+ Assert.Equal("Task 3 Description", changes[0].GetElementByTestId("timeline-item-description")!.TextContent.Trim());
+ Assert.Equal("Task modified", changes[1].GetElementByTestId("timeline-item-title")!.TextContent.Trim());
+ Assert.Equal("by Test User", changes[1].GetElementByTestId("timeline-item-user")!.TextContent.Trim());
+ Assert.Equal("Overdue", changes[1].GetElementByTestId("timeline-item-status")!.TextContent.Trim());
+ Assert.NotNull(changes[1].GetElementByTestId("timeline-item-time"));
+ Assert.Equal("Task 2 Subject", changes[1].GetElementByTestId("timeline-item-summary")!.TextContent.Trim());
+ Assert.Equal("Task 2 Description", changes[1].GetElementByTestId("timeline-item-description")!.TextContent.Trim());
+ Assert.Equal("Task modified", changes[2].GetElementByTestId("timeline-item-title")!.TextContent.Trim());
+ Assert.Equal("by Test User", changes[2].GetElementByTestId("timeline-item-user")!.TextContent.Trim());
+ Assert.Equal("Active", changes[2].GetElementByTestId("timeline-item-status")!.TextContent.Trim());
+ Assert.NotNull(changes[2].GetElementByTestId("timeline-item-time"));
+ Assert.Equal("Task 1 Subject", changes[2].GetElementByTestId("timeline-item-summary")!.TextContent.Trim());
+ Assert.Equal("Task 1 Description", changes[2].GetElementByTestId("timeline-item-description")!.TextContent.Trim());
+ }
+
+ [Fact]
+ public async Task Get_WithPersonIdForPersonWithNameOrDateOfBirthChanges_DisplaysChangesAsExpected()
+ {
+ // Arrange
+ var person = await TestData.CreatePerson();
await TestData.CreateNameChangeIncident(b => b.WithCustomerId(person.ContactId).WithRejectedStatus());
await TestData.CreateDateOfBirthChangeIncident(b => b.WithCustomerId(person.ContactId).WithApprovedStatus());
@@ -65,7 +138,7 @@ public async Task Get_WithPersonIdForPersonWithNotes_DisplaysChangesInDescending
var doc = await response.GetDocument();
var changes = doc.GetAllElementsByTestId("timeline-item");
Assert.NotEmpty(changes);
- Assert.Equal(7, changes.Count);
+ Assert.Equal(2, changes.Count);
Assert.Equal("Request to change date of birth case resolved", changes[0].GetElementByTestId("timeline-item-title")!.TextContent.Trim());
Assert.Equal("by Test User", changes[0].GetElementByTestId("timeline-item-user")!.TextContent.Trim());
Assert.Null(changes[0].GetElementByTestId("timeline-item-status"));
@@ -76,35 +149,98 @@ public async Task Get_WithPersonIdForPersonWithNotes_DisplaysChangesInDescending
Assert.Null(changes[1].GetElementByTestId("timeline-item-status"));
Assert.NotNull(changes[1].GetElementByTestId("timeline-item-time"));
Assert.Equal("Rejected", changes[1].GetElementByTestId("timeline-item-summary")!.TextContent.Trim());
- Assert.Equal("Task completed", changes[2].GetElementByTestId("timeline-item-title")!.TextContent.Trim());
- Assert.Equal("by Test User", changes[2].GetElementByTestId("timeline-item-user")!.TextContent.Trim());
- Assert.Equal("Closed", changes[2].GetElementByTestId("timeline-item-status")!.TextContent.Trim());
- Assert.NotNull(changes[2].GetElementByTestId("timeline-item-time"));
- Assert.Equal("Task 3 Subject", changes[2].GetElementByTestId("timeline-item-summary")!.TextContent.Trim());
- Assert.Equal("Task 3 Description", changes[2].GetElementByTestId("timeline-item-description")!.TextContent.Trim());
- Assert.Equal("Task modified", changes[3].GetElementByTestId("timeline-item-title")!.TextContent.Trim());
- Assert.Equal("by Test User", changes[3].GetElementByTestId("timeline-item-user")!.TextContent.Trim());
- Assert.Equal("Overdue", changes[3].GetElementByTestId("timeline-item-status")!.TextContent.Trim());
- Assert.NotNull(changes[3].GetElementByTestId("timeline-item-time"));
- Assert.Equal("Task 2 Subject", changes[3].GetElementByTestId("timeline-item-summary")!.TextContent.Trim());
- Assert.Equal("Task 2 Description", changes[3].GetElementByTestId("timeline-item-description")!.TextContent.Trim());
- Assert.Equal("Task modified", changes[4].GetElementByTestId("timeline-item-title")!.TextContent.Trim());
- Assert.Equal("by Test User", changes[4].GetElementByTestId("timeline-item-user")!.TextContent.Trim());
- Assert.Equal("Active", changes[4].GetElementByTestId("timeline-item-status")!.TextContent.Trim());
- Assert.NotNull(changes[4].GetElementByTestId("timeline-item-time"));
- Assert.Equal("Task 1 Subject", changes[4].GetElementByTestId("timeline-item-summary")!.TextContent.Trim());
- Assert.Equal("Task 1 Description", changes[4].GetElementByTestId("timeline-item-description")!.TextContent.Trim());
- Assert.Equal("Note modified", changes[5].GetElementByTestId("timeline-item-title")!.TextContent.Trim());
- Assert.Equal("by Test User", changes[5].GetElementByTestId("timeline-item-user")!.TextContent.Trim());
- Assert.Null(changes[5].GetElementByTestId("timeline-item-status"));
- Assert.NotNull(changes[5].GetElementByTestId("timeline-item-time"));
- Assert.Equal("Note 2 Subject", changes[5].GetElementByTestId("timeline-item-summary")!.TextContent.Trim());
- Assert.Equal("Note 2 Description", changes[5].GetElementByTestId("timeline-item-description")!.TextContent.Trim());
- Assert.Equal("Note modified", changes[6].GetElementByTestId("timeline-item-title")!.TextContent.Trim());
- Assert.Equal("by Test User", changes[6].GetElementByTestId("timeline-item-user")!.TextContent.Trim());
- Assert.Null(changes[6].GetElementByTestId("timeline-item-status"));
- Assert.NotNull(changes[6].GetElementByTestId("timeline-item-time"));
- Assert.Equal("Note 1 Subject", changes[6].GetElementByTestId("timeline-item-summary")!.TextContent.Trim());
- Assert.Equal("Note 1 Description", changes[6].GetElementByTestId("timeline-item-description")!.TextContent.Trim());
+ }
+
+ [Fact]
+ public async Task Get_WithPersonIdForPersonWithDeletedMandatoryQualification_DisplaysChangesAsExpected()
+ {
+ // Arrange
+ var person = await TestData.CreatePerson(b => b.WithMandatoryQualification().WithMandatoryQualification());
+ var mqs = new (bool RaisedByDqtUser, TestData.MandatoryQualificationInfo Mq, DateTime CreatedUtc)[]
+ {
+ (true, person.MandatoryQualifications[0], Clock.UtcNow.AddSeconds(-2)),
+ (false, person.MandatoryQualifications[1], Clock.UtcNow)
+ };
+
+ var dqtUserId = await TestData.GetCurrentCrmUserId();
+ var user = await TestData.CreateUser();
+
+ var deletedEvents = new List();
+
+ foreach (var mqInfo in mqs)
+ {
+ var mq = mqInfo.Mq;
+ var establishment = mq.DqtMqEstablishmentValue is string establishmentValue ?
+ await TestData.ReferenceDataCache.GetMqEstablishmentByValue(mq.DqtMqEstablishmentValue) :
+ null;
+ Core.DataStore.Postgres.Models.MandatoryQualificationProvider.TryMapFromDqtMqEstablishment(establishment, out var provider);
+
+ var deletedEvent = new MandatoryQualificationDeletedEvent()
+ {
+ EventId = Guid.NewGuid(),
+ CreatedUtc = mqInfo.CreatedUtc,
+ RaisedBy = mqInfo.RaisedByDqtUser ? RaisedByUserInfo.FromDqtUser(dqtUserId, "Test User") : RaisedByUserInfo.FromUserId(user.UserId),
+ PersonId = person.ContactId,
+ MandatoryQualification = new()
+ {
+ QualificationId = mq.QualificationId,
+ Provider = new()
+ {
+ MandatoryQualificationProviderId = provider?.MandatoryQualificationProviderId,
+ Name = provider?.Name,
+ DqtMqEstablishmentId = establishment?.Id,
+ DqtMqEstablishmentName = establishment?.dfeta_name
+ },
+ Specialism = mq.Specialism,
+ Status = mq.Status,
+ StartDate = mq.StartDate,
+ EndDate = mq.EndDate,
+ },
+ DeletionReason = "Added in error",
+ DeletionReasonDetail = "Some extra information",
+ EvidenceFile = null
+ };
+
+ deletedEvents.Add(deletedEvent);
+
+ await TestData.DeleteMandatoryQualification(
+ mq.QualificationId,
+ EventInfo.Create(deletedEvent).Serialize(),
+ true);
+ }
+
+ var request = new HttpRequestMessage(HttpMethod.Get, $"/persons/{person.ContactId}/changelog");
+
+ // Act
+ var response = await HttpClient.SendAsync(request);
+
+ // Assert
+ Assert.Equal(StatusCodes.Status200OK, (int)response.StatusCode);
+
+ var doc = await response.GetDocument();
+ var changes = doc.GetAllElementsByTestId("timeline-item");
+ Assert.NotEmpty(changes);
+ Assert.Equal(2, changes.Count);
+ Assert.Null(changes[0].GetElementByTestId("timeline-item-status"));
+ Assert.Equal($"By {user.Name} on", changes[0].GetElementByTestId("raised-by")!.TextContent.Trim());
+ Assert.NotNull(changes[0].GetElementByTestId("timeline-item-time"));
+ Assert.Equal(deletedEvents[1].DeletionReason, changes[0].GetElementByTestId("deletion-reason")!.TextContent.Trim());
+ Assert.Equal(deletedEvents[1].DeletionReasonDetail, changes[0].GetElementByTestId("deletion-reason-detail")!.TextContent.Trim());
+ Assert.Equal(deletedEvents[1].MandatoryQualification.Provider!.Name, changes[0].GetElementByTestId("provider")!.TextContent.Trim());
+ Assert.Equal(deletedEvents[1].MandatoryQualification.Specialism!.Value.GetTitle(), changes[0].GetElementByTestId("specialism")!.TextContent.Trim());
+ Assert.Equal(deletedEvents[1].MandatoryQualification.StartDate!.Value.ToString("d MMMM yyyy"), changes[0].GetElementByTestId("start-date")!.TextContent.Trim());
+ Assert.Equal(deletedEvents[1].MandatoryQualification.Status!.Value.GetTitle(), changes[0].GetElementByTestId("status")!.TextContent.Trim());
+ Assert.Equal(deletedEvents[1].MandatoryQualification.EndDate.HasValue ? deletedEvents[1].MandatoryQualification.EndDate!.Value.ToString("d MMMM yyyy") : "None", changes[0].GetElementByTestId("end-date")!.TextContent.Trim());
+
+ Assert.Null(changes[1].GetElementByTestId("timeline-item-status"));
+ Assert.Equal($"By Test User on", changes[1].GetElementByTestId("raised-by")!.TextContent.Trim());
+ Assert.NotNull(changes[0].GetElementByTestId("timeline-item-time"));
+ Assert.Equal(deletedEvents[0].DeletionReason, changes[1].GetElementByTestId("deletion-reason")!.TextContent.Trim());
+ Assert.Equal(deletedEvents[0].DeletionReasonDetail, changes[1].GetElementByTestId("deletion-reason-detail")!.TextContent.Trim());
+ Assert.Equal(deletedEvents[0].MandatoryQualification.Provider!.Name, changes[1].GetElementByTestId("provider")!.TextContent.Trim());
+ Assert.Equal(deletedEvents[0].MandatoryQualification.Specialism!.Value.GetTitle(), changes[1].GetElementByTestId("specialism")!.TextContent.Trim());
+ Assert.Equal(deletedEvents[0].MandatoryQualification.StartDate!.Value.ToString("d MMMM yyyy"), changes[1].GetElementByTestId("start-date")!.TextContent.Trim());
+ Assert.Equal(deletedEvents[0].MandatoryQualification.Status!.Value.GetTitle(), changes[1].GetElementByTestId("status")!.TextContent.Trim());
+ Assert.Equal(deletedEvents[0].MandatoryQualification.EndDate.HasValue ? deletedEvents[0].MandatoryQualification.EndDate!.Value.ToString("d MMMM yyyy") : "None", changes[1].GetElementByTestId("end-date")!.TextContent.Trim());
}
}
diff --git a/TeachingRecordSystem/tests/TeachingRecordSystem.TestCommon/TestData.UpdateQualification.cs b/TeachingRecordSystem/tests/TeachingRecordSystem.TestCommon/TestData.UpdateQualification.cs
new file mode 100644
index 000000000..c2c725acc
--- /dev/null
+++ b/TeachingRecordSystem/tests/TeachingRecordSystem.TestCommon/TestData.UpdateQualification.cs
@@ -0,0 +1,24 @@
+using Microsoft.Xrm.Sdk.Messages;
+using TeachingRecordSystem.Core.Dqt.Models;
+
+namespace TeachingRecordSystem.TestCommon;
+
+public partial class TestData
+{
+ public async Task DeleteMandatoryQualification(Guid qualificationId, string trsDeletedEvent, bool? syncEnabled = null)
+ {
+ await OrganizationService.ExecuteAsync(new UpdateRequest()
+ {
+ Target = new dfeta_qualification()
+ {
+ Id = qualificationId,
+ dfeta_TrsDeletedEvent = trsDeletedEvent,
+ StateCode = dfeta_qualificationState.Inactive
+ }
+ });
+
+ await SyncConfiguration.SyncIfEnabled(
+ helper => helper.SyncMandatoryQualification(qualificationId, CancellationToken.None),
+ syncEnabled);
+ }
+}