diff --git a/.gitignore b/.gitignore
index 2066c31cf..35bffa7a2 100644
--- a/.gitignore
+++ b/.gitignore
@@ -514,3 +514,5 @@ opencode.json
specs/
tests/DfE.CheckPerformanceData.E2ETests/Snapshots/failures/
.spec-context.json
+/.ai-codex
+/AGENTS.md
diff --git a/src/DfE.CheckPerformanceData.Application/AmendmentRequests/BulkSubmissionService.cs b/src/DfE.CheckPerformanceData.Application/AmendmentRequests/BulkSubmissionService.cs
index e25c28682..6d6e3af1e 100644
--- a/src/DfE.CheckPerformanceData.Application/AmendmentRequests/BulkSubmissionService.cs
+++ b/src/DfE.CheckPerformanceData.Application/AmendmentRequests/BulkSubmissionService.cs
@@ -102,7 +102,8 @@ await analytics.TrackSafeAsync(new RequestSubmittedEvent
if (submitted.Count > 0)
{
var window = await checkYourPupilDataService.GetCheckingWindowAsync(windowId);
- await requestNotificationService.NotifyBulkSubmissionConfirmedAsync(windowId, window.EndDate, submitted);
+ await requestNotificationService.NotifyBulkSubmissionConfirmedAsync(
+ windowId, window.EndDate, submitted, EmailSubstitutions.From(window));
}
return new BulkSubmissionResult { Submitted = submitted, Skipped = skipped };
diff --git a/src/DfE.CheckPerformanceData.Application/LandingPage/ILandingPageService.cs b/src/DfE.CheckPerformanceData.Application/LandingPage/ILandingPageService.cs
index 032cecaa2..8555b5d02 100644
--- a/src/DfE.CheckPerformanceData.Application/LandingPage/ILandingPageService.cs
+++ b/src/DfE.CheckPerformanceData.Application/LandingPage/ILandingPageService.cs
@@ -29,5 +29,6 @@ public sealed class CheckingWindowDto
public required CheckingWindowType CheckingWindowType { get; init; }
public bool HasPupilData { get; init; }
public required DateTime StartDate { get; init; }
+ public string TurnaroundCommitment { get; init; } = string.Empty;
}
diff --git a/src/DfE.CheckPerformanceData.Application/Notify/EmailNotification.cs b/src/DfE.CheckPerformanceData.Application/Notify/EmailNotification.cs
index 728f6eba5..d7c4c4fdf 100644
--- a/src/DfE.CheckPerformanceData.Application/Notify/EmailNotification.cs
+++ b/src/DfE.CheckPerformanceData.Application/Notify/EmailNotification.cs
@@ -38,4 +38,16 @@ public sealed record EmailNotification
/// single-reference notifications (which use ).
///
public IReadOnlyList? ReferenceNumbers { get; init; }
+
+ /// Checking-exercise name shown in the email body (template ((ce name))).
+ public string CeName { get; init; } = string.Empty;
+
+ /// "Student" for Post 16 windows, otherwise "Pupil" (template ((learner noun))).
+ public string LearnerNoun { get; init; } = string.Empty;
+
+ ///
+ /// Per-window turnaround commitment phrase (template ((turnaround commitment))).
+ /// Empty = not configured; the personalisation key is omitted (FR-006).
+ ///
+ public string TurnaroundCommitment { get; init; } = string.Empty;
}
diff --git a/src/DfE.CheckPerformanceData.Application/Notify/EmailSubstitutions.cs b/src/DfE.CheckPerformanceData.Application/Notify/EmailSubstitutions.cs
new file mode 100644
index 000000000..94e020e44
--- /dev/null
+++ b/src/DfE.CheckPerformanceData.Application/Notify/EmailSubstitutions.cs
@@ -0,0 +1,42 @@
+using System.ComponentModel.DataAnnotations;
+using System.Reflection;
+using DfE.CheckPerformanceData.Application.LandingPage;
+using DfE.CheckPerformanceData.Domain.Enums;
+
+namespace DfE.CheckPerformanceData.Application.Notify;
+
+///
+/// Immutable carrier for the three checking-exercise-specific email substitution
+/// values (ce name, learner noun, turnaround commitment).
+/// Built by callers from the checking window they already hold, then passed through
+/// → →
+/// . Not persisted.
+///
+public sealed record EmailSubstitutions(string CeName, string LearnerNoun, string TurnaroundCommitment)
+{
+ ///
+ /// Derives the three substitution values from a checking window.
+ /// CeName: window.Title, falling back to the [Display(Name)] of CheckingWindowType
+ /// when Title is null/whitespace.
+ /// LearnerNoun: "Student" when KeyStage is Post16, else "Pupil".
+ /// TurnaroundCommitment: window.TurnaroundCommitment (may be empty; empty means the
+ /// personalisation key is omitted, per FR-006).
+ ///
+ public static EmailSubstitutions From(CheckingWindowDto window)
+ {
+ var ceName = string.IsNullOrWhiteSpace(window.Title)
+ ? DisplayNameOf(window.CheckingWindowType)
+ : window.Title;
+
+ var learnerNoun = window.KeyStage == KeyStages.Post16 ? "Student" : "Pupil";
+
+ return new EmailSubstitutions(ceName, learnerNoun, window.TurnaroundCommitment);
+ }
+
+ private static string DisplayNameOf(CheckingWindowType type)
+ {
+ var attribute = type.GetType().GetField(type.ToString())
+ ?.GetCustomAttribute();
+ return attribute?.Name ?? type.ToString();
+ }
+}
diff --git a/src/DfE.CheckPerformanceData.Application/Notify/INotifyService.cs b/src/DfE.CheckPerformanceData.Application/Notify/INotifyService.cs
index 2a30f3b26..e11f30e70 100644
--- a/src/DfE.CheckPerformanceData.Application/Notify/INotifyService.cs
+++ b/src/DfE.CheckPerformanceData.Application/Notify/INotifyService.cs
@@ -29,6 +29,8 @@ public interface INotifyService
/// Display-friendly deadline text.
/// Deduplicated recipient email addresses.
/// Which notification template to use.
+ /// Checking-exercise-specific values (ce name, learner noun,
+ /// turnaround commitment) gated per notification type.
/// Optional URL (e.g. "submit others" or withdrawal link).
///
/// For a consolidated bulk submission email: every reference in the batch, listed in the
@@ -40,6 +42,7 @@ Task SendNotificationsAsync(
string deadline,
IReadOnlyCollection recipientEmails,
NotificationType notificationType,
+ EmailSubstitutions substitutions,
string? url = null,
IReadOnlyCollection? referenceNumbers = null);
diff --git a/src/DfE.CheckPerformanceData.Application/Notify/IRequestNotificationService.cs b/src/DfE.CheckPerformanceData.Application/Notify/IRequestNotificationService.cs
index c97580157..ae91f7da8 100644
--- a/src/DfE.CheckPerformanceData.Application/Notify/IRequestNotificationService.cs
+++ b/src/DfE.CheckPerformanceData.Application/Notify/IRequestNotificationService.cs
@@ -6,9 +6,9 @@ namespace DfE.CheckPerformanceData.Application.Notify;
public interface IRequestNotificationService
{
- Task NotifySubmissionConfirmedAsync(Guid windowId, DateTime deadlineDate, string referenceNumber);
- Task NotifyBulkSubmissionConfirmedAsync(Guid windowId, DateTime deadlineDate, IReadOnlyList referenceNumbers);
- Task NotifyDataCheckConfirmedAsync(DateTime deadlineDate, string referenceNumber);
+ Task NotifySubmissionConfirmedAsync(Guid windowId, DateTime deadlineDate, string referenceNumber, EmailSubstitutions substitutions);
+ Task NotifyBulkSubmissionConfirmedAsync(Guid windowId, DateTime deadlineDate, IReadOnlyList referenceNumbers, EmailSubstitutions substitutions);
+ Task NotifyDataCheckConfirmedAsync(DateTime deadlineDate, string referenceNumber, EmailSubstitutions substitutions);
///
/// Confirms a submitted 16-19 results enquiry to the person who submitted it (AB#296648).
@@ -19,7 +19,7 @@ public interface IRequestNotificationService
///
Task NotifyResultsEnquirySubmittedAsync(string referenceNumber);
- Task NotifyAmendmentWithdrawnAsync(string referenceNumber, DateTime deadlineDate);
+ Task NotifyAmendmentWithdrawnAsync(string referenceNumber, DateTime deadlineDate, EmailSubstitutions substitutions);
- Task NotifyDataCheckWithdrawnAsync(string referenceNumber, DateTime deadlineDate);
+ Task NotifyDataCheckWithdrawnAsync(string referenceNumber, DateTime deadlineDate, EmailSubstitutions substitutions);
}
diff --git a/src/DfE.CheckPerformanceData.Application/RequestSubmission/IRequestService.cs b/src/DfE.CheckPerformanceData.Application/RequestSubmission/IRequestService.cs
index 5701f1c1d..722192389 100644
--- a/src/DfE.CheckPerformanceData.Application/RequestSubmission/IRequestService.cs
+++ b/src/DfE.CheckPerformanceData.Application/RequestSubmission/IRequestService.cs
@@ -1,4 +1,5 @@
using DfE.CheckPerformanceData.Application.Journey;
+using DfE.CheckPerformanceData.Application.Notify;
using DfE.CheckPerformanceData.Domain.Enums;
namespace DfE.CheckPerformanceData.Application.RequestSubmission;
@@ -32,7 +33,7 @@ public interface IRequestService
Task ConfirmRequestAsync(Guid windowId, RequestState journey);
Task SaveDraftAsync(Guid windowId, RequestState journey, RequestStatus status);
Task ResumeDraftAsync(Guid windowId, string referenceNumber);
- Task ConfirmDataCorrectAsync(Guid windowId, string referenceNumber, DateTime endDate);
+ Task ConfirmDataCorrectAsync(Guid windowId, string referenceNumber, DateTime endDate, EmailSubstitutions substitutions);
///
/// Deletes a request, scoped to the current user's organisation. Drafts
diff --git a/src/DfE.CheckPerformanceData.Application/RequestSubmission/RequestService.cs b/src/DfE.CheckPerformanceData.Application/RequestSubmission/RequestService.cs
index 4a678dff1..740e84135 100644
--- a/src/DfE.CheckPerformanceData.Application/RequestSubmission/RequestService.cs
+++ b/src/DfE.CheckPerformanceData.Application/RequestSubmission/RequestService.cs
@@ -145,10 +145,12 @@ public async Task ConfirmRequestAsync(Guid windowId, RequestState journey)
{
await SubmitRequestAsync(windowId, journey);
await requestNotificationService.NotifySubmissionConfirmedAsync(
- windowId, journey.CheckingWindow!.EndDate, journey.ReferenceNumber ?? string.Empty);
+ windowId, journey.CheckingWindow!.EndDate, journey.ReferenceNumber ?? string.Empty,
+ EmailSubstitutions.From(journey.CheckingWindow));
}
- public async Task ConfirmDataCorrectAsync(Guid windowId, string referenceNumber, DateTime endDate)
+ public async Task ConfirmDataCorrectAsync(
+ Guid windowId, string referenceNumber, DateTime endDate, EmailSubstitutions substitutions)
{
await requestRepository.UpsertAsync(new ChangeRequestData
{
@@ -167,7 +169,8 @@ await requestRepository.UpsertAsync(new ChangeRequestData
RequestTypeDescription = "Confirm Pupil Data Declaration"
});
- await requestNotificationService.NotifyDataCheckConfirmedAsync(endDate, referenceNumber);
+ await requestNotificationService.NotifyDataCheckConfirmedAsync(
+ endDate, referenceNumber, substitutions);
}
public async Task SaveDraftAsync(Guid windowId, RequestState journey, RequestStatus status)
@@ -216,11 +219,13 @@ public async Task DeleteAsync(Guid windowId, string refer
if (row?.RequestType == RequestType.Amendment)
{
- await requestNotificationService.NotifyAmendmentWithdrawnAsync(referenceNumber, deadline);
+ await requestNotificationService.NotifyAmendmentWithdrawnAsync(
+ referenceNumber, deadline, EmailSubstitutions.From(window));
}
else if (row?.RequestType == RequestType.ConfirmCorrect)
{
- await requestNotificationService.NotifyDataCheckWithdrawnAsync(referenceNumber, deadline);
+ await requestNotificationService.NotifyDataCheckWithdrawnAsync(
+ referenceNumber, deadline, EmailSubstitutions.From(window));
}
else
{
diff --git a/src/DfE.CheckPerformanceData.Application/WindowManagement/IWindowService.cs b/src/DfE.CheckPerformanceData.Application/WindowManagement/IWindowService.cs
index eea084e34..436978903 100644
--- a/src/DfE.CheckPerformanceData.Application/WindowManagement/IWindowService.cs
+++ b/src/DfE.CheckPerformanceData.Application/WindowManagement/IWindowService.cs
@@ -31,6 +31,7 @@ public sealed class CheckingWindowDto
public bool Validated { get; set; }
public DateTime? ValidatedAt { get; set; }
public bool IsOpen { get; set; }
+ public string TurnaroundCommitment { get; set; } = string.Empty;
///
/// The CSV + schema pairs ingested for this window, in sort order. A Post16 window has two
diff --git a/src/DfE.CheckPerformanceData.Infrastructure/Notify/DevConsoleNotifyService.cs b/src/DfE.CheckPerformanceData.Infrastructure/Notify/DevConsoleNotifyService.cs
index 9ea436035..a58d7aa4c 100644
--- a/src/DfE.CheckPerformanceData.Infrastructure/Notify/DevConsoleNotifyService.cs
+++ b/src/DfE.CheckPerformanceData.Infrastructure/Notify/DevConsoleNotifyService.cs
@@ -19,6 +19,7 @@ public Task SendNotificationsAsync(
string deadline,
IReadOnlyCollection recipientEmails,
NotificationType notificationType,
+ EmailSubstitutions substitutions,
string? url = null,
IReadOnlyCollection? referenceNumbers = null)
{
@@ -27,9 +28,10 @@ public Task SendNotificationsAsync(
var refs = referenceNumbers is { Count: > 0 } ? string.Join(", ", referenceNumbers) : referenceNumber;
_logger.LogInformation(
- "[Notify:Fake] Sending {NotificationType} notification\n Reference(s): {References}\n Deadline: {Deadline}\n Recipients: {RecipientCount} — [{RecipientEmails}]{Url}",
+ "[Notify:Fake] Sending {NotificationType} notification\n Reference(s): {References}\n Deadline: {Deadline}\n Recipients: {RecipientCount} — [{RecipientEmails}]{Url}\n CeName: {CeName}\n LearnerNoun: {LearnerNoun}\n TurnaroundCommitment: {TurnaroundCommitment}",
notificationType, refs, deadline, recipientCount, recipientList,
- string.IsNullOrEmpty(url) ? "" : $"\n URL: {url}");
+ string.IsNullOrEmpty(url) ? "" : $"\n URL: {url}",
+ substitutions.CeName, substitutions.LearnerNoun, substitutions.TurnaroundCommitment);
return Task.CompletedTask;
}
diff --git a/src/DfE.CheckPerformanceData.Infrastructure/Notify/NotificationSender.cs b/src/DfE.CheckPerformanceData.Infrastructure/Notify/NotificationSender.cs
index ad32941d5..f2b8efd7e 100644
--- a/src/DfE.CheckPerformanceData.Infrastructure/Notify/NotificationSender.cs
+++ b/src/DfE.CheckPerformanceData.Infrastructure/Notify/NotificationSender.cs
@@ -36,6 +36,10 @@ await notifyService.SendNotificationsAsync(
notification.Deadline,
recipients,
notification.Type,
+ new EmailSubstitutions(
+ notification.CeName,
+ notification.LearnerNoun,
+ notification.TurnaroundCommitment),
notification.LinkUrl,
notification.ReferenceNumbers);
}
diff --git a/src/DfE.CheckPerformanceData.Infrastructure/Notify/NotifyService.cs b/src/DfE.CheckPerformanceData.Infrastructure/Notify/NotifyService.cs
index 0d71475d7..c663b12ec 100644
--- a/src/DfE.CheckPerformanceData.Infrastructure/Notify/NotifyService.cs
+++ b/src/DfE.CheckPerformanceData.Infrastructure/Notify/NotifyService.cs
@@ -33,6 +33,7 @@ public async Task SendNotificationsAsync(
string deadline,
IReadOnlyCollection recipientEmails,
NotificationType notificationType,
+ EmailSubstitutions substitutions,
string? url = null,
IReadOnlyCollection? referenceNumbers = null)
{
@@ -68,7 +69,7 @@ public async Task SendNotificationsAsync(
{
try
{
- await SendEmailAsync(email, referenceNumber, deadline, templateId, url, referenceNumbers);
+ await SendEmailAsync(email, referenceNumber, deadline, templateId, url, referenceNumbers, notificationType, substitutions);
}
catch (Exception ex)
{
@@ -114,7 +115,9 @@ private async Task SendEmailAsync(
string deadline,
string templateId,
string? url,
- IReadOnlyCollection? referenceNumbers = null)
+ IReadOnlyCollection? referenceNumbers,
+ NotificationType notificationType,
+ EmailSubstitutions substitutions)
{
var personalisation = new Dictionary
{
@@ -134,6 +137,25 @@ private async Task SendEmailAsync(
personalisation["references"] = string.Join("\n", referenceNumbers);
}
+ // GOV.UK Notify raises on BOTH missing and extra personalisation keys, so each key is
+ // gated per notification type: the keys added here must exactly match the templates.
+ if (notificationType is not NotificationType.ResultsEnquirySubmitted)
+ {
+ personalisation["ce name"] = substitutions.CeName;
+ }
+
+ if (notificationType is NotificationType.DataCheckConfirmed
+ or NotificationType.DataCheckWithdrawn
+ or NotificationType.AmendmentWithdrawn)
+ {
+ personalisation["learner noun"] = substitutions.LearnerNoun;
+ }
+
+ if (!string.IsNullOrEmpty(substitutions.TurnaroundCommitment))
+ {
+ personalisation["turnaround commitment"] = substitutions.TurnaroundCommitment;
+ }
+
if (_resiliencePipeline is not null)
{
await _resiliencePipeline.ExecuteAsync(
diff --git a/src/DfE.CheckPerformanceData.Infrastructure/Notify/RequestNotificationService.cs b/src/DfE.CheckPerformanceData.Infrastructure/Notify/RequestNotificationService.cs
index ad5c45262..43bbcf095 100644
--- a/src/DfE.CheckPerformanceData.Infrastructure/Notify/RequestNotificationService.cs
+++ b/src/DfE.CheckPerformanceData.Infrastructure/Notify/RequestNotificationService.cs
@@ -18,7 +18,8 @@ public sealed class RequestNotificationService(
INotificationDispatcher dispatcher,
IOptions notifySettings) : IRequestNotificationService
{
- public async Task NotifySubmissionConfirmedAsync(Guid windowId, DateTime deadlineDate, string referenceNumber)
+ public async Task NotifySubmissionConfirmedAsync(
+ Guid windowId, DateTime deadlineDate, string referenceNumber, EmailSubstitutions substitutions)
{
var linkUrl = emailLinkGenerator.GenerateLink(
"WhatToChange", "Index", new { windowId }, "SubmissionNotification");
@@ -31,7 +32,10 @@ await dispatcher.EnqueueAsync(new EmailNotification
LinkUrl = linkUrl,
Ukprn = currentUserService.Ukprn,
OriginatorEmail = currentUserService.Email,
- IncludeOrganisationUsers = true
+ IncludeOrganisationUsers = true,
+ CeName = substitutions.CeName,
+ LearnerNoun = substitutions.LearnerNoun,
+ TurnaroundCommitment = substitutions.TurnaroundCommitment
});
}
@@ -53,7 +57,7 @@ await dispatcher.EnqueueAsync(new EmailNotification
}
public async Task NotifyBulkSubmissionConfirmedAsync(
- Guid windowId, DateTime deadlineDate, IReadOnlyList referenceNumbers)
+ Guid windowId, DateTime deadlineDate, IReadOnlyList referenceNumbers, EmailSubstitutions substitutions)
{
if (referenceNumbers.Count == 0) return;
@@ -75,7 +79,10 @@ await dispatcher.EnqueueAsync(new EmailNotification
LinkUrl = linkUrl,
Ukprn = currentUserService.Ukprn,
OriginatorEmail = currentUserService.Email,
- IncludeOrganisationUsers = true
+ IncludeOrganisationUsers = true,
+ CeName = substitutions.CeName,
+ LearnerNoun = substitutions.LearnerNoun,
+ TurnaroundCommitment = substitutions.TurnaroundCommitment
});
}
return;
@@ -91,11 +98,15 @@ await dispatcher.EnqueueAsync(new EmailNotification
LinkUrl = linkUrl,
Ukprn = currentUserService.Ukprn,
OriginatorEmail = currentUserService.Email,
- IncludeOrganisationUsers = true
+ IncludeOrganisationUsers = true,
+ CeName = substitutions.CeName,
+ LearnerNoun = substitutions.LearnerNoun,
+ TurnaroundCommitment = substitutions.TurnaroundCommitment
});
}
- public async Task NotifyDataCheckConfirmedAsync(DateTime deadlineDate, string referenceNumber)
+ public async Task NotifyDataCheckConfirmedAsync(
+ DateTime deadlineDate, string referenceNumber, EmailSubstitutions substitutions)
{
await dispatcher.EnqueueAsync(new EmailNotification
{
@@ -104,11 +115,15 @@ await dispatcher.EnqueueAsync(new EmailNotification
Deadline = FormatDeadline(deadlineDate),
Ukprn = currentUserService.Ukprn,
OriginatorEmail = currentUserService.Email,
- IncludeOrganisationUsers = true
+ IncludeOrganisationUsers = true,
+ CeName = substitutions.CeName,
+ LearnerNoun = substitutions.LearnerNoun,
+ TurnaroundCommitment = substitutions.TurnaroundCommitment
});
}
- public async Task NotifyAmendmentWithdrawnAsync(string referenceNumber, DateTime deadlineDate)
+ public async Task NotifyAmendmentWithdrawnAsync(
+ string referenceNumber, DateTime deadlineDate, EmailSubstitutions substitutions)
{
await dispatcher.EnqueueAsync(new EmailNotification
{
@@ -117,11 +132,15 @@ await dispatcher.EnqueueAsync(new EmailNotification
Deadline = FormatDeadline(deadlineDate),
Ukprn = currentUserService.Ukprn,
OriginatorEmail = currentUserService.Email,
- IncludeOrganisationUsers = false
+ IncludeOrganisationUsers = false,
+ CeName = substitutions.CeName,
+ LearnerNoun = substitutions.LearnerNoun,
+ TurnaroundCommitment = substitutions.TurnaroundCommitment
});
}
- public async Task NotifyDataCheckWithdrawnAsync(string referenceNumber, DateTime deadlineDate)
+ public async Task NotifyDataCheckWithdrawnAsync(
+ string referenceNumber, DateTime deadlineDate, EmailSubstitutions substitutions)
{
await dispatcher.EnqueueAsync(new EmailNotification
{
@@ -130,7 +149,10 @@ await dispatcher.EnqueueAsync(new EmailNotification
Deadline = FormatDeadline(deadlineDate),
Ukprn = currentUserService.Ukprn,
OriginatorEmail = currentUserService.Email,
- IncludeOrganisationUsers = true
+ IncludeOrganisationUsers = true,
+ CeName = substitutions.CeName,
+ LearnerNoun = substitutions.LearnerNoun,
+ TurnaroundCommitment = substitutions.TurnaroundCommitment
});
}
diff --git a/src/DfE.CheckPerformanceData.Persistence/Entities/CheckingWindow.cs b/src/DfE.CheckPerformanceData.Persistence/Entities/CheckingWindow.cs
index 6c4b875bd..69d30bf41 100644
--- a/src/DfE.CheckPerformanceData.Persistence/Entities/CheckingWindow.cs
+++ b/src/DfE.CheckPerformanceData.Persistence/Entities/CheckingWindow.cs
@@ -12,6 +12,7 @@ public sealed class CheckingWindow
public KeyStages KeyStage { get; init; }
public CheckingWindowType CheckingWindowType { get; init; }
public string Title { get; init; } = string.Empty;
+ public string TurnaroundCommitment { get; init; } = string.Empty;
public bool Published { get; init; } = false;
public string IngressFile { get; init; } = string.Empty;
public string SchemaFile { get; init; } = string.Empty;
@@ -62,6 +63,9 @@ public void Configure(EntityTypeBuilder builder)
builder.Property(x => x.Title)
.IsRequired()
.HasMaxLength(200);
+
+ builder.Property(x => x.TurnaroundCommitment)
+ .HasMaxLength(200);
builder.Property(x => x.IngressFile)
.HasMaxLength(255);
diff --git a/src/DfE.CheckPerformanceData.Persistence/Migrations/20260818095733_AddCheckingWindowTurnaroundCommitment.Designer.cs b/src/DfE.CheckPerformanceData.Persistence/Migrations/20260818095733_AddCheckingWindowTurnaroundCommitment.Designer.cs
new file mode 100644
index 000000000..3bfc147f8
--- /dev/null
+++ b/src/DfE.CheckPerformanceData.Persistence/Migrations/20260818095733_AddCheckingWindowTurnaroundCommitment.Designer.cs
@@ -0,0 +1,1351 @@
+//
+using System;
+using DfE.CheckPerformanceData.Persistence.Contexts;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
+using NpgsqlTypes;
+
+#nullable disable
+
+namespace DfE.CheckPerformanceData.Persistence.Migrations
+{
+ [DbContext(typeof(PortalDbContext))]
+ [Migration("20260818095733_AddCheckingWindowTurnaroundCommitment")]
+ partial class AddCheckingWindowTurnaroundCommitment
+ {
+ ///
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder
+ .HasAnnotation("ProductVersion", "10.0.9")
+ .HasAnnotation("Relational:MaxIdentifierLength", 63);
+
+ NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
+
+ modelBuilder.Entity("DfE.CheckPerformance.Persistence.Entities.AuditEntry", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("bigint");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("Action")
+ .IsRequired()
+ .HasMaxLength(50)
+ .HasColumnType("character varying(50)");
+
+ b.Property("ChangedColumns")
+ .HasColumnType("text");
+
+ b.Property("EntityId")
+ .IsRequired()
+ .HasMaxLength(256)
+ .HasColumnType("character varying(256)");
+
+ b.Property("EntityType")
+ .IsRequired()
+ .HasMaxLength(256)
+ .HasColumnType("character varying(256)");
+
+ b.Property("NewValues")
+ .HasColumnType("text");
+
+ b.Property("OldValues")
+ .HasColumnType("text");
+
+ b.Property("Timestamp")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("UserId")
+ .HasColumnType("text");
+
+ b.HasKey("Id");
+
+ b.HasIndex("EntityType");
+
+ b.HasIndex("Timestamp");
+
+ b.ToTable("AuditEntries");
+ });
+
+ modelBuilder.Entity("DfE.CheckPerformance.Persistence.Entities.QueueMetricEvent", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("bigint")
+ .HasColumnName("id");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("DecisionStatus")
+ .HasMaxLength(50)
+ .HasColumnType("character varying(50)")
+ .HasColumnName("decision_status");
+
+ b.Property("LatencyMs")
+ .HasColumnType("double precision")
+ .HasColumnName("latency_ms");
+
+ b.Property("MessageId")
+ .HasColumnType("uuid")
+ .HasColumnName("message_id");
+
+ b.Property("QueueName")
+ .IsRequired()
+ .HasMaxLength(100)
+ .HasColumnType("character varying(100)")
+ .HasColumnName("queue_name");
+
+ b.Property("RecordedAtUtc")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("recorded_at_utc");
+
+ b.Property("ReferenceNumber")
+ .IsRequired()
+ .HasMaxLength(100)
+ .HasColumnType("character varying(100)")
+ .HasColumnName("reference_number");
+
+ b.Property("RulesVersion")
+ .HasMaxLength(100)
+ .HasColumnType("character varying(100)")
+ .HasColumnName("rules_version");
+
+ b.Property("Stage")
+ .IsRequired()
+ .HasMaxLength(50)
+ .HasColumnType("character varying(50)")
+ .HasColumnName("stage");
+
+ b.HasKey("Id");
+
+ b.HasIndex("RecordedAtUtc")
+ .HasDatabaseName("ix_queue_metrics_events_recorded_at");
+
+ b.HasIndex("QueueName", "RecordedAtUtc")
+ .HasDatabaseName("ix_queue_metrics_events_queue_recorded");
+
+ b.HasIndex("ReferenceNumber", "RecordedAtUtc")
+ .HasDatabaseName("ix_queue_metrics_events_reference");
+
+ b.ToTable("queue_metrics_events", (string)null);
+ });
+
+ modelBuilder.Entity("DfE.CheckPerformance.Persistence.Entities.SearchEvent", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("bigint")
+ .HasColumnName("id");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("IsSeeded")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("boolean")
+ .HasDefaultValue(false)
+ .HasColumnName("is_seeded");
+
+ b.Property("JobId")
+ .HasColumnType("text")
+ .HasColumnName("job_id");
+
+ b.Property("LatencyMs")
+ .HasColumnType("integer")
+ .HasColumnName("latency_ms");
+
+ b.Property("OccurredAtUtc")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("occurred_at_utc");
+
+ b.Property("QueryNormalised")
+ .HasColumnType("text")
+ .HasColumnName("query_normalised");
+
+ b.Property("QueryRaw")
+ .HasColumnType("text")
+ .HasColumnName("query_raw");
+
+ b.Property("ResultsBlocks")
+ .HasColumnType("integer")
+ .HasColumnName("results_blocks");
+
+ b.Property("ResultsPages")
+ .HasColumnType("integer")
+ .HasColumnName("results_pages");
+
+ b.Property("ResultsTotal")
+ .ValueGeneratedOnAddOrUpdate()
+ .HasColumnType("integer")
+ .HasColumnName("results_total")
+ .HasComputedColumnSql("results_pages + results_blocks", true);
+
+ b.Property("Scope")
+ .HasColumnType("text")
+ .HasColumnName("scope");
+
+ b.Property("SessionId")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasColumnName("session_id");
+
+ b.Property("ZeroResults")
+ .ValueGeneratedOnAddOrUpdate()
+ .HasColumnType("boolean")
+ .HasColumnName("zero_results")
+ .HasComputedColumnSql("(results_pages + results_blocks) = 0", true);
+
+ b.HasKey("Id");
+
+ b.HasIndex("JobId")
+ .HasDatabaseName("ix_search_events_job_id")
+ .HasFilter("job_id IS NOT NULL");
+
+ b.HasIndex("OccurredAtUtc")
+ .HasDatabaseName("ix_search_events_occurred_at");
+
+ b.HasIndex("QueryNormalised")
+ .HasDatabaseName("ix_search_events_query_normalised");
+
+ b.HasIndex("SessionId")
+ .HasDatabaseName("ix_search_events_session_id");
+
+ b.HasIndex("OccurredAtUtc", "QueryNormalised")
+ .HasDatabaseName("ix_search_events_occurred_at_query_normalised")
+ .HasFilter("query_normalised IS NOT NULL");
+
+ b.HasIndex("OccurredAtUtc", "SessionId")
+ .HasDatabaseName("ix_search_events_occurred_at_session_id");
+
+ b.HasIndex("ZeroResults", "OccurredAtUtc")
+ .HasDatabaseName("ix_search_events_zero_results_occurred_at")
+ .HasFilter("zero_results = true");
+
+ b.ToTable("search_events", (string)null);
+ });
+
+ modelBuilder.Entity("DfE.CheckPerformance.Persistence.Entities.SearchEventResult", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("bigint")
+ .HasColumnName("id");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("IsSeeded")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("boolean")
+ .HasDefaultValue(false)
+ .HasColumnName("is_seeded");
+
+ b.Property("JobId")
+ .HasColumnType("text")
+ .HasColumnName("job_id");
+
+ b.Property("Position")
+ .HasColumnType("integer")
+ .HasColumnName("position");
+
+ b.Property("Rank")
+ .HasColumnType("real")
+ .HasColumnName("rank");
+
+ b.Property("ResultKey")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasColumnName("result_key");
+
+ b.Property("ResultKind")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasColumnName("result_kind");
+
+ b.Property("SearchEventId")
+ .HasColumnType("bigint")
+ .HasColumnName("search_event_id");
+
+ b.HasKey("Id");
+
+ b.HasIndex("JobId")
+ .HasDatabaseName("ix_search_event_results_job_id")
+ .HasFilter("job_id IS NOT NULL");
+
+ b.HasIndex("SearchEventId")
+ .HasDatabaseName("ix_search_event_results_search_event_id");
+
+ b.ToTable("search_event_results", (string)null);
+ });
+
+ modelBuilder.Entity("DfE.CheckPerformance.Persistence.Entities.SearchMessage", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("bigint")
+ .HasColumnName("id");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("Email")
+ .HasColumnType("text")
+ .HasColumnName("email");
+
+ b.Property("IsRead")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("boolean")
+ .HasDefaultValue(false)
+ .HasColumnName("is_read");
+
+ b.Property("IsSeeded")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("boolean")
+ .HasDefaultValue(false)
+ .HasColumnName("is_seeded");
+
+ b.Property("JobId")
+ .HasColumnType("text")
+ .HasColumnName("job_id");
+
+ b.Property("ReadAtUtc")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("read_at_utc");
+
+ b.Property("ReadByAdminSub")
+ .HasColumnType("text")
+ .HasColumnName("read_by_admin_sub");
+
+ b.Property("SessionId")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasColumnName("session_id");
+
+ b.Property("SubmittedAtUtc")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("submitted_at_utc");
+
+ b.Property("WhatGot")
+ .HasColumnType("text")
+ .HasColumnName("what_got");
+
+ b.Property("WhatLookingFor")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasColumnName("what_looking_for");
+
+ b.HasKey("Id");
+
+ b.HasIndex("IsRead")
+ .HasDatabaseName("ix_search_messages_is_read");
+
+ b.HasIndex("JobId")
+ .HasDatabaseName("ix_search_messages_job_id")
+ .HasFilter("job_id IS NOT NULL");
+
+ b.HasIndex("SessionId")
+ .HasDatabaseName("ix_search_messages_session_id");
+
+ b.HasIndex("SubmittedAtUtc")
+ .HasDatabaseName("ix_search_messages_submitted_at");
+
+ b.ToTable("search_messages", (string)null);
+ });
+
+ modelBuilder.Entity("DfE.CheckPerformance.Persistence.Entities.ShareToken", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property("CreatedAtUtc")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at_utc");
+
+ b.Property("CreatedBy")
+ .IsRequired()
+ .HasMaxLength(200)
+ .HasColumnType("character varying(200)")
+ .HasColumnName("created_by");
+
+ b.Property("Label")
+ .IsRequired()
+ .HasMaxLength(200)
+ .HasColumnType("character varying(200)")
+ .HasColumnName("label");
+
+ b.Property("RevokedAtUtc")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("revoked_at_utc");
+
+ b.Property("Surface")
+ .IsRequired()
+ .HasMaxLength(20)
+ .HasColumnType("character varying(20)")
+ .HasColumnName("surface");
+
+ b.Property("TokenHash")
+ .IsRequired()
+ .HasMaxLength(128)
+ .HasColumnType("character varying(128)")
+ .HasColumnName("token_hash");
+
+ b.HasKey("Id");
+
+ b.HasIndex("TokenHash")
+ .HasDatabaseName("ix_share_tokens_token_hash");
+
+ b.ToTable("share_tokens", (string)null);
+ });
+
+ modelBuilder.Entity("DfE.CheckPerformanceData.Persistence.Entities.AdminSectionAccess", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("CreatedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("CreatedBy")
+ .HasColumnType("text");
+
+ b.Property("RoleName")
+ .IsRequired()
+ .HasMaxLength(128)
+ .HasColumnType("character varying(128)");
+
+ b.Property("SectionKey")
+ .IsRequired()
+ .HasMaxLength(128)
+ .HasColumnType("character varying(128)");
+
+ b.HasKey("Id");
+
+ b.HasIndex("RoleName", "SectionKey")
+ .IsUnique();
+
+ b.ToTable("AdminSectionAccesses");
+ });
+
+ modelBuilder.Entity("DfE.CheckPerformanceData.Persistence.Entities.AppLog", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("bigint");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("Category")
+ .IsRequired()
+ .HasMaxLength(256)
+ .HasColumnType("character varying(256)");
+
+ b.Property("CorrelationId")
+ .HasMaxLength(128)
+ .HasColumnType("character varying(128)");
+
+ b.Property("EventId")
+ .HasColumnType("integer");
+
+ b.Property("Exception")
+ .HasColumnType("text");
+
+ b.Property("Level")
+ .IsRequired()
+ .HasMaxLength(32)
+ .HasColumnType("character varying(32)");
+
+ b.Property("Message")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.Property("RequestPath")
+ .HasMaxLength(1024)
+ .HasColumnType("character varying(1024)");
+
+ b.Property("StateJson")
+ .HasColumnType("jsonb");
+
+ b.Property("Timestamp")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("UserId")
+ .HasMaxLength(256)
+ .HasColumnType("character varying(256)");
+
+ b.HasKey("Id");
+
+ b.HasIndex("Category");
+
+ b.HasIndex("Level");
+
+ b.HasIndex("Timestamp")
+ .IsDescending();
+
+ b.ToTable("AppLogs");
+ });
+
+ modelBuilder.Entity("DfE.CheckPerformanceData.Persistence.Entities.ChangeRequest", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid");
+
+ b.Property("AmendmentType")
+ .HasMaxLength(20)
+ .HasColumnType("character varying(20)");
+
+ b.Property("CrmId")
+ .HasMaxLength(100)
+ .HasColumnType("character varying(100)");
+
+ b.Property("DecidedAtUtc")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("MatchedRuleId")
+ .HasMaxLength(100)
+ .HasColumnType("character varying(100)");
+
+ b.Property("OrganisationUrn")
+ .HasColumnType("bigint");
+
+ b.Property("Outcome")
+ .HasMaxLength(20)
+ .HasColumnType("character varying(20)");
+
+ b.Property("OutcomeKey")
+ .HasMaxLength(100)
+ .HasColumnType("character varying(100)");
+
+ b.Property("PupilFirstname")
+ .HasMaxLength(100)
+ .HasColumnType("character varying(100)");
+
+ b.Property("PupilId")
+ .HasColumnType("uuid");
+
+ b.Property("PupilSurname")
+ .HasMaxLength(100)
+ .HasColumnType("character varying(100)");
+
+ b.Property("PupilUpn")
+ .HasMaxLength(50)
+ .HasColumnType("character varying(50)");
+
+ b.Property("ReferenceNumber")
+ .IsRequired()
+ .HasMaxLength(50)
+ .HasColumnType("character varying(50)");
+
+ b.Property("RequestType")
+ .IsRequired()
+ .HasMaxLength(20)
+ .HasColumnType("character varying(20)");
+
+ b.Property("RequestTypeDescription")
+ .IsRequired()
+ .HasMaxLength(100)
+ .HasColumnType("character varying(100)");
+
+ b.Property("RulesVersion")
+ .HasMaxLength(100)
+ .HasColumnType("character varying(100)");
+
+ b.Property("Status")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.Property("Submitted")
+ .HasColumnType("timestamp without time zone");
+
+ b.Property("SubmittedByEmail")
+ .HasMaxLength(256)
+ .HasColumnType("character varying(256)");
+
+ b.Property("SubmittedById")
+ .HasColumnType("uuid");
+
+ b.Property("SubmittedByName")
+ .IsRequired()
+ .HasMaxLength(200)
+ .HasColumnType("character varying(200)");
+
+ b.Property("WindowId")
+ .HasColumnType("uuid");
+
+ b.Property("WithdrawnAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("WithdrawnByEmail")
+ .HasColumnType("text");
+
+ b.Property("WorkerStatus")
+ .HasColumnType("text");
+
+ b.HasKey("Id");
+
+ b.HasIndex("CrmId")
+ .IsUnique()
+ .HasFilter("\"CrmId\" IS NOT NULL");
+
+ b.HasIndex("ReferenceNumber")
+ .IsUnique();
+
+ b.HasIndex("Status");
+
+ b.HasIndex("WindowId", "OrganisationUrn");
+
+ b.ToTable("ChangeRequests");
+ });
+
+ modelBuilder.Entity("DfE.CheckPerformanceData.Persistence.Entities.CheckingWindow", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasDefaultValueSql("gen_random_uuid()");
+
+ b.Property("CheckingWindowType")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.Property("EndDate")
+ .HasColumnType("timestamp without time zone");
+
+ b.Property("IngressFile")
+ .IsRequired()
+ .HasMaxLength(255)
+ .HasColumnType("character varying(255)");
+
+ b.Property("IngressFileChecksum")
+ .IsRequired()
+ .HasMaxLength(256)
+ .HasColumnType("character varying(256)");
+
+ b.Property("KeyStage")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.Property("Published")
+ .HasColumnType("boolean");
+
+ b.Property("SchemaFile")
+ .IsRequired()
+ .HasMaxLength(255)
+ .HasColumnType("character varying(255)");
+
+ b.Property("SchemaFileChecksum")
+ .IsRequired()
+ .HasMaxLength(256)
+ .HasColumnType("character varying(256)");
+
+ b.Property("StartDate")
+ .HasColumnType("timestamp without time zone");
+
+ b.Property("Title")
+ .IsRequired()
+ .HasMaxLength(200)
+ .HasColumnType("character varying(200)");
+
+ b.Property("TurnaroundCommitment")
+ .IsRequired()
+ .HasMaxLength(200)
+ .HasColumnType("character varying(200)");
+
+ b.HasKey("Id");
+
+ b.ToTable("CheckingWindows");
+ });
+
+ modelBuilder.Entity("DfE.CheckPerformanceData.Persistence.Entities.CheckingWindowDataset", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasDefaultValueSql("gen_random_uuid()");
+
+ b.Property("CheckingWindowId")
+ .HasColumnType("uuid");
+
+ b.Property("Included")
+ .HasColumnType("boolean");
+
+ b.Property("IngressFile")
+ .IsRequired()
+ .HasMaxLength(255)
+ .HasColumnType("character varying(255)");
+
+ b.Property("IngressFileChecksum")
+ .IsRequired()
+ .HasMaxLength(256)
+ .HasColumnType("character varying(256)");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasMaxLength(50)
+ .HasColumnType("character varying(50)");
+
+ b.Property("SchemaFile")
+ .IsRequired()
+ .HasMaxLength(255)
+ .HasColumnType("character varying(255)");
+
+ b.Property("SchemaFileChecksum")
+ .IsRequired()
+ .HasMaxLength(256)
+ .HasColumnType("character varying(256)");
+
+ b.Property("SortOrder")
+ .HasColumnType("integer");
+
+ b.HasKey("Id");
+
+ b.HasIndex("CheckingWindowId", "Name")
+ .IsUnique();
+
+ b.ToTable("CheckingWindowDatasets");
+ });
+
+ modelBuilder.Entity("DfE.CheckPerformanceData.Persistence.Entities.ContentBlock", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("AppearInSearch")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("boolean")
+ .HasDefaultValue(true);
+
+ b.Property("BlockType")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.Property("ContentId")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasDefaultValueSql("gen_random_uuid()");
+
+ b.Property("CreatedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("CreatedBy")
+ .HasColumnType("text");
+
+ b.Property("Key")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.Property("Keywords")
+ .HasColumnType("text");
+
+ b.Property("LastSeenAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("LastSeenPath")
+ .HasColumnType("text");
+
+ b.Property("SearchVector")
+ .IsRequired()
+ .ValueGeneratedOnAddOrUpdate()
+ .HasColumnType("tsvector")
+ .HasComputedColumnSql("setweight(to_tsvector('english', coalesce(\"Keywords\", '')), 'A') || setweight(to_tsvector('english', coalesce(\"ValuePlainText\", '')), 'B')", true);
+
+ b.Property("UpdatedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("UpdatedBy")
+ .HasColumnType("text");
+
+ b.Property("Value")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.Property("ValuePlainText")
+ .IsRequired()
+ .ValueGeneratedOnAdd()
+ .HasColumnType("text")
+ .HasDefaultValue("");
+
+ b.HasKey("Id");
+
+ b.HasIndex("ContentId")
+ .IsUnique();
+
+ b.HasIndex("Key")
+ .IsUnique();
+
+ b.HasIndex("SearchVector");
+
+ NpgsqlIndexBuilderExtensions.HasMethod(b.HasIndex("SearchVector"), "gin");
+
+ b.ToTable("ContentBlocks");
+ });
+
+ modelBuilder.Entity("DfE.CheckPerformanceData.Persistence.Entities.ContentBlockVersion", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("ContentBlockId")
+ .HasColumnType("integer");
+
+ b.Property("CreatedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("CreatedBy")
+ .HasColumnType("text");
+
+ b.Property("Value")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.Property("VersionNumber")
+ .HasColumnType("integer");
+
+ b.HasKey("Id");
+
+ b.HasIndex("ContentBlockId", "VersionNumber")
+ .IsUnique();
+
+ b.ToTable("ContentBlockVersions");
+ });
+
+ modelBuilder.Entity("DfE.CheckPerformanceData.Persistence.Entities.Country", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid");
+
+ b.Property("Code")
+ .IsRequired()
+ .HasMaxLength(10)
+ .HasColumnType("character varying(10)");
+
+ b.Property("Kind")
+ .HasColumnType("integer");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasMaxLength(200)
+ .HasColumnType("character varying(200)");
+
+ b.Property("OfficialName")
+ .IsRequired()
+ .HasMaxLength(300)
+ .HasColumnType("character varying(300)");
+
+ b.HasKey("Id");
+
+ b.HasIndex("Code")
+ .IsUnique();
+
+ b.HasIndex("Name");
+
+ b.ToTable("Countries");
+ });
+
+ modelBuilder.Entity("DfE.CheckPerformanceData.Persistence.Entities.DeadLetterEntity", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property("Attempts")
+ .HasColumnType("integer")
+ .HasColumnName("attempts");
+
+ b.Property("DeadLetteredAtUtc")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("dead_lettered_at_utc");
+
+ b.Property("EnqueuedAtUtc")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("enqueued_at_utc");
+
+ b.Property("Payload")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasColumnName("payload");
+
+ b.Property("PayloadHash")
+ .IsRequired()
+ .HasMaxLength(64)
+ .HasColumnType("character varying(64)")
+ .HasColumnName("payload_hash");
+
+ b.Property("QueueName")
+ .IsRequired()
+ .HasMaxLength(100)
+ .HasColumnType("character varying(100)")
+ .HasColumnName("queue_name");
+
+ b.Property("Reason")
+ .IsRequired()
+ .HasMaxLength(1024)
+ .HasColumnType("character varying(1024)")
+ .HasColumnName("reason");
+
+ b.HasKey("Id");
+
+ b.HasIndex("DeadLetteredAtUtc")
+ .HasDatabaseName("ix_queue_dead_letters_dead_lettered_at");
+
+ b.ToTable("queue_dead_letters", (string)null);
+ });
+
+ modelBuilder.Entity("DfE.CheckPerformanceData.Persistence.Entities.DevZendeskTicket", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property("CreatedAtUtc")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at_utc");
+
+ b.Property("Priority")
+ .IsRequired()
+ .HasMaxLength(50)
+ .HasColumnType("character varying(50)")
+ .HasColumnName("priority");
+
+ b.Property("RawJson")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasColumnName("raw_json");
+
+ b.Property("ReferenceNumber")
+ .IsRequired()
+ .HasMaxLength(100)
+ .HasColumnType("character varying(100)")
+ .HasColumnName("reference_number");
+
+ b.Property("Status")
+ .IsRequired()
+ .HasMaxLength(50)
+ .HasColumnType("character varying(50)")
+ .HasColumnName("status");
+
+ b.Property("Subject")
+ .IsRequired()
+ .HasMaxLength(512)
+ .HasColumnType("character varying(512)")
+ .HasColumnName("subject");
+
+ b.Property("TicketId")
+ .HasColumnType("bigint")
+ .HasColumnName("ticket_id");
+
+ b.HasKey("Id");
+
+ b.HasIndex("CreatedAtUtc")
+ .HasDatabaseName("ix_dev_zendesk_outbox_created_at");
+
+ b.ToTable("dev_zendesk_outbox", (string)null);
+ });
+
+ modelBuilder.Entity("DfE.CheckPerformanceData.Persistence.Entities.OrganisationLogin", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasDefaultValueSql("gen_random_uuid()");
+
+ b.Property("Laestab")
+ .IsRequired()
+ .HasMaxLength(20)
+ .HasColumnType("character varying(20)");
+
+ b.Property("LoggedInAtUtc")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("OrganisationName")
+ .IsRequired()
+ .HasMaxLength(500)
+ .HasColumnType("character varying(500)");
+
+ b.Property("OrganisationUrn")
+ .HasColumnType("bigint");
+
+ b.HasKey("Id");
+
+ b.HasIndex("LoggedInAtUtc");
+
+ b.HasIndex("OrganisationUrn", "LoggedInAtUtc");
+
+ b.ToTable("OrganisationLogins");
+ });
+
+ modelBuilder.Entity("DfE.CheckPerformanceData.Persistence.Entities.PageNode", b =>
+ {
+ b.Property("Id")
+ .HasColumnType("uuid");
+
+ b.Property("AppearInSearch")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("boolean")
+ .HasDefaultValue(true);
+
+ b.Property("CreatedBy")
+ .HasColumnType("text");
+
+ b.Property("CreatedDate")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("DeletedBy")
+ .HasColumnType("text");
+
+ b.Property("DeletedDate")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("Keywords")
+ .HasColumnType("text");
+
+ b.Property("PageName")
+ .HasMaxLength(256)
+ .HasColumnType("character varying(256)");
+
+ b.Property("PageType")
+ .IsRequired()
+ .HasMaxLength(32)
+ .HasColumnType("character varying(32)");
+
+ b.Property("ParentId")
+ .HasColumnType("uuid");
+
+ b.Property("Path")
+ .IsRequired()
+ .HasMaxLength(2048)
+ .HasColumnType("character varying(2048)");
+
+ b.Property("SearchVector")
+ .IsRequired()
+ .ValueGeneratedOnAddOrUpdate()
+ .HasColumnType("tsvector")
+ .HasComputedColumnSql("setweight(to_tsvector('english', coalesce(\"Keywords\", '')), 'A') || setweight(to_tsvector('english', coalesce(\"Title\", '')), 'B') || setweight(to_tsvector('english', coalesce(\"Subtitle\", '')), 'C')", true);
+
+ b.Property("Segment")
+ .IsRequired()
+ .HasMaxLength(256)
+ .HasColumnType("character varying(256)");
+
+ b.Property("ShowInMenu")
+ .HasColumnType("boolean");
+
+ b.Property("SortOrder")
+ .HasColumnType("integer");
+
+ b.Property("Subtitle")
+ .HasMaxLength(1024)
+ .HasColumnType("character varying(1024)");
+
+ b.Property("Title")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.Property("UpdatedBy")
+ .HasColumnType("text");
+
+ b.Property("UpdatedDate")
+ .HasColumnType("timestamp with time zone");
+
+ b.HasKey("Id");
+
+ b.HasIndex("ParentId");
+
+ b.HasIndex("Path")
+ .IsUnique()
+ .HasFilter("\"DeletedDate\" IS NULL");
+
+ b.HasIndex("SearchVector");
+
+ NpgsqlIndexBuilderExtensions.HasMethod(b.HasIndex("SearchVector"), "gin");
+
+ b.ToTable("PageNodes");
+ });
+
+ modelBuilder.Entity("DfE.CheckPerformanceData.Persistence.Entities.PageNodeVersion", b =>
+ {
+ b.Property("Id")
+ .HasColumnType("uuid");
+
+ b.Property("BodyPlainText")
+ .IsRequired()
+ .ValueGeneratedOnAdd()
+ .HasColumnType("text")
+ .HasDefaultValue("");
+
+ b.Property("Content")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.Property("CreatedBy")
+ .HasColumnType("text");
+
+ b.Property("CreatedDate")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("IsCurrent")
+ .HasColumnType("boolean");
+
+ b.Property("MinorVersion")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer")
+ .HasDefaultValue(0);
+
+ b.Property("PageNodeId")
+ .HasColumnType("uuid");
+
+ b.Property("PublishFrom")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("PublishTo")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("SearchVector")
+ .IsRequired()
+ .ValueGeneratedOnAddOrUpdate()
+ .HasColumnType("tsvector")
+ .HasComputedColumnSql("setweight(to_tsvector('english', coalesce(\"BodyPlainText\", '')), 'D')", true);
+
+ b.Property("UpdatedBy")
+ .HasColumnType("text");
+
+ b.Property("UpdatedDate")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("VersionId")
+ .HasColumnType("integer");
+
+ b.HasKey("Id");
+
+ b.HasIndex("SearchVector");
+
+ NpgsqlIndexBuilderExtensions.HasMethod(b.HasIndex("SearchVector"), "gin");
+
+ b.HasIndex("PageNodeId", "IsCurrent");
+
+ b.HasIndex("PageNodeId", "VersionId")
+ .IsUnique();
+
+ b.ToTable("PageNodeVersions");
+ });
+
+ modelBuilder.Entity("DfE.CheckPerformanceData.Persistence.Entities.QueueMessageEntity", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property("Attempts")
+ .HasColumnType("integer")
+ .HasColumnName("attempts");
+
+ b.Property("EnqueuedAtUtc")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("enqueued_at_utc");
+
+ b.Property("Payload")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasColumnName("payload");
+
+ b.Property("QueueName")
+ .IsRequired()
+ .HasMaxLength(100)
+ .HasColumnType("character varying(100)")
+ .HasColumnName("queue_name");
+
+ b.Property("Status")
+ .IsRequired()
+ .HasMaxLength(20)
+ .HasColumnType("character varying(20)")
+ .HasColumnName("status");
+
+ b.Property("VisibleAfterUtc")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("visible_after_utc");
+
+ b.HasKey("Id");
+
+ b.HasIndex("QueueName", "Status", "VisibleAfterUtc")
+ .HasDatabaseName("ix_queue_messages_claim");
+
+ b.ToTable("queue_messages", (string)null);
+ });
+
+ modelBuilder.Entity("DfE.CheckPerformanceData.Persistence.Entities.RulesConfigVersion", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("ConfigType")
+ .IsRequired()
+ .HasMaxLength(20)
+ .HasColumnType("character varying(20)");
+
+ b.Property("Content")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.Property("CreatedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("CreatedBy")
+ .HasColumnType("text");
+
+ b.Property("VersionNumber")
+ .HasColumnType("integer");
+
+ b.HasKey("Id");
+
+ b.HasIndex("ConfigType", "VersionNumber")
+ .IsUnique();
+
+ b.ToTable("RulesConfigVersions");
+ });
+
+ modelBuilder.Entity("DfE.CheckPerformanceData.Persistence.Entities.Setting", b =>
+ {
+ b.Property("Key")
+ .HasMaxLength(200)
+ .HasColumnType("character varying(200)");
+
+ b.Property("Value")
+ .IsRequired()
+ .ValueGeneratedOnAdd()
+ .HasColumnType("text")
+ .HasDefaultValue("");
+
+ b.HasKey("Key");
+
+ b.ToTable("Settings");
+ });
+
+ modelBuilder.Entity("DfE.CheckPerformance.Persistence.Entities.SearchEventResult", b =>
+ {
+ b.HasOne("DfE.CheckPerformance.Persistence.Entities.SearchEvent", null)
+ .WithMany()
+ .HasForeignKey("SearchEventId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("DfE.CheckPerformanceData.Persistence.Entities.ChangeRequest", b =>
+ {
+ b.HasOne("DfE.CheckPerformanceData.Persistence.Entities.CheckingWindow", null)
+ .WithMany()
+ .HasForeignKey("WindowId")
+ .OnDelete(DeleteBehavior.Restrict)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("DfE.CheckPerformanceData.Persistence.Entities.CheckingWindow", b =>
+ {
+ b.OwnsOne("DfE.CheckPerformanceData.Persistence.Entities.WindowValidated", "Validated", b1 =>
+ {
+ b1.Property("CheckingWindowId")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid");
+
+ b1.Property("IngressValidationChecksum")
+ .IsRequired()
+ .HasMaxLength(256)
+ .HasColumnType("character varying(256)");
+
+ b1.Property("SchemaValidationChecksum")
+ .IsRequired()
+ .HasMaxLength(256)
+ .HasColumnType("character varying(256)");
+
+ b1.Property("ValidatedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b1.HasKey("CheckingWindowId");
+
+ b1.ToTable("CheckingWindows");
+
+ b1.WithOwner()
+ .HasForeignKey("CheckingWindowId");
+ });
+
+ b.Navigation("Validated");
+ });
+
+ modelBuilder.Entity("DfE.CheckPerformanceData.Persistence.Entities.CheckingWindowDataset", b =>
+ {
+ b.HasOne("DfE.CheckPerformanceData.Persistence.Entities.CheckingWindow", null)
+ .WithMany("Datasets")
+ .HasForeignKey("CheckingWindowId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("DfE.CheckPerformanceData.Persistence.Entities.ContentBlockVersion", b =>
+ {
+ b.HasOne("DfE.CheckPerformanceData.Persistence.Entities.ContentBlock", "ContentBlock")
+ .WithMany("Versions")
+ .HasForeignKey("ContentBlockId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("ContentBlock");
+ });
+
+ modelBuilder.Entity("DfE.CheckPerformanceData.Persistence.Entities.PageNode", b =>
+ {
+ b.HasOne("DfE.CheckPerformanceData.Persistence.Entities.PageNode", null)
+ .WithMany()
+ .HasForeignKey("ParentId")
+ .OnDelete(DeleteBehavior.Restrict)
+ .HasConstraintName("FK_PageNode_PageNode_ParentId");
+ });
+
+ modelBuilder.Entity("DfE.CheckPerformanceData.Persistence.Entities.PageNodeVersion", b =>
+ {
+ b.HasOne("DfE.CheckPerformanceData.Persistence.Entities.PageNode", "PageNode")
+ .WithMany("Versions")
+ .HasForeignKey("PageNodeId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("PageNode");
+ });
+
+ modelBuilder.Entity("DfE.CheckPerformanceData.Persistence.Entities.CheckingWindow", b =>
+ {
+ b.Navigation("Datasets");
+ });
+
+ modelBuilder.Entity("DfE.CheckPerformanceData.Persistence.Entities.ContentBlock", b =>
+ {
+ b.Navigation("Versions");
+ });
+
+ modelBuilder.Entity("DfE.CheckPerformanceData.Persistence.Entities.PageNode", b =>
+ {
+ b.Navigation("Versions");
+ });
+#pragma warning restore 612, 618
+ }
+ }
+}
diff --git a/src/DfE.CheckPerformanceData.Persistence/Migrations/20260818095733_AddCheckingWindowTurnaroundCommitment.cs b/src/DfE.CheckPerformanceData.Persistence/Migrations/20260818095733_AddCheckingWindowTurnaroundCommitment.cs
new file mode 100644
index 000000000..989ad1b7d
--- /dev/null
+++ b/src/DfE.CheckPerformanceData.Persistence/Migrations/20260818095733_AddCheckingWindowTurnaroundCommitment.cs
@@ -0,0 +1,30 @@
+using Microsoft.EntityFrameworkCore.Migrations;
+
+#nullable disable
+
+namespace DfE.CheckPerformanceData.Persistence.Migrations
+{
+ ///
+ public partial class AddCheckingWindowTurnaroundCommitment : Migration
+ {
+ ///
+ protected override void Up(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.AddColumn(
+ name: "TurnaroundCommitment",
+ table: "CheckingWindows",
+ type: "character varying(200)",
+ maxLength: 200,
+ nullable: false,
+ defaultValue: "");
+ }
+
+ ///
+ protected override void Down(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.DropColumn(
+ name: "TurnaroundCommitment",
+ table: "CheckingWindows");
+ }
+ }
+}
diff --git a/src/DfE.CheckPerformanceData.Persistence/Migrations/PortalDbContextModelSnapshot.cs b/src/DfE.CheckPerformanceData.Persistence/Migrations/PortalDbContextModelSnapshot.cs
index 881c2edbd..35112de5c 100644
--- a/src/DfE.CheckPerformanceData.Persistence/Migrations/PortalDbContextModelSnapshot.cs
+++ b/src/DfE.CheckPerformanceData.Persistence/Migrations/PortalDbContextModelSnapshot.cs
@@ -651,6 +651,11 @@ protected override void BuildModel(ModelBuilder modelBuilder)
.HasMaxLength(200)
.HasColumnType("character varying(200)");
+ b.Property("TurnaroundCommitment")
+ .IsRequired()
+ .HasMaxLength(200)
+ .HasColumnType("character varying(200)");
+
b.HasKey("Id");
b.ToTable("CheckingWindows");
diff --git a/src/DfE.CheckPerformanceData.Persistence/Repositories/CheckYourPupilDataRepository.cs b/src/DfE.CheckPerformanceData.Persistence/Repositories/CheckYourPupilDataRepository.cs
index bb0e13326..dfa290be5 100644
--- a/src/DfE.CheckPerformanceData.Persistence/Repositories/CheckYourPupilDataRepository.cs
+++ b/src/DfE.CheckPerformanceData.Persistence/Repositories/CheckYourPupilDataRepository.cs
@@ -43,7 +43,7 @@ public async Task GetCheckingWindowAsync(Guid windowId)
=> await dbContext.CheckingWindows
.AsNoTracking()
.Where(w => w.Id == windowId)
- .Select(w => new CheckingWindowDto { EndDate = w.EndDate, Title = w.Title, KeyStage = w.KeyStage, CheckingWindowType = w.CheckingWindowType, StartDate = w.StartDate })
+ .Select(w => new CheckingWindowDto { EndDate = w.EndDate, Title = w.Title, KeyStage = w.KeyStage, CheckingWindowType = w.CheckingWindowType, StartDate = w.StartDate, TurnaroundCommitment = w.TurnaroundCommitment })
.SingleAsync();
public async Task GetPupilAsync(Guid windowId, string laestab, Guid pupilId)
diff --git a/src/DfE.CheckPerformanceData.Persistence/Repositories/LandingPageRepository.cs b/src/DfE.CheckPerformanceData.Persistence/Repositories/LandingPageRepository.cs
index 5503e2b97..8e64a6604 100644
--- a/src/DfE.CheckPerformanceData.Persistence/Repositories/LandingPageRepository.cs
+++ b/src/DfE.CheckPerformanceData.Persistence/Repositories/LandingPageRepository.cs
@@ -27,6 +27,7 @@ public async Task> GetOpenWindowsAsync(DateTime now, str
w.KeyStage,
w.CheckingWindowType,
w.Title,
+ w.TurnaroundCommitment,
w.Id
})
.ToListAsync(cancellationToken);
@@ -54,6 +55,7 @@ public async Task