Skip to content

Commit

Permalink
Close alert journey (#845)
Browse files Browse the repository at this point in the history
* Added previous names, nino and gender to person view + CRM query changes + tests

* Tweaks from PR comments

* Added most of CRM query and UI for close alerts

* close alerts CRM + UI

* Close Alert UI

* Added CRM and UI tests

* removed anti-forgery attribute now it gets defaulted

* Fixed failing tests

* Added E2E test for close alert

* Fixes following PR comments
  • Loading branch information
hortha authored Oct 3, 2023
1 parent fb6591a commit afd9e9b
Show file tree
Hide file tree
Showing 22 changed files with 737 additions and 30 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
namespace TeachingRecordSystem.Core.Dqt.Queries;

public record CloseSanctionQuery(Guid SanctionId, DateOnly EndDate) : ICrmQuery<bool>;
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
namespace TeachingRecordSystem.Core.Dqt.Queries;

public record GetSanctionDetailsBySanctionIdQuery(Guid SanctionId) : ICrmQuery<SanctionDetailResult?>;
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
using Microsoft.PowerPlatform.Dataverse.Client;
using Microsoft.Xrm.Sdk.Messages;
using TeachingRecordSystem.Core.Dqt.Queries;

namespace TeachingRecordSystem.Core.Dqt.QueryHandlers;

public class CloseSanctionHandler : ICrmQueryHandler<CloseSanctionQuery, bool>
{
public async Task<bool> Execute(CloseSanctionQuery query, IOrganizationServiceAsync organizationService)
{
await organizationService.ExecuteAsync(new UpdateRequest()
{
Target = new dfeta_sanction()
{
Id = query.SanctionId,
dfeta_EndDate = query.EndDate.FromDateOnlyWithDqtBstFix(isLocalTime: true),
dfeta_Spent = true
}
});

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

namespace TeachingRecordSystem.Core.Dqt.QueryHandlers;

public class GetSanctionDetailsBySanctionIdHandler : ICrmQueryHandler<GetSanctionDetailsBySanctionIdQuery, SanctionDetailResult?>
{
public async Task<SanctionDetailResult?> Execute(GetSanctionDetailsBySanctionIdQuery query, IOrganizationServiceAsync organizationService)
{
var filter = new FilterExpression();
filter.AddCondition(dfeta_sanction.PrimaryIdAttribute, ConditionOperator.Equal, query.SanctionId);

var queryExpression = new QueryExpression(dfeta_sanction.EntityLogicalName)
{
ColumnSet = new ColumnSet(
dfeta_sanction.PrimaryIdAttribute,
dfeta_sanction.Fields.dfeta_SanctionDetails,
dfeta_sanction.Fields.dfeta_PersonId,
dfeta_sanction.Fields.dfeta_StartDate,
dfeta_sanction.Fields.dfeta_EndDate,
dfeta_sanction.Fields.dfeta_Spent,
dfeta_sanction.Fields.StateCode),
Criteria = filter
};

var sanctionCodeLink = queryExpression.AddLink(
dfeta_sanctioncode.EntityLogicalName,
dfeta_sanction.Fields.dfeta_SanctionCodeId,
dfeta_sanctioncode.PrimaryIdAttribute,
JoinOperator.Inner);

sanctionCodeLink.Columns = new ColumnSet(dfeta_sanctioncode.PrimaryIdAttribute, dfeta_sanctioncode.Fields.dfeta_name);
sanctionCodeLink.EntityAlias = typeof(dfeta_sanctioncode).Name;

var request = new RetrieveMultipleRequest()
{
Query = queryExpression
};

var result = (RetrieveMultipleResponse)await organizationService.ExecuteAsync(request);
return result.EntityCollection.Entities.Select(entity => new SanctionDetailResult(entity.ToEntity<dfeta_sanction>(), entity.Extract<dfeta_sanctioncode>().dfeta_name)).SingleOrDefault();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
using GovUk.Frontend.AspNetCore.ModelBinding;
using Microsoft.AspNetCore.Mvc.ModelBinding;

namespace TeachingRecordSystem.SupportUi.Infrastructure.ModelBinding;

public class DateOnlyModelBinder : IModelBinder
{
public const string Format = "yyyy-MM-dd";

private readonly IModelBinder _fallbackBinder;

public DateOnlyModelBinder(IModelBinder fallbackBinder)
{
_fallbackBinder = fallbackBinder;
}

public Task BindModelAsync(ModelBindingContext bindingContext)
{
var value = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);

if (!string.IsNullOrEmpty(value.FirstValue))
{
if (DateOnly.TryParseExact(value.FirstValue, Format, out var result))
{
bindingContext.Result = ModelBindingResult.Success(result);
}
else
{
bindingContext.Result = ModelBindingResult.Failed();
}
}
else
{
return _fallbackBinder.BindModelAsync(bindingContext);
}

return Task.CompletedTask;
}
}

public class DateOnlyModelBinderProvider : IModelBinderProvider
{
public IModelBinder? GetBinder(ModelBinderProviderContext context)
{
if (context.Metadata.UnderlyingOrModelType == typeof(DateOnly))
{
var fallbackBinder = new DateInputModelBinder();
return new DateOnlyModelBinder(fallbackBinder);
}

return null;
}
}

This file was deleted.

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
@page "/alerts/{alertId}/close/confirm"
@model TeachingRecordSystem.SupportUi.Pages.Alerts.CloseAlert.ConfirmModel
@{
ViewBag.Title = "Confirm closing alert";
}

@section BeforeContent {
<govuk-back-link href="@LinkGenerator.AlertClose(Model.AlertId, Model.EndDate)">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.AlertCloseConfirm(Model.AlertId, Model.EndDate)" method="post">
<govuk-summary-list>
<govuk-summary-list-row>
<govuk-summary-list-row-key>Alert type</govuk-summary-list-row-key>
<govuk-summary-list-row-value data-testid="alert-type">@Model.AlertType</govuk-summary-list-row-value>
</govuk-summary-list-row>
<govuk-summary-list-row>
<govuk-summary-list-row-key>End date</govuk-summary-list-row-key>
<govuk-summary-list-row-value data-testid="end-date">@Model.EndDate.ToString("dd/MM/yyyy")</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,56 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.AspNetCore.Mvc.RazorPages;
using TeachingRecordSystem.Core.Dqt.Queries;

namespace TeachingRecordSystem.SupportUi.Pages.Alerts.CloseAlert;

public class ConfirmModel : PageModel
{
private readonly TrsLinkGenerator _linkGenerator;
private readonly ICrmQueryDispatcher _crmQueryDispatcher;

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

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

[FromQuery(Name = "endDate")]
public DateOnly EndDate { get; set; }

public string? AlertType { get; set; }

public Guid? PersonId { get; set; }

public async Task<IActionResult> OnPost()
{
await _crmQueryDispatcher.ExecuteQuery(new CloseSanctionQuery(AlertId, EndDate));

TempData.SetFlashSuccess("Alert closed");

return Redirect(_linkGenerator.PersonAlerts(PersonId!.Value));
}

public override async Task OnPageHandlerExecutionAsync(PageHandlerExecutingContext context, PageHandlerExecutionDelegate next)
{
var alert = await _crmQueryDispatcher.ExecuteQuery(new GetSanctionDetailsBySanctionIdQuery(AlertId));
if (alert is null
|| alert.Sanction.StateCode != Core.Dqt.Models.dfeta_sanctionState.Active
|| alert.Sanction.dfeta_EndDate is not null)
{
context.Result = NotFound();
return;
}

AlertType = alert.Description;
PersonId = alert.Sanction.dfeta_PersonId.Id;

await next();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
@page "/alerts/{alertId}/close"
@model IndexModel
@{
ViewBag.Title = "Close alert";
}

@section BeforeContent {
<govuk-back-link href="@LinkGenerator.PersonDetail(Model.PersonId!.Value)">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.AlertClose(Model.AlertId, endDate: null)" method="post">
<govuk-date-input asp-for="EndDate">
<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,69 @@
using System.ComponentModel.DataAnnotations;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.AspNetCore.Mvc.RazorPages;
using TeachingRecordSystem.Core.Dqt.Queries;

namespace TeachingRecordSystem.SupportUi.Pages.Alerts.CloseAlert;

public class IndexModel : PageModel
{
private readonly TrsLinkGenerator _linkGenerator;
private readonly ICrmQueryDispatcher _crmQueryDispatcher;

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

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

[BindProperty(SupportsGet = true)]
[Display(Name = "End date")]
public DateOnly? EndDate { get; set; }

public Guid? PersonId { get; set; }

public DateOnly? StartDate { get; set; }

public IActionResult OnPost()
{
if (EndDate is null)
{
ModelState.AddModelError(nameof(EndDate), "Add an end date");
}

if (EndDate <= StartDate)
{
ModelState.AddModelError(nameof(EndDate), "End date must be after the start date");
}

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

return Redirect(_linkGenerator.AlertCloseConfirm(AlertId, EndDate!.Value));
}

public override async Task OnPageHandlerExecutionAsync(PageHandlerExecutingContext context, PageHandlerExecutionDelegate next)
{
var alert = await _crmQueryDispatcher.ExecuteQuery(new GetSanctionDetailsBySanctionIdQuery(AlertId));
if (alert is null
|| alert.Sanction.StateCode != Core.Dqt.Models.dfeta_sanctionState.Active
|| alert.Sanction.dfeta_EndDate is not null)
{
context.Result = NotFound();
return;
}

PersonId = alert.Sanction.dfeta_PersonId.Id;
StartDate = alert.Sanction.dfeta_StartDate.ToDateOnlyWithDqtBstFix(isLocalTime: true);

await next();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ else
<govuk-summary-card data-testid="[email protected]">
<govuk-summary-card-title data-testid="[email protected]">@alert.Description</govuk-summary-card-title>
<govuk-summary-card-actions>
<govuk-summary-card-action href="@LinkGenerator.CloseAlert(alert.AlertId)" visually-hidden-text="Close">Close</govuk-summary-card-action>
<govuk-summary-card-action href="@LinkGenerator.AlertClose(alert.AlertId, endDate: null)" visually-hidden-text="close alert" data-testid="[email protected]">Close</govuk-summary-card-action>
</govuk-summary-card-actions>
<govuk-summary-list>
<govuk-summary-list-row>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
using TeachingRecordSystem.SupportUi;
using TeachingRecordSystem.SupportUi.Infrastructure;
using TeachingRecordSystem.SupportUi.Infrastructure.Filters;
using TeachingRecordSystem.SupportUi.Infrastructure.ModelBinding;
using TeachingRecordSystem.SupportUi.Infrastructure.Redis;
using TeachingRecordSystem.SupportUi.Infrastructure.Security;
using TeachingRecordSystem.SupportUi.Services;
Expand Down Expand Up @@ -116,6 +117,8 @@
options.Filters.Add(new AuthorizeFilter(policy));

options.Filters.Add(new CheckUserExistsFilter());

options.ModelBinderProviders.Insert(2, new DateOnlyModelBinderProvider());
})
.AddCookieTempDataProvider(options =>
{
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
using TeachingRecordSystem.Core.Dqt.Models;
using TeachingRecordSystem.SupportUi.Infrastructure.ModelBinding;

namespace TeachingRecordSystem.SupportUi;

public class TrsLinkGenerator
{
protected const string DateOnlyFormat = DateOnlyModelBinder.Format;

private readonly LinkGenerator _linkGenerator;

public TrsLinkGenerator(LinkGenerator linkGenerator)
Expand All @@ -19,7 +22,9 @@ public TrsLinkGenerator(LinkGenerator linkGenerator)

public string Alert(Guid alertId) => GetRequiredPathByPage("/Alerts/Alert/Index", routeValues: new { alertId });

public string CloseAlert(Guid alertId) => GetRequiredPathByPage("/Alerts/Alert/Close", routeValues: new { alertId });
public string AlertClose(Guid alertId, DateOnly? endDate) => GetRequiredPathByPage("/Alerts/CloseAlert/Index", routeValues: new { alertId, endDate = endDate?.ToString(DateOnlyFormat) });

public string AlertCloseConfirm(Guid alertId, DateOnly endDate) => GetRequiredPathByPage("/Alerts/CloseAlert/Confirm", routeValues: new { alertId, endDate = endDate.ToString(DateOnlyFormat) });

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

Expand Down
Loading

0 comments on commit afd9e9b

Please sign in to comment.