Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion docs/results-enquiry.md
Original file line number Diff line number Diff line change
Expand Up @@ -336,7 +336,11 @@ enquiry up. This method is the only place that queues an enquiry.
How a school sees its own enquiries is designed: AB#298325 added an Issues tab
beside Requests on the Amendment request summary page (`AmendmentRequestsService.GetAmendmentRequestsAsync`,
`GetSubmittedResultsEnquiriesAsync`), listing submitted enquiries with pupil-name search, enriched from
each row's journey blob for CYPMD id and qualification text.
each row's journey blob for CYPMD id, qualification text and the **Grade achieved** value (#712).
Grade achieved is the saved record's grade, never the live results feed: the revised grade for an
incorrect-grade enquiry, the grade entered for a missing qualification, and the grade held for the
reported result for a result-does-not-belong enquiry. A record that cannot supply one shows an
empty cell.

Two consequences, both commented in code:

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using DfE.CheckPerformanceData.Application.CurrentUser;
using DfE.CheckPerformanceData.Application.Journey;
using DfE.CheckPerformanceData.Application.RequestSubmission;
using DfE.CheckPerformanceData.Application.ResultsEnquiry;
using DfE.CheckPerformanceData.Application.WindowManagement;
using DfE.CheckPerformanceData.Domain.Enums;
using Microsoft.Extensions.Logging;
Expand Down Expand Up @@ -59,6 +60,7 @@ public async Task<AmendmentRequestsResult> GetAmendmentRequestsAsync(Guid window
QualificationText = state?.SelectedQualification?.QualificationTitle
?? state?.SelectedResult?.QualificationName
?? "",
GradeText = GradeAchievedFor(state),
ReferenceNumber = enquiry.ReferenceNumber
});
}
Expand All @@ -79,10 +81,12 @@ public async Task<AmendmentRequestsResult> GetAmendmentRequestsAsync(Guid window
.Where(e => e.ExerciseType is not null)
.Select(e => new ExerciseDeadlineDto
{
Exercise = e.ExerciseType.Value,
// The Where above filters out the display-only exercises, so the kind is known
// here; the compiler cannot see across the LINQ chain, hence the suppression.
Exercise = e.ExerciseType!.Value,
EndDate = e.EndDateFor(laestab),
// The clock lives in one place, so "has this closed" is asked, never computed.
IsOpen = checkingExercises.IsOpen(window.Exercises, e.ExerciseType.Value, laestab)
IsOpen = checkingExercises.IsOpen(window.Exercises, e.ExerciseType!.Value, laestab)
})
.ToList(),
Rows = requests.Select(r => new AmendmentRequestDto
Expand All @@ -107,6 +111,28 @@ public async Task<AmendmentRequestsResult> GetAmendmentRequestsAsync(Guid window
};
}

/// <summary>
/// #712: the tab's Grade achieved value. The saved journey record is the only source: the
/// revised grade the school chose, the grade it entered for the missing qualification, or the
/// grade the DfE holds for the reported result. Undecidable degrades to "" — the row is the
/// record of truth and the cell is enrichment. A present value is verbatim; only a missing,
/// null or whitespace-only value shows as an empty cell.
/// </summary>
private static string GradeAchievedFor(RequestState? state)
{
var value = state?.SelectedWhatToChange switch
{
WhatToChange.IncorrectGrade => AnswerText(state, EnquiryQuestionIds.RevisedGrade),
WhatToChange.MissingQualification => AnswerText(state, EnquiryQuestionIds.MissingGrade),
WhatToChange.ResultDoesNotBelong => state?.SelectedResult?.Grade,
_ => null
};
return string.IsNullOrWhiteSpace(value) ? "" : value;
}

private static string? AnswerText(RequestState? state, string questionId) =>
state?.QuestionAnswers.GetValueOrDefault(questionId)?.TextValue;

/// <summary>"Results enquiry - Missing qualification" → "Missing qualification". The suffix IS
/// the user-facing kind label; falling back to the whole string keeps an unexpected format
/// visible rather than blank.</summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,5 +18,10 @@ public sealed class ResultsEnquiryIssueDto
public required string TypeLabel { get; init; }

public required string QualificationText { get; init; }

/// <summary>#712: the row's Grade achieved value — the revised grade, the entered grade or the
/// held grade, per the enquiry kind. "" when it cannot be determined, matching the sibling cells.</summary>
public required string GradeText { get; init; }

public required string ReferenceNumber { get; init; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
namespace DfE.CheckPerformanceData.Application.ResultsEnquiry;

/// <summary>
/// #712: the single owner of the two results-enquiry grade question ids. The ids are a
/// serialization contract between the journey flow files (IncorrectGrade_Post16.json,
/// MissingQualification_Post16.json) and the code that reads the saved answers — the flow tests
/// pin them against the JSON. The Web constants delegate here so one owner holds the contract.
/// </summary>
public static class EnquiryQuestionIds
{
public const string RevisedGrade = "q-revised-grade";
public const string MissingGrade = "q-missing-grade";
}
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,8 @@ private async Task<AmendmentRequestsViewModel> BuildIndexViewModelAsync(Guid win
Submitted = r.Submitted,
CypmdId = r.CypmdId,
TypeLabel = r.TypeLabel,
QualificationText = r.QualificationText
QualificationText = r.QualificationText,
GradeText = r.GradeText
}).ToList(),
ShowResultsEnquiries = result.HasResultsEnquiry
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,9 @@ public sealed class IssueRowViewModel
public required string TypeLabel { get; init; }
public required string QualificationText { get; init; }

/// <summary>#712: the row's Grade achieved value; "" when it cannot be determined.</summary>
public required string GradeText { get; init; }

// Same conversion and format as SubmittedRequestRowViewModel.SubmittedDateText: the stored
// instant is UTC, display is London wall clock, GDS-style written-out date.
public string DateText => LondonTime.ToLondon(Submitted).ToString("d MMMM yyyy");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,12 +42,12 @@ public sealed class JourneyController(
/// AB#296648: the revised-grade question id from <c>IncorrectGrade_Post16.json</c>. Named here
/// because changing the selected result has to clear this one answer specifically.
/// </summary>
internal const string RevisedGradeQuestionId = "q-revised-grade";
internal const string RevisedGradeQuestionId = EnquiryQuestionIds.RevisedGrade;

/// <summary>AB#297848: the missing-qualification flow's syllabus and grade question ids. Named
/// here because choosing a different qualification has to clear these two answers specifically.</summary>
internal const string SyllabusQuestionId = "q-syllabus-code";
internal const string MissingGradeQuestionId = "q-missing-grade";
internal const string MissingGradeQuestionId = EnquiryQuestionIds.MissingGrade;

private const long MaxUploadBytes = 10 * 1024 * 1024; // 10 MB

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,7 @@ public SummaryViewModel BuildSummaryVm(
ShowSyllabusCode = journey.SelectedQualification?.SyllabusCodes.Count > 0,
AwardDate = awardDate,
Ncn = Answer(NcnQuestionId),
GradeAchieved = Answer(MissingGradeQuestionId),
GradeAchieved = Answer(EnquiryQuestionIds.MissingGrade),
AdditionalInformation = Answer(AdditionalInfoQuestionId),
QualificationPageId = qualificationPageId,
DetailsPageId = detailsPageId,
Expand All @@ -181,11 +181,11 @@ public SummaryViewModel BuildSummaryVm(
private const string CohortCountQuestionId = "q-cohort-count";
private const string AdditionalInfoQuestionId = "q-additional-info";

// Question ids from MissingQualification_Post16.json. AB#297848.
// Question ids from MissingQualification_Post16.json. AB#297848. The missing-grade id is
// read directly from EnquiryQuestionIds, its single owner (#712).
private const string SyllabusQuestionId = "q-syllabus-code";
private const string AwardDateQuestionId = "q-award-date";
private const string NcnQuestionId = "q-ncn";
private const string MissingGradeQuestionId = "q-missing-grade";

/// <summary>How a window type is named to a school on the summary.</summary>
private static string KeyStageLabelFor(Domain.Enums.CheckingWindowType? windowType) => windowType switch
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -255,6 +255,7 @@
<th scope="col" class="govuk-table__header">CYPMD id</th>
<th scope="col" class="govuk-table__header">Type</th>
<th scope="col" class="govuk-table__header">Qualification</th>
<th scope="col" class="govuk-table__header">Grade achieved</th>
</tr>
</thead>
<tbody class="govuk-table__body">
Expand All @@ -266,6 +267,7 @@
<td class="govuk-table__cell">@issue.CypmdId</td>
<td class="govuk-table__cell">@issue.TypeLabel</td>
<td class="govuk-table__cell">@issue.QualificationText</td>
<td class="govuk-table__cell">@issue.GradeText</td>
</tr>
}
</tbody>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -350,6 +350,7 @@ public async Task Index_MapsIssueRowsOntoTheViewModel()
CypmdId = "500001",
TypeLabel = "Missing qualification",
QualificationText = "ABRSM level 3",
GradeText = "A*",
ReferenceNumber = "REF-1"
}
],
Expand All @@ -364,6 +365,7 @@ public async Task Index_MapsIssueRowsOntoTheViewModel()
Assert.Equal("500001", row.CypmdId);
Assert.Equal("Missing qualification", row.TypeLabel);
Assert.Equal("ABRSM level 3", row.QualificationText);
Assert.Equal("A*", row.GradeText);
}

// GDS style writes dates out; 01/10/2026 would also collide with the US reading. Matches the
Expand All @@ -374,7 +376,7 @@ public void IssueRowViewModel_FormatsTheDateGdsStyle()
var row = new IssueRowViewModel
{
PupilName = "A", Submitted = new DateTime(2026, 10, 1, 9, 0, 0, DateTimeKind.Utc),
CypmdId = "", TypeLabel = "", QualificationText = ""
CypmdId = "", TypeLabel = "", QualificationText = "", GradeText = ""
};
Assert.Equal("1 October 2026", row.DateText);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,22 @@ public void TheIssuesTableHasTheDesignedColumns()
Assert.Contains(">CYPMD id</th>", source);
Assert.Contains(">Type</th>", source);
Assert.Contains(">Qualification</th>", source);
Assert.Contains(">Grade achieved</th>", source);
}

// #712: the Grade achieved column is part of the Issues table — sliced like the row-actions
// pin, so the header and cell cannot be satisfied by a copy of the markup elsewhere in the
// panel.
[Fact]
public void TheGradeAchievedColumnSitsInsideTheIssuesTable()
{
var issuesPanel = ResultsEnquiriesPanel();
var tableStart = issuesPanel.IndexOf("<table", StringComparison.Ordinal);
Assert.True(tableStart >= 0, "The Issues table markup is missing.");
var tableEnd = issuesPanel.IndexOf("</table>", tableStart, StringComparison.Ordinal) + "</table>".Length;
var issuesTable = issuesPanel[tableStart..tableEnd];
Assert.Contains(">Grade achieved</th>", issuesTable);
Assert.Contains("@issue.GradeText", issuesTable);
}

// AC 4 (separation): the Issues TABLE renders enquiry fields only, with no per-row actions —
Expand Down
Loading
Loading