Skip to content

Commit

Permalink
Feature/183558 Added page for providing extra information about proje…
Browse files Browse the repository at this point in the history
…ct status change when status is cancelled or withdrawn (#989)

* Project status reason backend

* Frontend reasons page WIP

* Edit status reason and display in project overview

* Added API test

* Updated cypress tests, fixed referrer functionality

* Fixed bug with selecting cancelled/withdrawn reason

* Added section break
  • Loading branch information
jack-nimbleapproach authored Jan 15, 2025
1 parent b7a2663 commit 8356c6f
Show file tree
Hide file tree
Showing 28 changed files with 14,344 additions and 382 deletions.
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using Dfe.ManageFreeSchoolProjects.API.Contracts.Project.Risk;
using Dfe.ManageFreeSchoolProjects.API.Contracts.Common;
using Dfe.ManageFreeSchoolProjects.API.Contracts.Project.Risk;
using Dfe.ManageFreeSchoolProjects.API.Contracts.Project.Sites;
using Dfe.ManageFreeSchoolProjects.API.Contracts.Project.Tasks;

Expand All @@ -20,12 +21,22 @@ public record ProjectStatusResponse
{
public string CurrentFreeSchoolName { get; set; }
public ProjectStatus ProjectStatus { get; set; }


public ProjectCancelledReason ProjectCancelledReason { get; set; }

public ProjectWithdrawnReason ProjectWithdrawnReason { get; set; }

public DateTime? ProjectClosedDate { get; set; }

public DateTime? ProjectCancelledDate { get; set; }

public DateTime? ProjectWithdrawnDate { get; set; }

public YesNo? ProjectCancelledDueToNationalReviewOfPipelineProjects { get; set; }
public YesNo? ProjectWithdrawnDueToNationalReviewOfPipelineProjects { get; set; }
public string CommentaryForCancellation { get; set; }
public string CommentaryForWithdrawal { get; set; }

public string FreeSchoolsApplicationNumber { get; set; }
public string ProjectId { get; set; }
public string Urn { get; set; }
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
using System.ComponentModel;

namespace Dfe.ManageFreeSchoolProjects.API.Contracts.Project
{
public enum ProjectCancelledReason
{
NotSet,

[Description("Educational")]
Educational,

[Description("Governance")]
Governance,

[Description("Planning")]
Planning,

[Description("Procurement / Construction")]
ProcurementConstruction,

[Description("Property")]
Property,

[Description("Pupil numbers / viability")]
PupilNumbersViability,

[Description("Trust not content with site option")]
TrustNotContentWithSiteOption,

[Description("Trust not willing to open in temporary accommodation")]
TrustNotWillingToOpenInTemporaryAccommodation


}

public enum ProjectWithdrawnReason
{
NotSet,

[Description("Educational")]
Educational,

[Description("Governance")]
Governance,

[Description("Planning")]
Planning,

[Description("Procurement / Construction")]
ProcurementConstruction,

[Description("Property")]
Property,

[Description("Pupil numbers / viability")]
PupilNumbersViability,

[Description("Trust not content with site option")]
TrustNotContentWithSiteOption,

[Description("Trust not willing to open in temporary accommodation")]
TrustNotWillingToOpenInTemporaryAccommodation


}
}
Original file line number Diff line number Diff line change
@@ -1,12 +1,26 @@
using Dfe.ManageFreeSchoolProjects.API.Contracts.Common;

namespace Dfe.ManageFreeSchoolProjects.API.Contracts.Project;

public class UpdateProjectStatusRequest
{
public ProjectStatus ProjectStatus { get; set; }


public ProjectCancelledReason ProjectCancelledReason { get; set; }

public ProjectWithdrawnReason ProjectWithdrawnReason { get; set; }

public DateTime? CancelledDate { get; set; }

public DateTime? ClosedDate { get; set; }

public DateTime? WithdrawnDate { get; set; }

public YesNo? ProjectCancelledDueToNationalReviewOfPipelineProjects { get; set; }

public YesNo? ProjectWithdrawnDueToNationalReviewOfPipelineProjects { get; set; }

public string CommentaryForCancellation { get; set; }
public string CommentaryForWithdrawal { get; set; }

}
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,11 @@ namespace Dfe.ManageFreeSchoolProjects.API.Contracts.Project;
public class UpdateProjectStatusResponse
{
public ProjectStatus ProjectStatus { get; set; }


public ProjectCancelledReason ProjectCancelledReason { get; set; }

public ProjectWithdrawnReason ProjectWithdrawnReason { get; set; }

public DateTime? CancelledDate { get; set; }

public DateTime? ClosedDate { get; set; }
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
using Azure.Core;
using Dfe.ManageFreeSchoolProjects.API.Contracts.Common;
using Dfe.ManageFreeSchoolProjects.API.Contracts.Project;
using Dfe.ManageFreeSchoolProjects.API.Contracts.ResponseModels;
using Dfe.ManageFreeSchoolProjects.API.Tests.Fixtures;
using Dfe.ManageFreeSchoolProjects.API.Tests.Helpers;
using Dfe.ManageFreeSchoolProjects.API.UseCases.Project;
using Dfe.ManageFreeSchoolProjects.API.UseCases.ProjectOverview;
using System;
using System.Net;
using System.Net.Http.Json;
using System.Threading.Tasks;

namespace Dfe.ManageFreeSchoolProjects.API.Tests.Integration
{
[Collection(ApiTestCollection.ApiTestCollectionName)]
public class ProjectStatusApiTests : ApiTestsBase
{
public ProjectStatusApiTests(ApiTestFixture apiTestFixture) : base(apiTestFixture)
{
}

[Fact]
public async Task When_Post_ProjectDoesNotExist_Returns_404()
{
var projectId = Guid.NewGuid().ToString();
var updateStatusRequest = _autoFixture.Create<UpdateProjectStatusRequest>();

var updateStatusResponse = await _client.PostAsync($"/api/v1/client/updateprojectstatus?projectId={projectId}", updateStatusRequest.ConvertToJson());
updateStatusResponse.StatusCode.Should().Be(HttpStatusCode.NotFound);
}

[Fact]
public async Task When_Post_StatusValid_Returns_200()
{
var project = DatabaseModelBuilder.BuildProject();
var projectId = project.ProjectStatusProjectId;
var updateStatusRequest = new UpdateProjectStatusRequest()
{
ProjectStatus = ProjectStatus.Open,
ProjectCancelledReason = ProjectCancelledReason.NotSet,
ProjectWithdrawnReason = ProjectWithdrawnReason.NotSet,
CancelledDate = null,
ClosedDate = null,
WithdrawnDate = null,
ProjectCancelledDueToNationalReviewOfPipelineProjects = null,
ProjectWithdrawnDueToNationalReviewOfPipelineProjects = null,
CommentaryForCancellation = null,
CommentaryForWithdrawal = null

};


using var context = _testFixture.GetContext();
context.Kpi.Add(project);
await context.SaveChangesAsync();

var updateStatusResponse = await _client.PostAsync($"/api/v1/client/updateprojectstatus?projectId={projectId}", updateStatusRequest.ConvertToJson());
updateStatusResponse.StatusCode.Should().Be(HttpStatusCode.OK);

var overviewResponse = await _client.GetAsync($"/api/v1/client/projects/{project.ProjectStatusProjectId}/overview");
overviewResponse.StatusCode.Should().Be(HttpStatusCode.OK);

var result = await overviewResponse.Content.ReadFromJsonAsync<ApiSingleResponseV2<ProjectOverviewResponse>>();

// Project status
var projectStatus = result.Data.ProjectStatus;
projectStatus.ProjectStatus.Should().Be(ProjectStatus.Open);
projectStatus.ProjectCancelledReason.Should().Be(ProjectCancelledReason.NotSet);
projectStatus.ProjectWithdrawnReason.Should().Be(ProjectWithdrawnReason.NotSet);
projectStatus.ProjectCancelledDate.Should().BeNull();
projectStatus.ProjectClosedDate.Should().BeNull();
projectStatus.ProjectWithdrawnDate.Should().BeNull();
projectStatus.ProjectCancelledDueToNationalReviewOfPipelineProjects.Should().BeNull();
projectStatus.ProjectWithdrawnDueToNationalReviewOfPipelineProjects.Should().BeNull();
projectStatus.CommentaryForCancellation.Should().BeNull();
projectStatus.CommentaryForWithdrawal.Should().BeNull();


}

[Fact]
public async Task When_Post_StatusCancelledValid_Returns_200()
{
var project = DatabaseModelBuilder.BuildProject();
var projectId = project.ProjectStatusProjectId;

var cancelledDate = DateTime.UtcNow.Date;
var commentaryForCancellation = "Cancelled due to issues with planning";
var updateStatusRequest = new UpdateProjectStatusRequest()
{
ProjectStatus = ProjectStatus.Cancelled,
ProjectCancelledReason = ProjectCancelledReason.Planning,
ProjectWithdrawnReason = ProjectWithdrawnReason.NotSet,
CancelledDate = cancelledDate,
ClosedDate = null,
WithdrawnDate = null,
ProjectCancelledDueToNationalReviewOfPipelineProjects = YesNo.No,
ProjectWithdrawnDueToNationalReviewOfPipelineProjects = null,
CommentaryForCancellation = commentaryForCancellation,
CommentaryForWithdrawal = null

};


using var context = _testFixture.GetContext();
context.Kpi.Add(project);
await context.SaveChangesAsync();

var updateStatusResponse = await _client.PostAsync($"/api/v1/client/updateprojectstatus?projectId={projectId}", updateStatusRequest.ConvertToJson());
updateStatusResponse.StatusCode.Should().Be(HttpStatusCode.OK);

var overviewResponse = await _client.GetAsync($"/api/v1/client/projects/{project.ProjectStatusProjectId}/overview");
overviewResponse.StatusCode.Should().Be(HttpStatusCode.OK);

var result = await overviewResponse.Content.ReadFromJsonAsync<ApiSingleResponseV2<ProjectOverviewResponse>>();

// Project status
var projectStatus = result.Data.ProjectStatus;
projectStatus.ProjectStatus.Should().Be(ProjectStatus.Cancelled);
projectStatus.ProjectCancelledReason.Should().Be(ProjectCancelledReason.Planning);
projectStatus.ProjectWithdrawnReason.Should().Be(ProjectWithdrawnReason.NotSet);
projectStatus.ProjectCancelledDate.Should().Be(cancelledDate);
projectStatus.ProjectClosedDate.Should().BeNull();
projectStatus.ProjectWithdrawnDate.Should().BeNull();
projectStatus.ProjectCancelledDueToNationalReviewOfPipelineProjects.Should().Be(YesNo.No);
projectStatus.ProjectWithdrawnDueToNationalReviewOfPipelineProjects.Should().BeNull();
projectStatus.CommentaryForCancellation.Should().Be(commentaryForCancellation);
projectStatus.CommentaryForWithdrawal.Should().BeNull();


}

}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
using Dfe.ManageFreeSchoolProjects.API.Contracts.Project.Tasks;
using SchoolType = Dfe.ManageFreeSchoolProjects.API.Contracts.Project.SchoolType;
using ProjectStatusType = Dfe.ManageFreeSchoolProjects.API.Contracts.Project.ProjectStatus;
using Dfe.ManageFreeSchoolProjects.Data.Entities.Existing;
using ProjectCancelledReasonType = Dfe.ManageFreeSchoolProjects.API.Contracts.Project.ProjectCancelledReason;
using ProjectWithdrawnReasonType = Dfe.ManageFreeSchoolProjects.API.Contracts.Project.ProjectWithdrawnReason;

namespace Dfe.ManageFreeSchoolProjects.API.UseCases.Project
{
Expand Down Expand Up @@ -121,5 +122,69 @@ public static string FromProjectStatusType(ProjectStatusType projectStatus)
_ => throw new ArgumentOutOfRangeException(nameof(projectStatus), projectStatus, null)
};
}

public static ProjectCancelledReasonType ToProjectCancelledReasonType(string projectCancelledReason)
{
return projectCancelledReason?.ToLower() switch
{
"educational" => ProjectCancelledReasonType.Educational,
"governance" => ProjectCancelledReasonType.Governance,
"planning" => ProjectCancelledReasonType.Planning,
"procurement / construction" => ProjectCancelledReasonType.ProcurementConstruction,
"property" => ProjectCancelledReasonType.Property,
"pupil numbers / viability" => ProjectCancelledReasonType.PupilNumbersViability,
"trust not content with site option" => ProjectCancelledReasonType.TrustNotContentWithSiteOption,
"trust not willing to open in temporary accommodation" => ProjectCancelledReasonType.TrustNotWillingToOpenInTemporaryAccommodation,
_ => ProjectCancelledReasonType.NotSet
};
}

public static string FromProjectCancelledReasonType(ProjectCancelledReasonType projectCancelledReason)
{
return projectCancelledReason switch
{
ProjectCancelledReasonType.Educational => "educational",
ProjectCancelledReasonType.Governance => "governance",
ProjectCancelledReasonType.Planning => "planning",
ProjectCancelledReasonType.ProcurementConstruction => "procurement / construction",
ProjectCancelledReasonType.Property => "property",
ProjectCancelledReasonType.PupilNumbersViability => "pupil numbers / viability",
ProjectCancelledReasonType.TrustNotContentWithSiteOption => "trust not content with site option",
ProjectCancelledReasonType.TrustNotWillingToOpenInTemporaryAccommodation => "trust not willing to open in temporary accommodation",
_ => ""
};
}

public static ProjectWithdrawnReasonType ToProjectWithdrawnReasonType(string projectWithdrawnReason)
{
return projectWithdrawnReason?.ToLower() switch
{
"educational" => ProjectWithdrawnReasonType.Educational,
"governance" => ProjectWithdrawnReasonType.Governance,
"planning" => ProjectWithdrawnReasonType.Planning,
"procurement / construction" => ProjectWithdrawnReasonType.ProcurementConstruction,
"property" => ProjectWithdrawnReasonType.Property,
"pupil numbers / viability" => ProjectWithdrawnReasonType.PupilNumbersViability,
"trust not content with site option" => ProjectWithdrawnReasonType.TrustNotContentWithSiteOption,
"trust not willing to open in temporary accommodation" => ProjectWithdrawnReasonType.TrustNotWillingToOpenInTemporaryAccommodation,
_ => ProjectWithdrawnReasonType.NotSet
};
}

public static string FromProjectWithdrawnReasonType(ProjectWithdrawnReasonType projectWithdrawnReason)
{
return projectWithdrawnReason switch
{
ProjectWithdrawnReasonType.Educational => "educational",
ProjectWithdrawnReasonType.Governance => "governance",
ProjectWithdrawnReasonType.Planning => "planning",
ProjectWithdrawnReasonType.ProcurementConstruction => "procurement / construction",
ProjectWithdrawnReasonType.Property => "property",
ProjectWithdrawnReasonType.PupilNumbersViability => "pupil numbers / viability",
ProjectWithdrawnReasonType.TrustNotContentWithSiteOption => "trust not content with site option",
ProjectWithdrawnReasonType.TrustNotWillingToOpenInTemporaryAccommodation => "trust not willing to open in temporary accommodation",
_ => ""
};
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using Dfe.ManageFreeSchoolProjects.API.Exceptions;
using Dfe.ManageFreeSchoolProjects.API.Extensions;
using Dfe.ManageFreeSchoolProjects.Data;
using Dfe.ManageFreeSchoolProjects.Data.Migrations;
using Microsoft.EntityFrameworkCore;

namespace Dfe.ManageFreeSchoolProjects.API.UseCases.Project.ProjectStatus;
Expand Down Expand Up @@ -29,19 +30,19 @@ public async Task Execute(string projectId, UpdateProjectStatusRequest request)
throw new NotFoundException($"Project with id {projectId} not found");
}

var updateRequest = new UpdateProjectStatusRequest()
{
ProjectStatus = request.ProjectStatus,
WithdrawnDate = request.WithdrawnDate,
ClosedDate = request.ClosedDate,
CancelledDate = request.CancelledDate
};

dbProject.ProjectStatusProjectStatus = ProjectMapper.FromProjectStatusType(updateRequest.ProjectStatus);
dbProject.ProjectStatusDateClosed = updateRequest.ClosedDate;
dbProject.ProjectStatusDateCancelled = updateRequest.CancelledDate;
dbProject.ProjectStatusDateWithdrawn = updateRequest.WithdrawnDate;
dbProject.ProjectStatusProjectStatus = ProjectMapper.FromProjectStatusType(request.ProjectStatus);
dbProject.ProjectStatusDateClosed = request.ClosedDate;

dbProject.ProjectStatusDateCancelled = request.CancelledDate;
dbProject.ProjectStatusPrimaryReasonForCancellation = ProjectMapper.FromProjectCancelledReasonType(request.ProjectCancelledReason);
dbProject.ProjectStatusProjectCancelledDueToNationalReviewOfPipelineProjects = request.ProjectCancelledDueToNationalReviewOfPipelineProjects;
dbProject.ProjectStatusCommentaryForCancellation = request.CommentaryForCancellation;

dbProject.ProjectStatusDateWithdrawn = request.WithdrawnDate;
dbProject.ProjectStatusPrimaryReasonForWithdrawal = ProjectMapper.FromProjectWithdrawnReasonType(request.ProjectWithdrawnReason);
dbProject.ProjectStatusProjectWithdrawnDueToNationalReviewOfPipelineProjects = request.ProjectWithdrawnDueToNationalReviewOfPipelineProjects;
dbProject.ProjectStatusCommentaryForWithdrawal = request.CommentaryForWithdrawal;

await _context.SaveChangesAsync();
}

Expand Down
Loading

0 comments on commit 8356c6f

Please sign in to comment.