Skip to content

Commit

Permalink
CRM + UI + tests for editing a person's date of birth (#880)
Browse files Browse the repository at this point in the history
  • Loading branch information
hortha authored Oct 27, 2023
1 parent 96c50b9 commit c1d16ef
Show file tree
Hide file tree
Showing 16 changed files with 673 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
namespace TeachingRecordSystem.Core.Dqt.Queries;

public record UpdateContactDateOfBirthQuery(Guid ContactId, DateOnly? DateOfBirth) : ICrmQuery<bool>;

Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
using Microsoft.PowerPlatform.Dataverse.Client;
using Microsoft.Xrm.Sdk.Messages;
using TeachingRecordSystem.Core.Dqt.Queries;

namespace TeachingRecordSystem.Core.Dqt.QueryHandlers;

public class UpdateContactDateOfBirthHandler : ICrmQueryHandler<UpdateContactDateOfBirthQuery, bool>
{
public async Task<bool> Execute(UpdateContactDateOfBirthQuery query, IOrganizationServiceAsync organizationService)
{
await organizationService.ExecuteAsync(new UpdateRequest()
{
Target = new Contact()
{
Id = query.ContactId,
BirthDate = query.DateOfBirth.FromDateOnlyWithDqtBstFix(isLocalTime: false)
}
});

return true;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,5 @@ public static class JourneyNames
public const string AddAlert = nameof(AddAlert);
public const string CloseAlert = nameof(CloseAlert);
public const string EditName = nameof(EditName);
public const string EditDateOfBirth = nameof(EditDateOfBirth);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
@page "/persons/{personId}/edit-date-of-birth/confirm"
@model TeachingRecordSystem.SupportUi.Pages.Persons.PersonDetail.EditDateOfBirth.ConfirmModel
@{
ViewBag.Title = "Confirm date of birth change";
}

@section BeforeContent {
<govuk-back-link href="@LinkGenerator.PersonEditDateOfBirth(Model.PersonId, Model.JourneyInstance!.InstanceId)" />
}

<div class="govuk-grid-row">
<div class="govuk-grid-column-two-thirds-from-desktop">
<h1 class="govuk-heading-l">@ViewBag.Title</h1>
<form action="@LinkGenerator.PersonEditDateOfBirthConfirm(Model.PersonId, Model.JourneyInstance!.InstanceId)" method="post">
<govuk-summary-list>
<govuk-summary-list-row>
<govuk-summary-list-row-key>Current</govuk-summary-list-row-key>
<govuk-summary-list-row-value data-testid="current-value">@Model.CurrentValue</govuk-summary-list-row-value>
</govuk-summary-list-row>
<govuk-summary-list-row>
<govuk-summary-list-row-key>Change to</govuk-summary-list-row-key>
<govuk-summary-list-row-value data-testid="new-value">@Model.NewValue</govuk-summary-list-row-value>
</govuk-summary-list-row>
</govuk-summary-list>

<govuk-button type="submit">Confirm</govuk-button>
</form>
</div>
</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.AspNetCore.Mvc.RazorPages;
using TeachingRecordSystem.Core.Dqt.Models;
using TeachingRecordSystem.Core.Dqt.Queries;

namespace TeachingRecordSystem.SupportUi.Pages.Persons.PersonDetail.EditDateOfBirth;

[Journey(JourneyNames.EditDateOfBirth), RequireJourneyInstance]
public class ConfirmModel : PageModel
{
private readonly TrsLinkGenerator _linkGenerator;
private readonly ICrmQueryDispatcher _crmQueryDispatcher;

public ConfirmModel(
TrsLinkGenerator linkGenerator,
ICrmQueryDispatcher crmQueryDispatcher)
{
_linkGenerator = linkGenerator;
_crmQueryDispatcher = crmQueryDispatcher;
}

public JourneyInstance<EditDateOfBirthState>? JourneyInstance { get; set; }

[FromRoute]
public Guid PersonId { get; set; }

public string? CurrentValue { get; set; }

public string? NewValue { get; set; }

public async Task<IActionResult> OnPost()
{
await _crmQueryDispatcher.ExecuteQuery(
new UpdateContactDateOfBirthQuery(
PersonId,
JourneyInstance!.State.DateOfBirth));

await JourneyInstance!.CompleteAsync();

TempData.SetFlashSuccess("Record has been updated");

return Redirect(_linkGenerator.PersonDetail(PersonId));
}

public override async Task OnPageHandlerExecutionAsync(PageHandlerExecutingContext context, PageHandlerExecutionDelegate next)
{
var state = JourneyInstance!.State;
if (!state.IsComplete)
{
context.Result = Redirect(_linkGenerator.PersonEditDateOfBirth(PersonId, JourneyInstance!.InstanceId));
return;
}

var person = await _crmQueryDispatcher.ExecuteQuery(
new GetContactDetailByIdQuery(
PersonId,
new ColumnSet(
Contact.PrimaryIdAttribute,
Contact.Fields.BirthDate)));

CurrentValue = person!.Contact.BirthDate.ToDateOnlyWithDqtBstFix(isLocalTime: false)!.Value.ToString("dd/MM/yyyy");
NewValue = state.DateOfBirth!.Value.ToString("dd/MM/yyyy");

await next();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
using System.Diagnostics.CodeAnalysis;
using System.Text.Json.Serialization;
using TeachingRecordSystem.Core.Dqt.Models;
using TeachingRecordSystem.Core.Dqt.Queries;

namespace TeachingRecordSystem.SupportUi.Pages.Persons.PersonDetail.EditDateOfBirth;

public class EditDateOfBirthState
{
public bool Initialized { get; set; }

public DateOnly? DateOfBirth { get; set; }

[JsonIgnore]
[MemberNotNullWhen(true, nameof(DateOfBirth))]
public bool IsComplete => DateOfBirth.HasValue;

public async Task EnsureInitialized(ICrmQueryDispatcher crmQueryDispatcher, Guid personId)
{
if (Initialized)
{
return;
}

var person = await crmQueryDispatcher.ExecuteQuery(
new GetContactDetailByIdQuery(
personId,
new ColumnSet(
Contact.PrimaryIdAttribute,
Contact.Fields.BirthDate)));
DateOfBirth = person!.Contact.BirthDate.ToDateOnlyWithDqtBstFix(isLocalTime: false);
Initialized = true;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
@page "/persons/{personId}/edit-date-of-birth"
@model TeachingRecordSystem.SupportUi.Pages.Persons.PersonDetail.EditDateOfBirth.IndexModel
@{
ViewBag.Title = "Change date of birth";
}

@section BeforeContent {
<govuk-back-link href="@LinkGenerator.PersonDetail(Model.PersonId)">Back</govuk-back-link>
}

<h1 class="govuk-heading-l">@ViewBag.Title</h1>

<div class="govuk-grid-row">
<div class="govuk-grid-column-two-thirds-from-desktop">
<form action="@LinkGenerator.PersonEditDateOfBirth(Model.PersonId, Model.JourneyInstance!.InstanceId)" method="post">
<govuk-date-input asp-for="DateOfBirth">
<govuk-date-input-fieldset>
<govuk-date-input-fieldset-legend class="govuk-fieldset__legend--m" />
</govuk-date-input-fieldset>
</govuk-date-input>

<govuk-button type="submit">Continue</govuk-button>
</form>
</div>
</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
using System.ComponentModel.DataAnnotations;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.AspNetCore.Mvc.RazorPages;

namespace TeachingRecordSystem.SupportUi.Pages.Persons.PersonDetail.EditDateOfBirth;

[Journey(JourneyNames.EditDateOfBirth), ActivatesJourney, RequireJourneyInstance]
public class IndexModel : PageModel
{
private readonly TrsLinkGenerator _linkGenerator;
private readonly ICrmQueryDispatcher _crmQueryDispatcher;

public IndexModel(
TrsLinkGenerator linkGenerator,
ICrmQueryDispatcher crmQueryDispatcher)
{
_linkGenerator = linkGenerator;
_crmQueryDispatcher = crmQueryDispatcher;
}

public JourneyInstance<EditDateOfBirthState>? JourneyInstance { get; set; }

[FromRoute]
public Guid PersonId { get; set; }

[BindProperty]
[Display(Name = "Date of birth")]
public DateOnly? DateOfBirth { get; set; }

public void OnGet()
{
DateOfBirth ??= JourneyInstance!.State.DateOfBirth;
}

public async Task<IActionResult> OnPost()
{
if (DateOfBirth is null)
{
ModelState.AddModelError(nameof(DateOfBirth), "Enter a date of birth");
}

if (!ModelState.IsValid)
{
return this.PageWithErrors();
}

await JourneyInstance!.UpdateStateAsync(s => s.DateOfBirth = DateOfBirth);

return Redirect(_linkGenerator.PersonEditDateOfBirthConfirm(PersonId, JourneyInstance!.InstanceId));
}

public override async Task OnPageHandlerExecutionAsync(PageHandlerExecutingContext context, PageHandlerExecutionDelegate next)
{
await JourneyInstance!.State.EnsureInitialized(_crmQueryDispatcher, PersonId);

await next();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,9 @@
<govuk-summary-list-row>
<govuk-summary-list-row-key>Date of birth</govuk-summary-list-row-key>
<govuk-summary-list-row-value data-testid="personal-details-date-of-birth" use-empty-fallback>@(Model.Person.DateOfBirth.HasValue ? Model.Person.DateOfBirth.Value.ToString("dd/MM/yyyy") : string.Empty)</govuk-summary-list-row-value>
<govuk-summary-list-row-actions>
<govuk-summary-list-row-action href="@LinkGenerator.PersonEditDateOfBirth(Model.PersonId, null)" visually-hidden-text="change date of birth" data-testid="date-of-birth-change-link">Change</govuk-summary-list-row-action>
</govuk-summary-list-row-actions>
</govuk-summary-list-row>
<govuk-summary-list-row>
<govuk-summary-list-row-key>Gender</govuk-summary-list-row-key>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,12 @@
typeof(TeachingRecordSystem.SupportUi.Pages.Persons.PersonDetail.EditName.EditNameState),
requestDataKeys: new[] { "personId" },
appendUniqueKey: true));

options.JourneyRegistry.RegisterJourney(new JourneyDescriptor(
JourneyNames.EditDateOfBirth,
typeof(TeachingRecordSystem.SupportUi.Pages.Persons.PersonDetail.EditDateOfBirth.EditDateOfBirthState),
requestDataKeys: new[] { "personId" },
appendUniqueKey: true));
});

builder.Services
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,10 @@ public string PersonChangeLog(Guid personId, string? search = null, ContactSearc

public string PersonEditNameConfirm(Guid personId, JourneyInstanceId journeyInstanceId) => GetRequiredPathByPage("/Persons/PersonDetail/EditName/Confirm", routeValues: new { personId }, journeyInstanceId: journeyInstanceId);

public string PersonEditDateOfBirth(Guid personId, JourneyInstanceId? journeyInstanceId) => GetRequiredPathByPage("/Persons/PersonDetail/EditDateOfBirth/Index", routeValues: new { personId }, journeyInstanceId: journeyInstanceId);

public string PersonEditDateOfBirthConfirm(Guid personId, JourneyInstanceId journeyInstanceId) => GetRequiredPathByPage("/Persons/PersonDetail/EditDateOfBirth/Confirm", routeValues: new { personId }, journeyInstanceId: journeyInstanceId);

public string Users() => GetRequiredPathByPage("/Users/Index");

public string AddUser() => GetRequiredPathByPage("/Users/AddUser/Index");
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
namespace TeachingRecordSystem.Core.Dqt.Tests.QueryTests;

public class UpdateContactDateOfBirthTests : IAsyncLifetime
{
private readonly CrmClientFixture.TestDataScope _dataScope;
private readonly CrmQueryDispatcher _crmQueryDispatcher;

public UpdateContactDateOfBirthTests(CrmClientFixture crmClientFixture)
{
_dataScope = crmClientFixture.CreateTestDataScope();
_crmQueryDispatcher = _dataScope.CreateQueryDispatcher();
}

public Task InitializeAsync() => Task.CompletedTask;

public async Task DisposeAsync() => await _dataScope.DisposeAsync();

[Fact]
public async Task QueryExecutesSuccessfully()
{
// Arrange
var person = await _dataScope.TestData.CreatePerson();
var newDateOfBirth = _dataScope.TestData.GenerateChangedDateOfBirth(person.DateOfBirth);

var query = new UpdateContactDateOfBirthQuery(
person.ContactId,
newDateOfBirth);

// Act
await _crmQueryDispatcher.ExecuteQuery(query);

// Assert
using var ctx = new DqtCrmServiceContext(_dataScope.OrganizationService);

var updatedContact = ctx.ContactSet.SingleOrDefault(c => c.GetAttributeValue<Guid>(Contact.PrimaryIdAttribute) == person.ContactId);
Assert.NotNull(updatedContact);
Assert.Equal(newDateOfBirth, updatedContact.BirthDate.ToDateOnlyWithDqtBstFix(isLocalTime: false));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,16 @@ public static async Task AssertOnPersonEditNameConfirmPage(this IPage page, Guid
await page.WaitForUrlPathAsync($"/persons/{personId}/edit-name/confirm");
}

public static async Task AssertOnPersonEditDateOfBirthPage(this IPage page, Guid personId)
{
await page.WaitForUrlPathAsync($"/persons/{personId}/edit-date-of-birth");
}

public static async Task AssertOnPersonEditDateOfBirthConfirmPage(this IPage page, Guid personId)
{
await page.WaitForUrlPathAsync($"/persons/{personId}/edit-date-of-birth/confirm");
}

public static async Task AssertFlashMessage(this IPage page, string expectedHeader)
{
Assert.Equal(expectedHeader, await page.InnerTextAsync($".govuk-notification-banner__heading:text-is('{expectedHeader}')"));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,4 +39,35 @@ public async Task EditName()

await page.AssertFlashMessage("Record has been updated");
}

[Fact]
public async Task EditDateOfBirth()
{
var person = await TestData.CreatePerson();
var personId = person.ContactId;
var newDateOfBirth = TestData.GenerateChangedDateOfBirth(person.DateOfBirth);

await using var context = await HostFixture.CreateBrowserContext();
var page = await context.NewPageAsync();

await page.GoToPersonDetailPage(personId);

await page.AssertOnPersonDetailPage(personId);

await page.ClickLinkForElementWithTestId("date-of-birth-change-link");

await page.AssertOnPersonEditDateOfBirthPage(personId);

await page.FillDateInput(newDateOfBirth);

await page.ClickContinueButton();

await page.AssertOnPersonEditDateOfBirthConfirmPage(personId);

await page.ClickConfirmButton();

await page.AssertOnPersonDetailPage(personId);

await page.AssertFlashMessage("Record has been updated");
}
}
Loading

0 comments on commit c1d16ef

Please sign in to comment.