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"; +} + +
+
+

Mandatory qualification deleted

+
+

+ 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); + } +}