diff --git a/TeachingRecordSystem/src/TeachingRecordSystem.AuthorizeAccess/AuthorizeAccessLinkGenerator.cs b/TeachingRecordSystem/src/TeachingRecordSystem.AuthorizeAccess/AuthorizeAccessLinkGenerator.cs index a7d54ca10..924745c16 100644 --- a/TeachingRecordSystem/src/TeachingRecordSystem.AuthorizeAccess/AuthorizeAccessLinkGenerator.cs +++ b/TeachingRecordSystem/src/TeachingRecordSystem.AuthorizeAccess/AuthorizeAccessLinkGenerator.cs @@ -23,6 +23,9 @@ public string Trn(JourneyInstanceId journeyInstanceId, bool? fromCheckAnswers = public string CheckAnswers(JourneyInstanceId journeyInstanceId) => GetRequiredPathByPage("/CheckAnswers", journeyInstanceId: journeyInstanceId); + public string SupportRequestSubmitted(JourneyInstanceId journeyInstanceId) => + GetRequiredPathByPage("/SupportRequestSubmitted", journeyInstanceId: journeyInstanceId); + public string Found(JourneyInstanceId journeyInstanceId) => GetRequiredPathByPage("/Found", journeyInstanceId: journeyInstanceId); diff --git a/TeachingRecordSystem/src/TeachingRecordSystem.AuthorizeAccess/Pages/CheckAnswers.cshtml.cs b/TeachingRecordSystem/src/TeachingRecordSystem.AuthorizeAccess/Pages/CheckAnswers.cshtml.cs index bdf6189ff..8c18ca4fc 100644 --- a/TeachingRecordSystem/src/TeachingRecordSystem.AuthorizeAccess/Pages/CheckAnswers.cshtml.cs +++ b/TeachingRecordSystem/src/TeachingRecordSystem.AuthorizeAccess/Pages/CheckAnswers.cshtml.cs @@ -2,12 +2,16 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Filters; using Microsoft.AspNetCore.Mvc.RazorPages; +using Npgsql; +using TeachingRecordSystem.Core.DataStore.Postgres; +using TeachingRecordSystem.Core.DataStore.Postgres.Models; +using TeachingRecordSystem.Core.Models.SupportTaskData; using TeachingRecordSystem.FormFlow; namespace TeachingRecordSystem.AuthorizeAccess.Pages; [Journey(SignInJourneyState.JourneyName), RequireJourneyInstance] -public class CheckAnswersModel(SignInJourneyHelper helper) : PageModel +public class CheckAnswersModel(SignInJourneyHelper helper, TrsDbContext dbContext, IClock clock) : PageModel { public JourneyInstance? JourneyInstance { get; set; } @@ -25,7 +29,58 @@ public void OnGet() { } - public IActionResult OnPost() => throw new NotImplementedException(); + public async Task OnPost() + { + var subject = JourneyInstance!.State.OneLoginAuthenticationTicket!.Principal.FindFirstValue("sub")!; + var email = JourneyInstance!.State.OneLoginAuthenticationTicket!.Principal.FindFirstValue("email")!; + + var supportTask = new SupportTask() + { + SupportTaskReference = SupportTask.GenerateSupportTaskReference(), + CreatedOn = clock.UtcNow, + UpdatedOn = clock.UtcNow, + SupportTaskType = SupportTaskType.ConnectOneLoginUser, + Status = SupportTaskStatus.Open, + Data = new ConnectOneLoginUserData() + { + Verified = true, + OneLoginUserSubject = subject, + OneLoginUserEmail = email, + VerifiedNames = JourneyInstance.State.VerifiedNames, + VerifiedDatesOfBirth = JourneyInstance.State.VerifiedDatesOfBirth, + StatedNationalInsuranceNumber = JourneyInstance.State.NationalInsuranceNumber, + StatedTrn = JourneyInstance.State.Trn + }, + OneLoginUserSubject = subject + }; + dbContext.SupportTasks.Add(supportTask); + + dbContext.AddEvent(new SupportTaskCreatedEvent() + { + EventId = Guid.NewGuid(), + CreatedUtc = clock.UtcNow, + RaisedBy = SystemUser.SystemUserId, + SupportTask = EventModels.SupportTask.FromModel(supportTask) + }); + + while (true) + { + try + { + await dbContext.SaveChangesAsync(); + break; + } + catch (Exception ex) when (ex.InnerException is PostgresException postgresException && postgresException.SqlState == PostgresErrorCodes.UniqueViolation) + { + supportTask.SupportTaskReference = SupportTask.GenerateSupportTaskReference(); + continue; + } + } + + await JourneyInstance.UpdateStateAsync(state => state.HasPendingSupportRequest = true); + + return Redirect(helper.LinkGenerator.SupportRequestSubmitted(JourneyInstance!.InstanceId)); + } public override void OnPageHandlerExecuting(PageHandlerExecutingContext context) { diff --git a/TeachingRecordSystem/src/TeachingRecordSystem.AuthorizeAccess/Pages/NotFound.cshtml b/TeachingRecordSystem/src/TeachingRecordSystem.AuthorizeAccess/Pages/NotFound.cshtml index 85dbd5bdf..09c0f0c38 100644 --- a/TeachingRecordSystem/src/TeachingRecordSystem.AuthorizeAccess/Pages/NotFound.cshtml +++ b/TeachingRecordSystem/src/TeachingRecordSystem.AuthorizeAccess/Pages/NotFound.cshtml @@ -9,10 +9,10 @@ }
-
-

@ViewBag.Title

+ + @ViewBag.Title -
+

We’ve been unable to match your answers to a teaching record. @@ -30,6 +30,6 @@ Check your answers

-
-
+ +
diff --git a/TeachingRecordSystem/src/TeachingRecordSystem.AuthorizeAccess/Pages/SupportRequestSubmitted.cshtml b/TeachingRecordSystem/src/TeachingRecordSystem.AuthorizeAccess/Pages/SupportRequestSubmitted.cshtml new file mode 100644 index 000000000..80d9afe4e --- /dev/null +++ b/TeachingRecordSystem/src/TeachingRecordSystem.AuthorizeAccess/Pages/SupportRequestSubmitted.cshtml @@ -0,0 +1,13 @@ +@page "/request-submitted" +@model TeachingRecordSystem.AuthorizeAccess.Pages.SupportRequestSubmittedModel +@{ + ViewBag.Title = "Support request submitted"; +} + + + @ViewBag.Title + + We’ll contact you within
+ 5 days +
+
diff --git a/TeachingRecordSystem/src/TeachingRecordSystem.AuthorizeAccess/Pages/SupportRequestSubmitted.cshtml.cs b/TeachingRecordSystem/src/TeachingRecordSystem.AuthorizeAccess/Pages/SupportRequestSubmitted.cshtml.cs new file mode 100644 index 000000000..f1247b6bf --- /dev/null +++ b/TeachingRecordSystem/src/TeachingRecordSystem.AuthorizeAccess/Pages/SupportRequestSubmitted.cshtml.cs @@ -0,0 +1,46 @@ +using Microsoft.AspNetCore.Mvc.Filters; +using Microsoft.AspNetCore.Mvc.RazorPages; +using TeachingRecordSystem.FormFlow; + +namespace TeachingRecordSystem.AuthorizeAccess.Pages; + +[Journey(SignInJourneyState.JourneyName), RequireJourneyInstance] +public class SupportRequestSubmittedModel(SignInJourneyHelper helper) : PageModel +{ + public JourneyInstance? JourneyInstance { get; set; } + + public void OnGet() + { + } + + public override void OnPageHandlerExecuting(PageHandlerExecutingContext context) + { + var state = JourneyInstance!.State; + + if (state.OneLoginAuthenticationTicket is null || !state.IdentityVerified) + { + // Not authenticated/verified with One Login + context.Result = BadRequest(); + } + else if (state.AuthenticationTicket is not null) + { + // Already matched to a Teaching Record + context.Result = Redirect(helper.GetSafeRedirectUri(JourneyInstance)); + } + else if (!state.HaveNationalInsuranceNumber.HasValue) + { + // Not answered the NINO question + context.Result = Redirect(helper.LinkGenerator.NationalInsuranceNumber(JourneyInstance.InstanceId)); + } + else if (!state.HaveTrn.HasValue) + { + // Not answered the TRN question + context.Result = Redirect(helper.LinkGenerator.Trn(JourneyInstance.InstanceId)); + } + else if (!state.HasPendingSupportRequest) + { + // Not submitted a submit request + context.Result = Redirect(helper.LinkGenerator.CheckAnswers(JourneyInstance.InstanceId)); + } + } +} diff --git a/TeachingRecordSystem/src/TeachingRecordSystem.AuthorizeAccess/SignInJourneyState.cs b/TeachingRecordSystem/src/TeachingRecordSystem.AuthorizeAccess/SignInJourneyState.cs index 78d2160f1..b412cce0a 100644 --- a/TeachingRecordSystem/src/TeachingRecordSystem.AuthorizeAccess/SignInJourneyState.cs +++ b/TeachingRecordSystem/src/TeachingRecordSystem.AuthorizeAccess/SignInJourneyState.cs @@ -36,6 +36,8 @@ public class SignInJourneyState( public bool AttemptedIdentityVerification { get; set; } + public bool HasPendingSupportRequest { get; set; } + [JsonInclude] public bool IdentityVerified { get; private set; } diff --git a/TeachingRecordSystem/src/TeachingRecordSystem.Core/DataStore/Postgres/Mappings/SupportTaskMapping.cs b/TeachingRecordSystem/src/TeachingRecordSystem.Core/DataStore/Postgres/Mappings/SupportTaskMapping.cs index 964c26a8f..08698709b 100644 --- a/TeachingRecordSystem/src/TeachingRecordSystem.Core/DataStore/Postgres/Mappings/SupportTaskMapping.cs +++ b/TeachingRecordSystem/src/TeachingRecordSystem.Core/DataStore/Postgres/Mappings/SupportTaskMapping.cs @@ -1,3 +1,4 @@ +using System.Text.Json; using Microsoft.EntityFrameworkCore.Metadata.Builders; using TeachingRecordSystem.Core.DataStore.Postgres.Models; @@ -14,5 +15,7 @@ public void Configure(EntityTypeBuilder builder) builder.HasOne().WithMany().HasForeignKey(p => p.PersonId).HasConstraintName("fk_support_tasks_person"); builder.HasIndex(t => t.OneLoginUserSubject); builder.HasIndex(t => t.PersonId); + builder.Property("_data").HasColumnName("data").IsRequired(); + builder.Ignore(t => t.Data); } } diff --git a/TeachingRecordSystem/src/TeachingRecordSystem.Core/DataStore/Postgres/Migrations/20240425141449_SupportTaskCreatedOn.Designer.cs b/TeachingRecordSystem/src/TeachingRecordSystem.Core/DataStore/Postgres/Migrations/20240425141449_SupportTaskCreatedOn.Designer.cs new file mode 100644 index 000000000..eb20c2f9d --- /dev/null +++ b/TeachingRecordSystem/src/TeachingRecordSystem.Core/DataStore/Postgres/Migrations/20240425141449_SupportTaskCreatedOn.Designer.cs @@ -0,0 +1,2114 @@ +// +using System; +using System.Collections.Generic; +using System.Text.Json; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using TeachingRecordSystem.Core.DataStore.Postgres; + +#nullable disable + +namespace TeachingRecordSystem.Core.DataStore.Postgres.Migrations +{ + [DbContext(typeof(TrsDbContext))] + [Migration("20240425141449_SupportTaskCreatedOn")] + partial class SupportTaskCreatedOn + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.4") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreApplication", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("ApplicationType") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("application_type"); + + b.Property("ClientId") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("client_id"); + + b.Property("ClientSecret") + .HasColumnType("text") + .HasColumnName("client_secret"); + + b.Property("ClientType") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("client_type"); + + b.Property("ConcurrencyToken") + .IsConcurrencyToken() + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("concurrency_token"); + + b.Property("ConsentType") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("consent_type"); + + b.Property("DisplayName") + .HasColumnType("text") + .HasColumnName("display_name"); + + b.Property("DisplayNames") + .HasColumnType("text") + .HasColumnName("display_names"); + + b.Property("JsonWebKeySet") + .HasColumnType("text") + .HasColumnName("json_web_key_set"); + + b.Property("Permissions") + .HasColumnType("text") + .HasColumnName("permissions"); + + b.Property("PostLogoutRedirectUris") + .HasColumnType("text") + .HasColumnName("post_logout_redirect_uris"); + + b.Property("Properties") + .HasColumnType("text") + .HasColumnName("properties"); + + b.Property("RedirectUris") + .HasColumnType("text") + .HasColumnName("redirect_uris"); + + b.Property("Requirements") + .HasColumnType("text") + .HasColumnName("requirements"); + + b.Property("Settings") + .HasColumnType("text") + .HasColumnName("settings"); + + b.HasKey("Id") + .HasName("pk_oidc_applications"); + + b.HasIndex("ClientId") + .IsUnique() + .HasDatabaseName("ix_oidc_applications_client_id"); + + b.ToTable("oidc_applications", (string)null); + }); + + modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreAuthorization", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("ApplicationId") + .HasColumnType("uuid") + .HasColumnName("application_id"); + + b.Property("ConcurrencyToken") + .IsConcurrencyToken() + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("concurrency_token"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone") + .HasColumnName("creation_date"); + + b.Property("Properties") + .HasColumnType("text") + .HasColumnName("properties"); + + b.Property("Scopes") + .HasColumnType("text") + .HasColumnName("scopes"); + + b.Property("Status") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("status"); + + b.Property("Subject") + .HasMaxLength(400) + .HasColumnType("character varying(400)") + .HasColumnName("subject"); + + b.Property("Type") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("type"); + + b.HasKey("Id") + .HasName("pk_oidc_authorizations"); + + b.HasIndex("ApplicationId", "Status", "Subject", "Type") + .HasDatabaseName("ix_oidc_authorizations_application_id_status_subject_type"); + + b.ToTable("oidc_authorizations", (string)null); + }); + + modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreScope", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("ConcurrencyToken") + .IsConcurrencyToken() + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("concurrency_token"); + + b.Property("Description") + .HasColumnType("text") + .HasColumnName("description"); + + b.Property("Descriptions") + .HasColumnType("text") + .HasColumnName("descriptions"); + + b.Property("DisplayName") + .HasColumnType("text") + .HasColumnName("display_name"); + + b.Property("DisplayNames") + .HasColumnType("text") + .HasColumnName("display_names"); + + b.Property("Name") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("name"); + + b.Property("Properties") + .HasColumnType("text") + .HasColumnName("properties"); + + b.Property("Resources") + .HasColumnType("text") + .HasColumnName("resources"); + + b.HasKey("Id") + .HasName("pk_oidc_scopes"); + + b.HasIndex("Name") + .IsUnique() + .HasDatabaseName("ix_oidc_scopes_name"); + + b.ToTable("oidc_scopes", (string)null); + }); + + modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreToken", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("ApplicationId") + .HasColumnType("uuid") + .HasColumnName("application_id"); + + b.Property("AuthorizationId") + .HasColumnType("uuid") + .HasColumnName("authorization_id"); + + b.Property("ConcurrencyToken") + .IsConcurrencyToken() + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("concurrency_token"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone") + .HasColumnName("creation_date"); + + b.Property("ExpirationDate") + .HasColumnType("timestamp with time zone") + .HasColumnName("expiration_date"); + + b.Property("Payload") + .HasColumnType("text") + .HasColumnName("payload"); + + b.Property("Properties") + .HasColumnType("text") + .HasColumnName("properties"); + + b.Property("RedemptionDate") + .HasColumnType("timestamp with time zone") + .HasColumnName("redemption_date"); + + b.Property("ReferenceId") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("reference_id"); + + b.Property("Status") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("status"); + + b.Property("Subject") + .HasMaxLength(400) + .HasColumnType("character varying(400)") + .HasColumnName("subject"); + + b.Property("Type") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("type"); + + b.HasKey("Id") + .HasName("pk_oidc_tokens"); + + b.HasIndex("ReferenceId") + .IsUnique() + .HasDatabaseName("ix_oidc_tokens_reference_id"); + + b.HasIndex("ApplicationId", "Status", "Subject", "Type") + .HasDatabaseName("ix_oidc_tokens_application_id_status_subject_type"); + + b.ToTable("oidc_tokens", (string)null); + }); + + modelBuilder.Entity("TeachingRecordSystem.Core.DataStore.Postgres.Models.ApiKey", b => + { + b.Property("ApiKeyId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("api_key_id"); + + b.Property("ApplicationUserId") + .HasColumnType("uuid") + .HasColumnName("application_user_id"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("Expires") + .HasColumnType("timestamp with time zone") + .HasColumnName("expires"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("key"); + + b.Property("UpdatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_on"); + + b.HasKey("ApiKeyId") + .HasName("pk_api_keys"); + + b.HasIndex("ApplicationUserId") + .HasDatabaseName("ix_api_keys_application_user_id"); + + b.HasIndex("Key") + .IsUnique() + .HasDatabaseName("ix_api_keys_key"); + + b.ToTable("api_keys", (string)null); + }); + + modelBuilder.Entity("TeachingRecordSystem.Core.DataStore.Postgres.Models.EntityChangesJournal", b => + { + b.Property("Key") + .HasColumnType("text") + .HasColumnName("key"); + + b.Property("EntityLogicalName") + .HasColumnType("text") + .HasColumnName("entity_logical_name"); + + b.Property("DataToken") + .HasColumnType("text") + .HasColumnName("data_token"); + + b.Property("LastUpdated") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_updated"); + + b.Property("LastUpdatedBy") + .HasColumnType("text") + .HasColumnName("last_updated_by"); + + b.Property("NextQueryPageNumber") + .HasColumnType("integer") + .HasColumnName("next_query_page_number"); + + b.Property("NextQueryPageSize") + .HasColumnType("integer") + .HasColumnName("next_query_page_size"); + + b.Property("NextQueryPagingCookie") + .HasColumnType("text") + .HasColumnName("next_query_paging_cookie"); + + b.HasKey("Key", "EntityLogicalName") + .HasName("pk_entity_changes_journals"); + + b.ToTable("entity_changes_journals", (string)null); + }); + + modelBuilder.Entity("TeachingRecordSystem.Core.DataStore.Postgres.Models.Establishment", b => + { + b.Property("EstablishmentId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("establishment_id"); + + b.Property("Address3") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("address3") + .UseCollation("case_insensitive"); + + b.Property("County") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("county") + .UseCollation("case_insensitive"); + + b.Property("EstablishmentName") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)") + .HasColumnName("establishment_name") + .UseCollation("case_insensitive"); + + b.Property("EstablishmentNumber") + .HasMaxLength(4) + .HasColumnType("character(4)") + .HasColumnName("establishment_number") + .IsFixedLength(); + + b.Property("EstablishmentSourceId") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(1) + .HasColumnName("establishment_source_id"); + + b.Property("EstablishmentStatusCode") + .HasColumnType("integer") + .HasColumnName("establishment_status_code"); + + b.Property("EstablishmentStatusName") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("establishment_status_name"); + + b.Property("EstablishmentTypeCode") + .HasMaxLength(3) + .HasColumnType("character varying(3)") + .HasColumnName("establishment_type_code"); + + b.Property("EstablishmentTypeGroupCode") + .HasColumnType("integer") + .HasColumnName("establishment_type_group_code"); + + b.Property("EstablishmentTypeGroupName") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("establishment_type_group_name"); + + b.Property("EstablishmentTypeName") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("establishment_type_name") + .UseCollation("case_insensitive"); + + b.Property("LaCode") + .IsRequired() + .HasMaxLength(3) + .HasColumnType("character(3)") + .HasColumnName("la_code") + .IsFixedLength(); + + b.Property("LaName") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("la_name") + .UseCollation("case_insensitive"); + + b.Property("Locality") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("locality") + .UseCollation("case_insensitive"); + + b.Property("Postcode") + .HasMaxLength(10) + .HasColumnType("character varying(10)") + .HasColumnName("postcode") + .UseCollation("case_insensitive"); + + b.Property("Street") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("street") + .UseCollation("case_insensitive"); + + b.Property("Town") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("town") + .UseCollation("case_insensitive"); + + b.Property("Urn") + .HasMaxLength(6) + .HasColumnType("integer") + .HasColumnName("urn") + .IsFixedLength(); + + b.HasKey("EstablishmentId") + .HasName("pk_establishments"); + + b.HasIndex("EstablishmentSourceId") + .HasDatabaseName("ix_establishment_establishment_source_id"); + + b.HasIndex("Urn") + .IsUnique() + .HasDatabaseName("ix_establishment_urn"); + + b.HasIndex("LaCode", "EstablishmentNumber") + .HasDatabaseName("ix_establishment_la_code_establishment_number"); + + b.ToTable("establishments", (string)null); + }); + + modelBuilder.Entity("TeachingRecordSystem.Core.DataStore.Postgres.Models.EstablishmentSource", b => + { + b.Property("EstablishmentSourceId") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("establishment_source_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("EstablishmentSourceId")); + + b.Property("Name") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("name") + .UseCollation("case_insensitive"); + + b.HasKey("EstablishmentSourceId") + .HasName("pk_establishment_sources"); + + b.ToTable("establishment_sources", (string)null); + }); + + modelBuilder.Entity("TeachingRecordSystem.Core.DataStore.Postgres.Models.Event", b => + { + b.Property("EventId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("event_id"); + + b.Property("Created") + .HasColumnType("timestamp with time zone") + .HasColumnName("created"); + + b.Property("EventName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("event_name"); + + b.Property("Inserted") + .HasColumnType("timestamp with time zone") + .HasColumnName("inserted"); + + b.Property("Key") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("key"); + + b.Property("Payload") + .IsRequired() + .HasColumnType("jsonb") + .HasColumnName("payload"); + + b.Property("PersonId") + .HasColumnType("uuid") + .HasColumnName("person_id"); + + b.Property("Published") + .HasColumnType("boolean") + .HasColumnName("published"); + + b.HasKey("EventId") + .HasName("pk_events"); + + b.HasIndex("Key") + .IsUnique() + .HasDatabaseName("ix_events_key") + .HasFilter("key is not null"); + + b.HasIndex("Payload") + .HasDatabaseName("ix_events_payload"); + + NpgsqlIndexBuilderExtensions.HasMethod(b.HasIndex("Payload"), "gin"); + + b.HasIndex("PersonId", "EventName") + .HasDatabaseName("ix_events_person_id_event_name") + .HasFilter("person_id is not null"); + + NpgsqlIndexBuilderExtensions.IncludeProperties(b.HasIndex("PersonId", "EventName"), new[] { "Payload" }); + + b.ToTable("events", (string)null); + }); + + modelBuilder.Entity("TeachingRecordSystem.Core.DataStore.Postgres.Models.EytsAwardedEmailsJob", b => + { + b.Property("EytsAwardedEmailsJobId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("eyts_awarded_emails_job_id"); + + b.Property("AwardedToUtc") + .HasColumnType("timestamp with time zone") + .HasColumnName("awarded_to_utc"); + + b.Property("ExecutedUtc") + .HasColumnType("timestamp with time zone") + .HasColumnName("executed_utc"); + + b.HasKey("EytsAwardedEmailsJobId") + .HasName("pk_eyts_awarded_emails_jobs"); + + b.HasIndex("ExecutedUtc") + .HasDatabaseName("ix_eyts_awarded_emails_jobs_executed_utc"); + + b.ToTable("eyts_awarded_emails_jobs", (string)null); + }); + + modelBuilder.Entity("TeachingRecordSystem.Core.DataStore.Postgres.Models.EytsAwardedEmailsJobItem", b => + { + b.Property("EytsAwardedEmailsJobId") + .HasColumnType("uuid") + .HasColumnName("eyts_awarded_emails_job_id"); + + b.Property("PersonId") + .HasColumnType("uuid") + .HasColumnName("person_id"); + + b.Property("EmailAddress") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("email_address"); + + b.Property("EmailSent") + .HasColumnType("boolean") + .HasColumnName("email_sent"); + + b.Property("Personalization") + .IsRequired() + .HasColumnType("jsonb") + .HasColumnName("personalization"); + + b.Property("Trn") + .IsRequired() + .HasMaxLength(7) + .HasColumnType("character(7)") + .HasColumnName("trn") + .IsFixedLength(); + + b.HasKey("EytsAwardedEmailsJobId", "PersonId") + .HasName("pk_eyts_awarded_emails_job_items"); + + b.HasIndex("Personalization") + .HasDatabaseName("ix_eyts_awarded_emails_job_items_personalization"); + + NpgsqlIndexBuilderExtensions.HasMethod(b.HasIndex("Personalization"), "gin"); + + b.ToTable("eyts_awarded_emails_job_items", (string)null); + }); + + modelBuilder.Entity("TeachingRecordSystem.Core.DataStore.Postgres.Models.InductionCompletedEmailsJob", b => + { + b.Property("InductionCompletedEmailsJobId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("induction_completed_emails_job_id"); + + b.Property("AwardedToUtc") + .HasColumnType("timestamp with time zone") + .HasColumnName("awarded_to_utc"); + + b.Property("ExecutedUtc") + .HasColumnType("timestamp with time zone") + .HasColumnName("executed_utc"); + + b.HasKey("InductionCompletedEmailsJobId") + .HasName("pk_induction_completed_emails_jobs"); + + b.HasIndex("ExecutedUtc") + .HasDatabaseName("ix_induction_completed_emails_jobs_executed_utc"); + + b.ToTable("induction_completed_emails_jobs", (string)null); + }); + + modelBuilder.Entity("TeachingRecordSystem.Core.DataStore.Postgres.Models.InductionCompletedEmailsJobItem", b => + { + b.Property("InductionCompletedEmailsJobId") + .HasColumnType("uuid") + .HasColumnName("induction_completed_emails_job_id"); + + b.Property("PersonId") + .HasColumnType("uuid") + .HasColumnName("person_id"); + + b.Property("EmailAddress") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("email_address"); + + b.Property("EmailSent") + .HasColumnType("boolean") + .HasColumnName("email_sent"); + + b.Property("Personalization") + .IsRequired() + .HasColumnType("jsonb") + .HasColumnName("personalization"); + + b.Property("Trn") + .IsRequired() + .HasMaxLength(7) + .HasColumnType("character(7)") + .HasColumnName("trn") + .IsFixedLength(); + + b.HasKey("InductionCompletedEmailsJobId", "PersonId") + .HasName("pk_induction_completed_emails_job_items"); + + b.HasIndex("Personalization") + .HasDatabaseName("ix_induction_completed_emails_job_items_personalization"); + + NpgsqlIndexBuilderExtensions.HasMethod(b.HasIndex("Personalization"), "gin"); + + b.ToTable("induction_completed_emails_job_items", (string)null); + }); + + modelBuilder.Entity("TeachingRecordSystem.Core.DataStore.Postgres.Models.InternationalQtsAwardedEmailsJob", b => + { + b.Property("InternationalQtsAwardedEmailsJobId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("international_qts_awarded_emails_job_id"); + + b.Property("AwardedToUtc") + .HasColumnType("timestamp with time zone") + .HasColumnName("awarded_to_utc"); + + b.Property("ExecutedUtc") + .HasColumnType("timestamp with time zone") + .HasColumnName("executed_utc"); + + b.HasKey("InternationalQtsAwardedEmailsJobId") + .HasName("pk_international_qts_awarded_emails_jobs"); + + b.HasIndex("ExecutedUtc") + .HasDatabaseName("ix_international_qts_awarded_emails_jobs_executed_utc"); + + b.ToTable("international_qts_awarded_emails_jobs", (string)null); + }); + + modelBuilder.Entity("TeachingRecordSystem.Core.DataStore.Postgres.Models.InternationalQtsAwardedEmailsJobItem", b => + { + b.Property("InternationalQtsAwardedEmailsJobId") + .HasColumnType("uuid") + .HasColumnName("international_qts_awarded_emails_job_id"); + + b.Property("PersonId") + .HasColumnType("uuid") + .HasColumnName("person_id"); + + b.Property("EmailAddress") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("email_address"); + + b.Property("EmailSent") + .HasColumnType("boolean") + .HasColumnName("email_sent"); + + b.Property("Personalization") + .IsRequired() + .HasColumnType("jsonb") + .HasColumnName("personalization"); + + b.Property("Trn") + .IsRequired() + .HasMaxLength(7) + .HasColumnType("character(7)") + .HasColumnName("trn") + .IsFixedLength(); + + b.HasKey("InternationalQtsAwardedEmailsJobId", "PersonId") + .HasName("pk_international_qts_awarded_emails_job_items"); + + b.HasIndex("Personalization") + .HasDatabaseName("ix_international_qts_awarded_emails_job_items_personalization"); + + NpgsqlIndexBuilderExtensions.HasMethod(b.HasIndex("Personalization"), "gin"); + + b.ToTable("international_qts_awarded_emails_job_items", (string)null); + }); + + modelBuilder.Entity("TeachingRecordSystem.Core.DataStore.Postgres.Models.JourneyState", b => + { + b.Property("InstanceId") + .HasMaxLength(300) + .HasColumnType("character varying(300)") + .HasColumnName("instance_id"); + + b.Property("Completed") + .HasColumnType("timestamp with time zone") + .HasColumnName("completed"); + + b.Property("Created") + .HasColumnType("timestamp with time zone") + .HasColumnName("created"); + + b.Property("State") + .IsRequired() + .HasColumnType("text") + .HasColumnName("state"); + + b.Property("Updated") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated"); + + b.Property("UserId") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("user_id"); + + b.HasKey("InstanceId") + .HasName("pk_journey_states"); + + b.ToTable("journey_states", (string)null); + }); + + modelBuilder.Entity("TeachingRecordSystem.Core.DataStore.Postgres.Models.MandatoryQualificationProvider", b => + { + b.Property("MandatoryQualificationProviderId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("mandatory_qualification_provider_id"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("name"); + + b.HasKey("MandatoryQualificationProviderId") + .HasName("pk_mandatory_qualification_providers"); + + b.ToTable("mandatory_qualification_providers", (string)null); + + b.HasData( + new + { + MandatoryQualificationProviderId = new Guid("e28ea41d-408d-4c89-90cc-8b9b04ac68f5"), + Name = "University of Birmingham" + }, + new + { + MandatoryQualificationProviderId = new Guid("89f9a1aa-3d68-4985-a4ce-403b6044c18c"), + Name = "University of Leeds" + }, + new + { + MandatoryQualificationProviderId = new Guid("aa5c300e-3b7c-456c-8183-3520b3d55dca"), + Name = "University of Manchester" + }, + new + { + MandatoryQualificationProviderId = new Guid("f417e73e-e2ad-40eb-85e3-55865be7f6be"), + Name = "Mary Hare School / University of Hertfordshire" + }, + new + { + MandatoryQualificationProviderId = new Guid("fbf22e04-b274-4c80-aba8-79fb6a7a32ce"), + Name = "University of Edinburgh" + }, + new + { + MandatoryQualificationProviderId = new Guid("26204149-349c-4ad6-9466-bb9b83723eae"), + Name = "Liverpool John Moores University" + }, + new + { + MandatoryQualificationProviderId = new Guid("0c30f666-647c-4ea8-8883-0fc6010b56be"), + Name = "University of Oxford/Oxford Polytechnic" + }, + new + { + MandatoryQualificationProviderId = new Guid("d0e6d54c-5e90-438a-945d-f97388c2b352"), + Name = "University of Cambridge" + }, + new + { + MandatoryQualificationProviderId = new Guid("aec32252-ef25-452e-a358-34a04e03369c"), + Name = "University of Newcastle-upon-Tyne" + }, + new + { + MandatoryQualificationProviderId = new Guid("d9ee7054-7fde-4cfd-9a5e-4b99511d1b3d"), + Name = "University of Plymouth" + }, + new + { + MandatoryQualificationProviderId = new Guid("707d58ca-1953-413b-9a46-41e9b0be885e"), + Name = "University of Hertfordshire" + }, + new + { + MandatoryQualificationProviderId = new Guid("3fc648a7-18e4-49e7-8a4b-1612616b72d5"), + Name = "University of London" + }, + new + { + MandatoryQualificationProviderId = new Guid("374dceb8-8224-45b8-b7dc-a6b0282b1065"), + Name = "Bristol Polytechnic" + }, + new + { + MandatoryQualificationProviderId = new Guid("d4fc958b-21de-47ec-9f03-36ae237a1b11"), + Name = "University College, Swansea" + }); + }); + + modelBuilder.Entity("TeachingRecordSystem.Core.DataStore.Postgres.Models.NameSynonyms", b => + { + b.Property("NameSynonymsId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("name_synonyms_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("NameSynonymsId")); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("name") + .UseCollation("case_insensitive"); + + b.Property("Synonyms") + .IsRequired() + .HasColumnType("text[]") + .HasColumnName("synonyms") + .UseCollation("case_insensitive"); + + b.HasKey("NameSynonymsId") + .HasName("pk_name_synonyms"); + + b.HasIndex("Name") + .IsUnique() + .HasDatabaseName("ix_name_synonyms_name"); + + b.ToTable("name_synonyms", (string)null); + }); + + modelBuilder.Entity("TeachingRecordSystem.Core.DataStore.Postgres.Models.OneLoginUser", b => + { + b.Property("Subject") + .HasMaxLength(255) + .HasColumnType("character varying(255)") + .HasColumnName("subject"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("email"); + + b.Property("FirstOneLoginSignIn") + .HasColumnType("timestamp with time zone") + .HasColumnName("first_one_login_sign_in"); + + b.Property("FirstSignIn") + .HasColumnType("timestamp with time zone") + .HasColumnName("first_sign_in"); + + b.Property("LastCoreIdentityVc") + .HasColumnType("jsonb") + .HasColumnName("last_core_identity_vc"); + + b.Property("LastOneLoginSignIn") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_one_login_sign_in"); + + b.Property("LastSignIn") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_sign_in"); + + b.Property("PersonId") + .HasColumnType("uuid") + .HasColumnName("person_id"); + + b.Property("VerificationRoute") + .HasColumnType("integer") + .HasColumnName("verification_route"); + + b.Property("VerifiedDatesOfBirth") + .HasColumnType("jsonb") + .HasColumnName("verified_dates_of_birth"); + + b.Property("VerifiedNames") + .HasColumnType("jsonb") + .HasColumnName("verified_names"); + + b.Property("VerifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("verified_on"); + + b.HasKey("Subject") + .HasName("pk_one_login_users"); + + b.ToTable("one_login_users", (string)null); + }); + + modelBuilder.Entity("TeachingRecordSystem.Core.DataStore.Postgres.Models.Person", b => + { + b.Property("PersonId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("person_id"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("DateOfBirth") + .HasColumnType("date") + .HasColumnName("date_of_birth"); + + b.Property("DeletedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_on"); + + b.Property("DqtContactId") + .HasColumnType("uuid") + .HasColumnName("dqt_contact_id"); + + b.Property("DqtCreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("dqt_created_on"); + + b.Property("DqtFirstName") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("dqt_first_name") + .UseCollation("case_insensitive"); + + b.Property("DqtFirstSync") + .HasColumnType("timestamp with time zone") + .HasColumnName("dqt_first_sync"); + + b.Property("DqtLastName") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("dqt_last_name") + .UseCollation("case_insensitive"); + + b.Property("DqtLastSync") + .HasColumnType("timestamp with time zone") + .HasColumnName("dqt_last_sync"); + + b.Property("DqtMiddleName") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("dqt_middle_name") + .UseCollation("case_insensitive"); + + b.Property("DqtModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("dqt_modified_on"); + + b.Property("DqtState") + .HasColumnType("integer") + .HasColumnName("dqt_state"); + + b.Property("EmailAddress") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("email_address") + .UseCollation("case_insensitive"); + + b.Property("FirstName") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("first_name") + .UseCollation("case_insensitive"); + + b.Property("LastName") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("last_name") + .UseCollation("case_insensitive"); + + b.Property("MiddleName") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("middle_name") + .UseCollation("case_insensitive"); + + b.Property("NationalInsuranceNumber") + .HasMaxLength(9) + .HasColumnType("character(9)") + .HasColumnName("national_insurance_number") + .IsFixedLength(); + + b.Property("Trn") + .HasMaxLength(7) + .HasColumnType("character(7)") + .HasColumnName("trn") + .IsFixedLength(); + + b.Property("UpdatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_on"); + + b.HasKey("PersonId") + .HasName("pk_persons"); + + b.HasIndex("DqtContactId") + .IsUnique() + .HasDatabaseName("ix_persons_dqt_contact_id") + .HasFilter("dqt_contact_id is not null"); + + b.HasIndex("Trn") + .IsUnique() + .HasDatabaseName("ix_persons_trn") + .HasFilter("trn is not null"); + + b.ToTable("persons", (string)null); + }); + + modelBuilder.Entity("TeachingRecordSystem.Core.DataStore.Postgres.Models.PersonEmployment", b => + { + b.Property("PersonEmploymentId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("person_employment_id"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("EmploymentType") + .HasColumnType("integer") + .HasColumnName("employment_type"); + + b.Property("EndDate") + .HasColumnType("date") + .HasColumnName("end_date"); + + b.Property("EstablishmentId") + .HasColumnType("uuid") + .HasColumnName("establishment_id"); + + b.Property("PersonId") + .HasColumnType("uuid") + .HasColumnName("person_id"); + + b.Property("StartDate") + .HasColumnType("date") + .HasColumnName("start_date"); + + b.Property("UpdatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_on"); + + b.HasKey("PersonEmploymentId") + .HasName("pk_person_employments"); + + b.HasIndex("EstablishmentId") + .HasDatabaseName("ix_person_employments_establishment_id"); + + b.HasIndex("PersonId") + .HasDatabaseName("ix_person_employments_person_id"); + + b.ToTable("person_employments", (string)null); + }); + + modelBuilder.Entity("TeachingRecordSystem.Core.DataStore.Postgres.Models.PersonSearchAttribute", b => + { + b.Property("PersonSearchAttributeId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("person_search_attribute_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("PersonSearchAttributeId")); + + b.Property("AttributeKey") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("attribute_key") + .UseCollation("case_insensitive"); + + b.Property("AttributeType") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("attribute_type") + .UseCollation("case_insensitive"); + + b.Property("AttributeValue") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("attribute_value") + .UseCollation("case_insensitive"); + + b.Property("PersonId") + .HasColumnType("uuid") + .HasColumnName("person_id"); + + b.Property("Tags") + .IsRequired() + .HasColumnType("text[]") + .HasColumnName("tags"); + + b.HasKey("PersonSearchAttributeId") + .HasName("pk_person_search_attributes"); + + b.HasIndex("PersonId") + .HasDatabaseName("ix_person_search_attributes_person_id"); + + b.HasIndex("AttributeType", "AttributeValue") + .HasDatabaseName("ix_person_search_attributes_attribute_type_and_value"); + + b.ToTable("person_search_attributes", (string)null); + }); + + modelBuilder.Entity("TeachingRecordSystem.Core.DataStore.Postgres.Models.QtsAwardedEmailsJob", b => + { + b.Property("QtsAwardedEmailsJobId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("qts_awarded_emails_job_id"); + + b.Property("AwardedToUtc") + .HasColumnType("timestamp with time zone") + .HasColumnName("awarded_to_utc"); + + b.Property("ExecutedUtc") + .HasColumnType("timestamp with time zone") + .HasColumnName("executed_utc"); + + b.HasKey("QtsAwardedEmailsJobId") + .HasName("pk_qts_awarded_emails_jobs"); + + b.HasIndex("ExecutedUtc") + .HasDatabaseName("ix_qts_awarded_emails_jobs_executed_utc"); + + b.ToTable("qts_awarded_emails_jobs", (string)null); + }); + + modelBuilder.Entity("TeachingRecordSystem.Core.DataStore.Postgres.Models.QtsAwardedEmailsJobItem", b => + { + b.Property("QtsAwardedEmailsJobId") + .HasColumnType("uuid") + .HasColumnName("qts_awarded_emails_job_id"); + + b.Property("PersonId") + .HasColumnType("uuid") + .HasColumnName("person_id"); + + b.Property("EmailAddress") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("email_address"); + + b.Property("EmailSent") + .HasColumnType("boolean") + .HasColumnName("email_sent"); + + b.Property("Personalization") + .IsRequired() + .HasColumnType("jsonb") + .HasColumnName("personalization"); + + b.Property("Trn") + .IsRequired() + .HasMaxLength(7) + .HasColumnType("character(7)") + .HasColumnName("trn") + .IsFixedLength(); + + b.HasKey("QtsAwardedEmailsJobId", "PersonId") + .HasName("pk_qts_awarded_emails_job_items"); + + b.HasIndex("Personalization") + .HasDatabaseName("ix_qts_awarded_emails_job_items_personalization"); + + NpgsqlIndexBuilderExtensions.HasMethod(b.HasIndex("Personalization"), "gin"); + + b.ToTable("qts_awarded_emails_job_items", (string)null); + }); + + modelBuilder.Entity("TeachingRecordSystem.Core.DataStore.Postgres.Models.Qualification", b => + { + b.Property("QualificationId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("qualification_id"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("DeletedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_on"); + + b.Property("DqtCreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("dqt_created_on"); + + b.Property("DqtFirstSync") + .HasColumnType("timestamp with time zone") + .HasColumnName("dqt_first_sync"); + + b.Property("DqtLastSync") + .HasColumnType("timestamp with time zone") + .HasColumnName("dqt_last_sync"); + + b.Property("DqtModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("dqt_modified_on"); + + b.Property("DqtQualificationId") + .HasColumnType("uuid") + .HasColumnName("dqt_qualification_id"); + + b.Property("DqtState") + .HasColumnType("integer") + .HasColumnName("dqt_state"); + + b.Property("PersonId") + .HasColumnType("uuid") + .HasColumnName("person_id"); + + b.Property("QualificationType") + .HasColumnType("integer") + .HasColumnName("qualification_type"); + + b.Property("UpdatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_on"); + + b.HasKey("QualificationId") + .HasName("pk_qualifications"); + + b.HasIndex("DqtQualificationId") + .IsUnique() + .HasDatabaseName("ix_qualifications_dqt_qualification_id") + .HasFilter("dqt_qualification_id is not null"); + + b.HasIndex("PersonId") + .HasDatabaseName("ix_qualifications_person_id"); + + b.ToTable("qualifications", (string)null); + + b.HasDiscriminator("QualificationType"); + + b.UseTphMappingStrategy(); + }); + + modelBuilder.Entity("TeachingRecordSystem.Core.DataStore.Postgres.Models.SupportTask", b => + { + b.Property("SupportTaskReference") + .HasMaxLength(16) + .HasColumnType("character varying(16)") + .HasColumnName("support_task_reference"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("OneLoginUserSubject") + .HasColumnType("character varying(255)") + .HasColumnName("one_login_user_subject"); + + b.Property("PersonId") + .HasColumnType("uuid") + .HasColumnName("person_id"); + + b.Property("Status") + .HasColumnType("integer") + .HasColumnName("status"); + + b.Property("SupportTaskType") + .HasColumnType("integer") + .HasColumnName("support_task_type"); + + b.Property("UpdatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_on"); + + b.Property("_data") + .IsRequired() + .HasColumnType("jsonb") + .HasColumnName("data"); + + b.HasKey("SupportTaskReference") + .HasName("pk_support_tasks"); + + b.HasIndex("OneLoginUserSubject") + .HasDatabaseName("ix_support_tasks_one_login_user_subject"); + + b.HasIndex("PersonId") + .HasDatabaseName("ix_support_tasks_person_id"); + + b.ToTable("support_tasks", (string)null); + }); + + modelBuilder.Entity("TeachingRecordSystem.Core.DataStore.Postgres.Models.TpsCsvExtract", b => + { + b.Property("TpsCsvExtractId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("tps_csv_extract_id"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("Filename") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("filename"); + + b.HasKey("TpsCsvExtractId") + .HasName("pk_tps_csv_extracts"); + + b.ToTable("tps_csv_extracts", (string)null); + }); + + modelBuilder.Entity("TeachingRecordSystem.Core.DataStore.Postgres.Models.TpsCsvExtractItem", b => + { + b.Property("TpsCsvExtractItemId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("tps_csv_extract_item_id"); + + b.Property("Created") + .HasColumnType("timestamp with time zone") + .HasColumnName("created"); + + b.Property("DateOfBirth") + .HasColumnType("date") + .HasColumnName("date_of_birth"); + + b.Property("DateOfDeath") + .HasColumnType("date") + .HasColumnName("date_of_death"); + + b.Property("EmploymentEndDate") + .HasColumnType("date") + .HasColumnName("employment_end_date"); + + b.Property("EmploymentStartDate") + .HasColumnType("date") + .HasColumnName("employment_start_date"); + + b.Property("EmploymentType") + .HasColumnType("integer") + .HasColumnName("employment_type"); + + b.Property("EstablishmentEmailAddress") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("establishment_email_address"); + + b.Property("EstablishmentNumber") + .HasMaxLength(4) + .HasColumnType("character(4)") + .HasColumnName("establishment_number") + .IsFixedLength(); + + b.Property("EstablishmentPostcode") + .HasMaxLength(10) + .HasColumnType("character varying(10)") + .HasColumnName("establishment_postcode"); + + b.Property("ExtractDate") + .HasColumnType("date") + .HasColumnName("extract_date"); + + b.Property("Gender") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("character varying(10)") + .HasColumnName("gender"); + + b.Property("LocalAuthorityCode") + .IsRequired() + .HasMaxLength(3) + .HasColumnType("character(3)") + .HasColumnName("local_authority_code") + .IsFixedLength(); + + b.Property("MemberEmailAddress") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("member_email_address"); + + b.Property("MemberId") + .HasColumnType("integer") + .HasColumnName("member_id"); + + b.Property("MemberPostcode") + .HasMaxLength(10) + .HasColumnType("character varying(10)") + .HasColumnName("member_postcode"); + + b.Property("NationalInsuranceNumber") + .IsRequired() + .HasMaxLength(9) + .HasColumnType("character(9)") + .HasColumnName("national_insurance_number") + .IsFixedLength(); + + b.Property("Result") + .HasColumnType("integer") + .HasColumnName("result"); + + b.Property("TpsCsvExtractId") + .HasColumnType("uuid") + .HasColumnName("tps_csv_extract_id"); + + b.Property("TpsCsvExtractLoadItemId") + .HasColumnType("uuid") + .HasColumnName("tps_csv_extract_load_item_id"); + + b.Property("Trn") + .IsRequired() + .HasMaxLength(7) + .HasColumnType("character(7)") + .HasColumnName("trn") + .IsFixedLength(); + + b.Property("WithdrawlIndicator") + .HasMaxLength(1) + .HasColumnType("character(1)") + .HasColumnName("withdrawl_indicator") + .IsFixedLength(); + + b.HasKey("TpsCsvExtractItemId") + .HasName("pk_tps_csv_extract_items"); + + b.HasIndex("TpsCsvExtractId") + .HasDatabaseName("ix_tps_csv_extract_items_tps_csv_extract_id"); + + b.HasIndex("TpsCsvExtractLoadItemId") + .HasDatabaseName("ix_tps_csv_extract_items_tps_csv_extract_load_item_id"); + + b.HasIndex("Trn") + .HasDatabaseName("ix_tps_csv_extract_items_trn"); + + b.HasIndex("LocalAuthorityCode", "EstablishmentNumber") + .HasDatabaseName("ix_tps_csv_extract_items_la_code_establishment_number"); + + b.ToTable("tps_csv_extract_items", (string)null); + }); + + modelBuilder.Entity("TeachingRecordSystem.Core.DataStore.Postgres.Models.TpsCsvExtractLoadItem", b => + { + b.Property("TpsCsvExtractLoadItemId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("tps_csv_extract_load_item_id"); + + b.Property("Created") + .HasColumnType("timestamp with time zone") + .HasColumnName("created"); + + b.Property("DateOfBirth") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("date_of_birth"); + + b.Property("DateOfDeath") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("date_of_death"); + + b.Property("EmploymentEndDate") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("employment_end_date"); + + b.Property("EmploymentStartDate") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("employment_start_date"); + + b.Property("Errors") + .HasColumnType("integer") + .HasColumnName("errors"); + + b.Property("EstablishmentEmailAddress") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("establishment_email_address"); + + b.Property("EstablishmentNumber") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("establishment_number"); + + b.Property("EstablishmentPostcode") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("establishment_postcode"); + + b.Property("ExtractDate") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("extract_date"); + + b.Property("FullOrPartTimeIndicator") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("full_or_part_time_indicator"); + + b.Property("Gender") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("gender"); + + b.Property("LocalAuthorityCode") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("local_authority_code"); + + b.Property("MemberEmailAddress") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("member_email_address"); + + b.Property("MemberId") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("member_id"); + + b.Property("MemberPostcode") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("member_postcode"); + + b.Property("NationalInsuranceNumber") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("national_insurance_number"); + + b.Property("TpsCsvExtractId") + .HasColumnType("uuid") + .HasColumnName("tps_csv_extract_id"); + + b.Property("Trn") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("trn"); + + b.Property("WithdrawlIndicator") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("withdrawl_indicator"); + + b.HasKey("TpsCsvExtractLoadItemId") + .HasName("pk_tps_csv_extract_load_items"); + + b.ToTable("tps_csv_extract_load_items", (string)null); + }); + + modelBuilder.Entity("TeachingRecordSystem.Core.DataStore.Postgres.Models.TrnRequest", b => + { + b.Property("TrnRequestId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("trn_request_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("TrnRequestId")); + + b.Property("ClientId") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("client_id"); + + b.Property("IdentityUserId") + .HasColumnType("uuid") + .HasColumnName("identity_user_id"); + + b.Property("LinkedToIdentity") + .HasColumnType("boolean") + .HasColumnName("linked_to_identity"); + + b.Property("RequestId") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("request_id"); + + b.Property("TeacherId") + .HasColumnType("uuid") + .HasColumnName("teacher_id"); + + b.Property("TrnToken") + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasColumnName("trn_token"); + + b.HasKey("TrnRequestId") + .HasName("pk_trn_requests"); + + b.HasIndex("ClientId", "RequestId") + .IsUnique() + .HasDatabaseName("ix_trn_requests_client_id_request_id"); + + b.ToTable("trn_requests", (string)null); + }); + + modelBuilder.Entity("TeachingRecordSystem.Core.DataStore.Postgres.Models.UserBase", b => + { + b.Property("UserId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.Property("Active") + .HasColumnType("boolean") + .HasColumnName("active"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("name"); + + b.Property("UserType") + .HasColumnType("integer") + .HasColumnName("user_type"); + + b.HasKey("UserId") + .HasName("pk_users"); + + b.ToTable("users", (string)null); + + b.HasDiscriminator("UserType"); + + b.UseTphMappingStrategy(); + }); + + modelBuilder.Entity("TeachingRecordSystem.Core.DataStore.Postgres.Models.MandatoryQualification", b => + { + b.HasBaseType("TeachingRecordSystem.Core.DataStore.Postgres.Models.Qualification"); + + b.Property("DqtMqEstablishmentId") + .HasColumnType("uuid") + .HasColumnName("dqt_mq_establishment_id"); + + b.Property("DqtSpecialismId") + .HasColumnType("uuid") + .HasColumnName("dqt_specialism_id"); + + b.Property("EndDate") + .HasColumnType("date") + .HasColumnName("end_date"); + + b.Property("ProviderId") + .HasColumnType("uuid") + .HasColumnName("mq_provider_id"); + + b.Property("Specialism") + .HasColumnType("integer") + .HasColumnName("mq_specialism"); + + b.Property("StartDate") + .HasColumnType("date") + .HasColumnName("start_date"); + + b.Property("Status") + .HasColumnType("integer") + .HasColumnName("mq_status"); + + b.HasDiscriminator().HasValue(0); + }); + + modelBuilder.Entity("TeachingRecordSystem.Core.DataStore.Postgres.Models.ApplicationUser", b => + { + b.HasBaseType("TeachingRecordSystem.Core.DataStore.Postgres.Models.UserBase"); + + b.Property("ApiRoles") + .HasColumnType("varchar[]") + .HasColumnName("api_roles"); + + b.Property("ClientId") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("client_id"); + + b.Property("ClientSecret") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("client_secret"); + + b.Property("IsOidcClient") + .HasColumnType("boolean") + .HasColumnName("is_oidc_client"); + + b.Property("OneLoginAuthenticationSchemeName") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("one_login_authentication_scheme_name"); + + b.Property("OneLoginClientId") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("one_login_client_id"); + + b.Property("OneLoginPostLogoutRedirectUriPath") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("one_login_post_logout_redirect_uri_path"); + + b.Property("OneLoginPrivateKeyPem") + .HasMaxLength(2000) + .HasColumnType("character varying(2000)") + .HasColumnName("one_login_private_key_pem"); + + b.Property("OneLoginRedirectUriPath") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("one_login_redirect_uri_path"); + + b.Property>("PostLogoutRedirectUris") + .HasColumnType("varchar[]") + .HasColumnName("post_logout_redirect_uris"); + + b.Property>("RedirectUris") + .HasColumnType("varchar[]") + .HasColumnName("redirect_uris"); + + b.HasIndex("ClientId") + .IsUnique() + .HasDatabaseName("ix_users_client_id") + .HasFilter("client_id is not null"); + + b.HasIndex("OneLoginAuthenticationSchemeName") + .IsUnique() + .HasDatabaseName("ix_users_one_login_authentication_scheme_name") + .HasFilter("one_login_authentication_scheme_name is not null"); + + b.HasDiscriminator().HasValue(2); + }); + + modelBuilder.Entity("TeachingRecordSystem.Core.DataStore.Postgres.Models.SystemUser", b => + { + b.HasBaseType("TeachingRecordSystem.Core.DataStore.Postgres.Models.UserBase"); + + b.HasDiscriminator().HasValue(3); + + b.HasData( + new + { + UserId = new Guid("a81394d1-a498-46d8-af3e-e077596ab303"), + Active = true, + Name = "System", + UserType = 0 + }); + }); + + modelBuilder.Entity("TeachingRecordSystem.Core.DataStore.Postgres.Models.User", b => + { + b.HasBaseType("TeachingRecordSystem.Core.DataStore.Postgres.Models.UserBase"); + + b.Property("AzureAdUserId") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("azure_ad_user_id"); + + b.Property("DqtUserId") + .HasColumnType("uuid") + .HasColumnName("dqt_user_id"); + + b.Property("Email") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("email") + .UseCollation("case_insensitive"); + + b.Property("Roles") + .IsRequired() + .HasColumnType("varchar[]") + .HasColumnName("roles"); + + b.HasIndex("AzureAdUserId") + .IsUnique() + .HasDatabaseName("ix_users_azure_ad_user_id"); + + b.HasDiscriminator().HasValue(1); + }); + + modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreAuthorization", b => + { + b.HasOne("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreApplication", "Application") + .WithMany("Authorizations") + .HasForeignKey("ApplicationId") + .HasConstraintName("fk_oidc_authorizations_oidc_applications_application_id"); + + b.Navigation("Application"); + }); + + modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreToken", b => + { + b.HasOne("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreApplication", "Application") + .WithMany("Tokens") + .HasForeignKey("ApplicationId") + .HasConstraintName("fk_oidc_tokens_oidc_applications_application_id"); + + b.HasOne("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreAuthorization", "Authorization") + .WithMany("Tokens") + .HasForeignKey("AuthorizationId") + .HasConstraintName("fk_oidc_tokens_oidc_authorizations_authorization_id"); + + b.Navigation("Application"); + + b.Navigation("Authorization"); + }); + + modelBuilder.Entity("TeachingRecordSystem.Core.DataStore.Postgres.Models.ApiKey", b => + { + b.HasOne("TeachingRecordSystem.Core.DataStore.Postgres.Models.ApplicationUser", "ApplicationUser") + .WithMany("ApiKeys") + .HasForeignKey("ApplicationUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_api_key_application_user"); + + b.Navigation("ApplicationUser"); + }); + + modelBuilder.Entity("TeachingRecordSystem.Core.DataStore.Postgres.Models.Establishment", b => + { + b.HasOne("TeachingRecordSystem.Core.DataStore.Postgres.Models.EstablishmentSource", null) + .WithMany() + .HasForeignKey("EstablishmentSourceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_establishments_establishment_source_id"); + }); + + modelBuilder.Entity("TeachingRecordSystem.Core.DataStore.Postgres.Models.EytsAwardedEmailsJobItem", b => + { + b.HasOne("TeachingRecordSystem.Core.DataStore.Postgres.Models.EytsAwardedEmailsJob", "EytsAwardedEmailsJob") + .WithMany("JobItems") + .HasForeignKey("EytsAwardedEmailsJobId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_eyts_awarded_emails_job_items_eyts_awarded_emails_jobs_eyts"); + + b.Navigation("EytsAwardedEmailsJob"); + }); + + modelBuilder.Entity("TeachingRecordSystem.Core.DataStore.Postgres.Models.InductionCompletedEmailsJobItem", b => + { + b.HasOne("TeachingRecordSystem.Core.DataStore.Postgres.Models.InductionCompletedEmailsJob", "InductionCompletedEmailsJob") + .WithMany("JobItems") + .HasForeignKey("InductionCompletedEmailsJobId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_induction_completed_emails_job_items_induction_completed_em"); + + b.Navigation("InductionCompletedEmailsJob"); + }); + + modelBuilder.Entity("TeachingRecordSystem.Core.DataStore.Postgres.Models.InternationalQtsAwardedEmailsJobItem", b => + { + b.HasOne("TeachingRecordSystem.Core.DataStore.Postgres.Models.InternationalQtsAwardedEmailsJob", "InternationalQtsAwardedEmailsJob") + .WithMany("JobItems") + .HasForeignKey("InternationalQtsAwardedEmailsJobId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_international_qts_awarded_emails_job_items_international_qt"); + + b.Navigation("InternationalQtsAwardedEmailsJob"); + }); + + modelBuilder.Entity("TeachingRecordSystem.Core.DataStore.Postgres.Models.OneLoginUser", b => + { + b.HasOne("TeachingRecordSystem.Core.DataStore.Postgres.Models.Person", "Person") + .WithOne() + .HasForeignKey("TeachingRecordSystem.Core.DataStore.Postgres.Models.OneLoginUser", "PersonId") + .HasConstraintName("fk_one_login_users_persons_person_id"); + + b.Navigation("Person"); + }); + + modelBuilder.Entity("TeachingRecordSystem.Core.DataStore.Postgres.Models.PersonEmployment", b => + { + b.HasOne("TeachingRecordSystem.Core.DataStore.Postgres.Models.Establishment", null) + .WithMany() + .HasForeignKey("EstablishmentId") + .HasConstraintName("fk_person_employments_establishment_id"); + + b.HasOne("TeachingRecordSystem.Core.DataStore.Postgres.Models.Person", null) + .WithMany() + .HasForeignKey("PersonId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_person_employments_person_id"); + }); + + modelBuilder.Entity("TeachingRecordSystem.Core.DataStore.Postgres.Models.QtsAwardedEmailsJobItem", b => + { + b.HasOne("TeachingRecordSystem.Core.DataStore.Postgres.Models.QtsAwardedEmailsJob", "QtsAwardedEmailsJob") + .WithMany("JobItems") + .HasForeignKey("QtsAwardedEmailsJobId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_qts_awarded_emails_job_items_qts_awarded_emails_jobs_qts_aw"); + + b.Navigation("QtsAwardedEmailsJob"); + }); + + modelBuilder.Entity("TeachingRecordSystem.Core.DataStore.Postgres.Models.Qualification", b => + { + b.HasOne("TeachingRecordSystem.Core.DataStore.Postgres.Models.Person", null) + .WithMany() + .HasForeignKey("PersonId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_qualifications_person"); + }); + + modelBuilder.Entity("TeachingRecordSystem.Core.DataStore.Postgres.Models.SupportTask", b => + { + b.HasOne("TeachingRecordSystem.Core.DataStore.Postgres.Models.OneLoginUser", null) + .WithMany() + .HasForeignKey("OneLoginUserSubject") + .HasConstraintName("fk_support_tasks_one_login_user"); + + b.HasOne("TeachingRecordSystem.Core.DataStore.Postgres.Models.Person", null) + .WithMany() + .HasForeignKey("PersonId") + .HasConstraintName("fk_support_tasks_person"); + }); + + modelBuilder.Entity("TeachingRecordSystem.Core.DataStore.Postgres.Models.TpsCsvExtractItem", b => + { + b.HasOne("TeachingRecordSystem.Core.DataStore.Postgres.Models.TpsCsvExtract", null) + .WithMany() + .HasForeignKey("TpsCsvExtractId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_tps_csv_extract_items_tps_csv_extract_id"); + + b.HasOne("TeachingRecordSystem.Core.DataStore.Postgres.Models.TpsCsvExtractLoadItem", null) + .WithMany() + .HasForeignKey("TpsCsvExtractLoadItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_tps_csv_extract_items_tps_csv_extract_load_item_id"); + }); + + modelBuilder.Entity("TeachingRecordSystem.Core.DataStore.Postgres.Models.TpsCsvExtractLoadItem", b => + { + b.HasOne("TeachingRecordSystem.Core.DataStore.Postgres.Models.TpsCsvExtract", null) + .WithMany() + .HasForeignKey("TpsCsvExtractId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_tps_csv_extract_load_items_tps_csv_extract_id"); + }); + + modelBuilder.Entity("TeachingRecordSystem.Core.DataStore.Postgres.Models.MandatoryQualification", b => + { + b.HasOne("TeachingRecordSystem.Core.DataStore.Postgres.Models.MandatoryQualificationProvider", "Provider") + .WithMany() + .HasForeignKey("ProviderId") + .HasConstraintName("fk_qualifications_mandatory_qualification_provider"); + + b.Navigation("Provider"); + }); + + modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreApplication", b => + { + b.Navigation("Authorizations"); + + b.Navigation("Tokens"); + }); + + modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreAuthorization", b => + { + b.Navigation("Tokens"); + }); + + modelBuilder.Entity("TeachingRecordSystem.Core.DataStore.Postgres.Models.EytsAwardedEmailsJob", b => + { + b.Navigation("JobItems"); + }); + + modelBuilder.Entity("TeachingRecordSystem.Core.DataStore.Postgres.Models.InductionCompletedEmailsJob", b => + { + b.Navigation("JobItems"); + }); + + modelBuilder.Entity("TeachingRecordSystem.Core.DataStore.Postgres.Models.InternationalQtsAwardedEmailsJob", b => + { + b.Navigation("JobItems"); + }); + + modelBuilder.Entity("TeachingRecordSystem.Core.DataStore.Postgres.Models.QtsAwardedEmailsJob", b => + { + b.Navigation("JobItems"); + }); + + modelBuilder.Entity("TeachingRecordSystem.Core.DataStore.Postgres.Models.ApplicationUser", b => + { + b.Navigation("ApiKeys"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/TeachingRecordSystem/src/TeachingRecordSystem.Core/DataStore/Postgres/Migrations/20240425141449_SupportTaskCreatedOn.cs b/TeachingRecordSystem/src/TeachingRecordSystem.Core/DataStore/Postgres/Migrations/20240425141449_SupportTaskCreatedOn.cs new file mode 100644 index 000000000..f3b2443c7 --- /dev/null +++ b/TeachingRecordSystem/src/TeachingRecordSystem.Core/DataStore/Postgres/Migrations/20240425141449_SupportTaskCreatedOn.cs @@ -0,0 +1,41 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace TeachingRecordSystem.Core.DataStore.Postgres.Migrations +{ + /// + public partial class SupportTaskCreatedOn : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "created_on", + table: "support_tasks", + type: "timestamp with time zone", + nullable: false, + defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified)); + + migrationBuilder.AddColumn( + name: "updated_on", + table: "support_tasks", + type: "timestamp with time zone", + nullable: false, + defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified)); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "created_on", + table: "support_tasks"); + + migrationBuilder.DropColumn( + name: "updated_on", + table: "support_tasks"); + } + } +} diff --git a/TeachingRecordSystem/src/TeachingRecordSystem.Core/DataStore/Postgres/Migrations/TrsDbContextModelSnapshot.cs b/TeachingRecordSystem/src/TeachingRecordSystem.Core/DataStore/Postgres/Migrations/TrsDbContextModelSnapshot.cs index 5846cb4ad..fb4c0c76d 100644 --- a/TeachingRecordSystem/src/TeachingRecordSystem.Core/DataStore/Postgres/Migrations/TrsDbContextModelSnapshot.cs +++ b/TeachingRecordSystem/src/TeachingRecordSystem.Core/DataStore/Postgres/Migrations/TrsDbContextModelSnapshot.cs @@ -1356,10 +1356,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("character varying(16)") .HasColumnName("support_task_reference"); - b.Property("Data") - .IsRequired() - .HasColumnType("jsonb") - .HasColumnName("data"); + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); b.Property("OneLoginUserSubject") .HasColumnType("character varying(255)") @@ -1377,6 +1376,15 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("integer") .HasColumnName("support_task_type"); + b.Property("UpdatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_on"); + + b.Property("_data") + .IsRequired() + .HasColumnType("jsonb") + .HasColumnName("data"); + b.HasKey("SupportTaskReference") .HasName("pk_support_tasks"); diff --git a/TeachingRecordSystem/src/TeachingRecordSystem.Core/DataStore/Postgres/Models/SupportTask.cs b/TeachingRecordSystem/src/TeachingRecordSystem.Core/DataStore/Postgres/Models/SupportTask.cs index 85f7d6bd9..10015bcc5 100644 --- a/TeachingRecordSystem/src/TeachingRecordSystem.Core/DataStore/Postgres/Models/SupportTask.cs +++ b/TeachingRecordSystem/src/TeachingRecordSystem.Core/DataStore/Postgres/Models/SupportTask.cs @@ -1,20 +1,31 @@ using System.Security.Cryptography; using System.Text; using System.Text.Json; +using TeachingRecordSystem.Core.Models.SupportTaskData; namespace TeachingRecordSystem.Core.DataStore.Postgres.Models; public class SupportTask { + internal static readonly JsonSerializerOptions SerializerOptions = new(); private static readonly char[] _validReferenceChars = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789".ToCharArray(); - public required string SupportTaskReference { get; init; } + private JsonDocument _data = null!; + + public required string SupportTaskReference { get; set; } + public required DateTime CreatedOn { get; init; } + public required DateTime UpdatedOn { get; set; } public required SupportTaskType SupportTaskType { get; init; } public required SupportTaskStatus Status { get; set; } - public required JsonDocument Data { get; set; } public string? OneLoginUserSubject { get; init; } public Guid? PersonId { get; init; } + public required object Data + { + get => JsonSerializer.Deserialize(_data, GetDataType(), SerializerOptions)!; + set => _data = JsonSerializer.SerializeToDocument(value, GetDataType(), SerializerOptions); + } + public static string GenerateSupportTaskReference() { var random = GetEncodedRandomBytes(); @@ -63,4 +74,12 @@ static char GetCheckDigit(string input) return _validReferenceChars[checkCodePoint]; } } + + internal static Type GetDataType(SupportTaskType supportTaskType) => supportTaskType switch + { + SupportTaskType.ConnectOneLoginUser => typeof(ConnectOneLoginUserData), + _ => throw new ArgumentNullException($"Unknown {nameof(SupportTaskType)}: {supportTaskType}'.") + }; + + private Type GetDataType() => GetDataType(SupportTaskType); } diff --git a/TeachingRecordSystem/src/TeachingRecordSystem.Core/Events/Models/SupportTask.cs b/TeachingRecordSystem/src/TeachingRecordSystem.Core/Events/Models/SupportTask.cs new file mode 100644 index 000000000..4cae8e48b --- /dev/null +++ b/TeachingRecordSystem/src/TeachingRecordSystem.Core/Events/Models/SupportTask.cs @@ -0,0 +1,34 @@ +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace TeachingRecordSystem.Core.Events.Models; + +public record SupportTask +{ + [JsonInclude] + [JsonPropertyName("Data")] + private JsonDocument _data = null!; + + public required string SupportTaskReference { get; init; } + public required SupportTaskType SupportTaskType { get; init; } + public required SupportTaskStatus Status { get; init; } + public required string? OneLoginUserSubject { get; init; } + public required Guid? PersonId { get; init; } + + [JsonIgnore(Condition = JsonIgnoreCondition.Always)] + public object Data + { + get => JsonSerializer.Deserialize(_data, DataStore.Postgres.Models.SupportTask.GetDataType(SupportTaskType), DataStore.Postgres.Models.SupportTask.SerializerOptions)!; + init => _data = JsonSerializer.SerializeToDocument(value, DataStore.Postgres.Models.SupportTask.GetDataType(SupportTaskType), DataStore.Postgres.Models.SupportTask.SerializerOptions); + } + + public static SupportTask FromModel(DataStore.Postgres.Models.SupportTask model) => new() + { + SupportTaskReference = model.SupportTaskReference, + SupportTaskType = model.SupportTaskType, + Status = model.Status, + OneLoginUserSubject = model.OneLoginUserSubject, + PersonId = model.PersonId, + Data = model.Data + }; +} diff --git a/TeachingRecordSystem/src/TeachingRecordSystem.Core/Events/SupportTaskCreatedEvent.cs b/TeachingRecordSystem/src/TeachingRecordSystem.Core/Events/SupportTaskCreatedEvent.cs new file mode 100644 index 000000000..eccf767e2 --- /dev/null +++ b/TeachingRecordSystem/src/TeachingRecordSystem.Core/Events/SupportTaskCreatedEvent.cs @@ -0,0 +1,8 @@ +using TeachingRecordSystem.Core.Events.Models; + +namespace TeachingRecordSystem.Core.Events; + +public record SupportTaskCreatedEvent : EventBase +{ + public required SupportTask SupportTask { get; init; } +} diff --git a/TeachingRecordSystem/src/TeachingRecordSystem.Core/Models/SupportTaskData/ConnectOneLoginUserData.cs b/TeachingRecordSystem/src/TeachingRecordSystem.Core/Models/SupportTaskData/ConnectOneLoginUserData.cs new file mode 100644 index 000000000..2206926f6 --- /dev/null +++ b/TeachingRecordSystem/src/TeachingRecordSystem.Core/Models/SupportTaskData/ConnectOneLoginUserData.cs @@ -0,0 +1,13 @@ +namespace TeachingRecordSystem.Core.Models.SupportTaskData; + +public record ConnectOneLoginUserData +{ + public required bool Verified { get; init; } + public required string OneLoginUserSubject { get; init; } + public required string OneLoginUserEmail { get; init; } + public required string[][]? VerifiedNames { get; init; } + public required DateOnly[]? VerifiedDatesOfBirth { get; init; } + public required string? StatedNationalInsuranceNumber { get; init; } + public required string? StatedTrn { get; init; } + public Guid? PersonId { get; set; } +} diff --git a/TeachingRecordSystem/src/TeachingRecordSystem.Core/Models/SupportTaskType.cs b/TeachingRecordSystem/src/TeachingRecordSystem.Core/Models/SupportTaskType.cs index bbab0ac18..e11069aab 100644 --- a/TeachingRecordSystem/src/TeachingRecordSystem.Core/Models/SupportTaskType.cs +++ b/TeachingRecordSystem/src/TeachingRecordSystem.Core/Models/SupportTaskType.cs @@ -2,5 +2,5 @@ namespace TeachingRecordSystem.Core.Models; public enum SupportTaskType { - ConnectOneLoginUserToATeachingRecord = 1 + ConnectOneLoginUser = 1 } diff --git a/TeachingRecordSystem/tests/TeachingRecordSystem.AuthorizeAccess.EndToEndTests/SignInTests.cs b/TeachingRecordSystem/tests/TeachingRecordSystem.AuthorizeAccess.EndToEndTests/SignInTests.cs index f8b7d60c1..fe5746183 100644 --- a/TeachingRecordSystem/tests/TeachingRecordSystem.AuthorizeAccess.EndToEndTests/SignInTests.cs +++ b/TeachingRecordSystem/tests/TeachingRecordSystem.AuthorizeAccess.EndToEndTests/SignInTests.cs @@ -145,6 +145,9 @@ public async Task SignIn_UnknownVerifiedUser_DoesNotMatchWithNinoOrTrn() await page.ClickButton("Check your answers"); await page.WaitForUrlPathAsync("/check-answers"); + await page.ClickButton("Submit support request"); + + await page.WaitForUrlPathAsync("/request-submitted"); } [Fact] @@ -177,6 +180,9 @@ public async Task SignIn_UnknownVerifiedUserWithNeitherNinoNorTrn_DoesNotMatch() await page.ClickButton("Check your answers"); await page.WaitForUrlPathAsync("/check-answers"); + await page.ClickButton("Submit support request"); + + await page.WaitForUrlPathAsync("/request-submitted"); } [Fact] diff --git a/TeachingRecordSystem/tests/TeachingRecordSystem.AuthorizeAccess.Tests/PageTests/CheckAnswersTests.cs b/TeachingRecordSystem/tests/TeachingRecordSystem.AuthorizeAccess.Tests/PageTests/CheckAnswersTests.cs index f25746770..574376fea 100644 --- a/TeachingRecordSystem/tests/TeachingRecordSystem.AuthorizeAccess.Tests/PageTests/CheckAnswersTests.cs +++ b/TeachingRecordSystem/tests/TeachingRecordSystem.AuthorizeAccess.Tests/PageTests/CheckAnswersTests.cs @@ -1,4 +1,6 @@ using System.Diagnostics; +using TeachingRecordSystem.Core.DataStore.Postgres.Models; +using TeachingRecordSystem.Core.Models.SupportTaskData; namespace TeachingRecordSystem.AuthorizeAccess.Tests.PageTests; @@ -249,4 +251,70 @@ public async Task Post_AlreadyAuthenticated_RedirectsToStateRedirectUri() Assert.Equal(StatusCodes.Status302Found, (int)response.StatusCode); Assert.Equal($"{state.RedirectUri}?{journeyInstance.GetUniqueIdQueryParameter()}", response.Headers.Location?.OriginalString); } + + [Fact] + public async Task Post_ValidRequest_CreatesSupportTicketAndRedirectsToSupportRequestedSubmitted() + { + // Arrange + var state = CreateNewState(); + var journeyInstance = await CreateJourneyInstance(state); + + var oneLoginUser = await TestData.CreateOneLoginUser(verified: true); + + var ticket = CreateOneLoginAuthenticationTicket(vtr: SignInJourneyHelper.AuthenticationOnlyVtr, oneLoginUser); + await GetSignInJourneyHelper().OnUserAuthenticated(journeyInstance, ticket); + + var nationalInsuranceNumber = TestData.GenerateNationalInsuranceNumber(); + var trn = await TestData.GenerateTrn(); + + await journeyInstance.UpdateStateAsync(state => + { + state.SetNationalInsuranceNumber(true, nationalInsuranceNumber); + state.SetTrn(true, trn); + }); + + var request = new HttpRequestMessage(HttpMethod.Post, $"/check-answers?{journeyInstance.GetUniqueIdQueryParameter()}"); + + // Act + var response = await HttpClient.SendAsync(request); + + // Assert + Assert.Equal(StatusCodes.Status302Found, (int)response.StatusCode); + Assert.Equal($"/request-submitted?{journeyInstance.GetUniqueIdQueryParameter()}", response.Headers.Location?.OriginalString); + + var supportTask = await WithDbContext(dbContext => dbContext.SupportTasks.SingleAsync(t => t.OneLoginUserSubject == oneLoginUser.Subject)); + Assert.NotNull(supportTask); + Assert.Equal(Clock.UtcNow, supportTask.CreatedOn); + Assert.Equal(Clock.UtcNow, supportTask.UpdatedOn); + Assert.Equal(SupportTaskType.ConnectOneLoginUser, supportTask.SupportTaskType); + Assert.Equal(SupportTaskStatus.Open, supportTask.Status); + Assert.Equal(oneLoginUser.Subject, supportTask.OneLoginUserSubject); + var data = Assert.IsType(supportTask.Data); + Assert.True(data.Verified); + Assert.Equal(oneLoginUser.Subject, data.OneLoginUserSubject); + Assert.Equal(oneLoginUser.Email, data.OneLoginUserEmail); + Assert.Equal(oneLoginUser.VerifiedNames, data.VerifiedNames); + Assert.Equal(oneLoginUser.VerifiedDatesOfBirth, data.VerifiedDatesOfBirth); + Assert.Equal(nationalInsuranceNumber, data.StatedNationalInsuranceNumber); + Assert.Equal(trn, data.StatedTrn); + + EventObserver.AssertEventsSaved(e => + { + var supportTaskCreatedEvent = Assert.IsType(e); + Assert.Equal(Clock.UtcNow, supportTaskCreatedEvent.CreatedUtc); + Assert.Equal(supportTaskCreatedEvent.RaisedBy.UserId, SystemUser.SystemUserId); + Assert.Equal(supportTask.SupportTaskReference, supportTaskCreatedEvent.SupportTask.SupportTaskReference); + Assert.Equal(SupportTaskType.ConnectOneLoginUser, supportTaskCreatedEvent.SupportTask.SupportTaskType); + Assert.Equal(SupportTaskStatus.Open, supportTaskCreatedEvent.SupportTask.Status); + Assert.Equal(oneLoginUser.Subject, supportTaskCreatedEvent.SupportTask.OneLoginUserSubject); + var eventData = Assert.IsType(supportTask.Data); + Assert.True(eventData.Verified); + Assert.Equal(oneLoginUser.Subject, eventData.OneLoginUserSubject); + Assert.Equal(oneLoginUser.Email, eventData.OneLoginUserEmail); + Assert.Equal(oneLoginUser.VerifiedNames, eventData.VerifiedNames); + Assert.Equal(oneLoginUser.VerifiedDatesOfBirth, eventData.VerifiedDatesOfBirth); + Assert.Equal(nationalInsuranceNumber, eventData.StatedNationalInsuranceNumber); + Assert.Equal(trn, eventData.StatedTrn); + }); + } } diff --git a/TeachingRecordSystem/tests/TeachingRecordSystem.AuthorizeAccess.Tests/PageTests/SupportRequestSubmittedTests.cs b/TeachingRecordSystem/tests/TeachingRecordSystem.AuthorizeAccess.Tests/PageTests/SupportRequestSubmittedTests.cs new file mode 100644 index 000000000..fbf4de37f --- /dev/null +++ b/TeachingRecordSystem/tests/TeachingRecordSystem.AuthorizeAccess.Tests/PageTests/SupportRequestSubmittedTests.cs @@ -0,0 +1,175 @@ +using System.Diagnostics; + +namespace TeachingRecordSystem.AuthorizeAccess.Tests.PageTests; + +public class SupportRequestSubmittedTests(HostFixture hostFixture) : TestBase(hostFixture) +{ + [Fact] + public async Task Get_NotAuthenticatedWithOneLogin_ReturnsBadRequest() + { + // Arrange + var state = CreateNewState(); + var journeyInstance = await CreateJourneyInstance(state); + + var request = new HttpRequestMessage(HttpMethod.Get, $"/request-submitted?{journeyInstance.GetUniqueIdQueryParameter()}"); + + // Act + var response = await HttpClient.SendAsync(request); + + // Assert + Assert.Equal(StatusCodes.Status400BadRequest, (int)response.StatusCode); + } + + [Fact] + public async Task Get_NotVerifiedWithOneLogin_ReturnsBadRequest() + { + // Arrange + var state = CreateNewState(); + var journeyInstance = await CreateJourneyInstance(state); + + var ticket = CreateOneLoginAuthenticationTicket(vtr: SignInJourneyHelper.AuthenticationOnlyVtr, createCoreIdentityVc: false); + await GetSignInJourneyHelper().OnUserAuthenticated(journeyInstance, ticket); + + var request = new HttpRequestMessage(HttpMethod.Get, $"/request-submitted?{journeyInstance.GetUniqueIdQueryParameter()}"); + + // Act + var response = await HttpClient.SendAsync(request); + + // Assert + Assert.Equal(StatusCodes.Status400BadRequest, (int)response.StatusCode); + } + + [Fact] + public async Task Get_NationalInsuranceNumberNotSpecified_RedirectsToNationalInsuranceNumberPage() + { + // Arrange + var state = CreateNewState(); + var journeyInstance = await CreateJourneyInstance(state); + + var oneLoginUser = await TestData.CreateOneLoginUser(verified: true); + + var ticket = CreateOneLoginAuthenticationTicket(vtr: SignInJourneyHelper.AuthenticationOnlyVtr, oneLoginUser); + await GetSignInJourneyHelper().OnUserAuthenticated(journeyInstance, ticket); + + Debug.Assert(state.NationalInsuranceNumber is null); + + var request = new HttpRequestMessage(HttpMethod.Get, $"/request-submitted?{journeyInstance.GetUniqueIdQueryParameter()}"); + + // Act + var response = await HttpClient.SendAsync(request); + + // Assert + Assert.Equal(StatusCodes.Status302Found, (int)response.StatusCode); + Assert.Equal($"/national-insurance-number?{journeyInstance.GetUniqueIdQueryParameter()}", response.Headers.Location?.OriginalString); + } + + [Fact] + public async Task Get_TrnNotSpecified_RedirectsToTrnPage() + { + // Arrange + var state = CreateNewState(); + var journeyInstance = await CreateJourneyInstance(state); + + var oneLoginUser = await TestData.CreateOneLoginUser(verified: true); + + var ticket = CreateOneLoginAuthenticationTicket(vtr: SignInJourneyHelper.AuthenticationOnlyVtr, oneLoginUser); + await GetSignInJourneyHelper().OnUserAuthenticated(journeyInstance, ticket); + + Debug.Assert(state.NationalInsuranceNumber is null); + await journeyInstance.UpdateStateAsync(state => state.SetNationalInsuranceNumber(true, TestData.GenerateNationalInsuranceNumber())); + + var request = new HttpRequestMessage(HttpMethod.Get, $"/request-submitted?{journeyInstance.GetUniqueIdQueryParameter()}"); + + // Act + var response = await HttpClient.SendAsync(request); + + // Assert + Assert.Equal(StatusCodes.Status302Found, (int)response.StatusCode); + Assert.Equal($"/trn?{journeyInstance.GetUniqueIdQueryParameter()}", response.Headers.Location?.OriginalString); + } + + [Fact] + public async Task Get_AlreadyAuthenticated_RedirectsToStateRedirectUri() + { + // Arrange + var state = CreateNewState(); + var journeyInstance = await CreateJourneyInstance(state); + + var person = await TestData.CreatePerson(b => b.WithTrn()); + var oneLoginUser = await TestData.CreateOneLoginUser(person); + + var ticket = CreateOneLoginAuthenticationTicket(vtr: SignInJourneyHelper.AuthenticationOnlyVtr, oneLoginUser); + await GetSignInJourneyHelper().OnUserAuthenticated(journeyInstance, ticket); + + var request = new HttpRequestMessage(HttpMethod.Get, $"/request-submitted?{journeyInstance.GetUniqueIdQueryParameter()}"); + + // Act + var response = await HttpClient.SendAsync(request); + + // Assert + Assert.Equal(StatusCodes.Status302Found, (int)response.StatusCode); + Assert.Equal($"{state.RedirectUri}?{journeyInstance.GetUniqueIdQueryParameter()}", response.Headers.Location?.OriginalString); + } + + [Fact] + public async Task Get_SupportTicketNotCreated_RedirectsToCheckAnswersPage() + { + // Arrange + var state = CreateNewState(); + var journeyInstance = await CreateJourneyInstance(state); + + var oneLoginUser = await TestData.CreateOneLoginUser(verified: true); + + var ticket = CreateOneLoginAuthenticationTicket(vtr: SignInJourneyHelper.AuthenticationOnlyVtr, oneLoginUser); + await GetSignInJourneyHelper().OnUserAuthenticated(journeyInstance, ticket); + + var nationalInsuranceNumber = TestData.GenerateNationalInsuranceNumber(); + var trn = await TestData.GenerateTrn(); + + await journeyInstance.UpdateStateAsync(state => + { + state.SetNationalInsuranceNumber(true, nationalInsuranceNumber); + state.SetTrn(true, trn); + }); + + var request = new HttpRequestMessage(HttpMethod.Get, $"/request-submitted?{journeyInstance.GetUniqueIdQueryParameter()}"); + + // Act + var response = await HttpClient.SendAsync(request); + + // Assert + Assert.Equal(StatusCodes.Status302Found, (int)response.StatusCode); + Assert.Equal($"/check-answers?{journeyInstance.GetUniqueIdQueryParameter()}", response.Headers.Location?.OriginalString); + } + + [Fact] + public async Task Get_ValidRequest_ReturnsExpectedContent() + { + // Arrange + var state = CreateNewState(); + var journeyInstance = await CreateJourneyInstance(state); + + var oneLoginUser = await TestData.CreateOneLoginUser(verified: true); + + var ticket = CreateOneLoginAuthenticationTicket(vtr: SignInJourneyHelper.AuthenticationOnlyVtr, oneLoginUser); + await GetSignInJourneyHelper().OnUserAuthenticated(journeyInstance, ticket); + + var nationalInsuranceNumber = TestData.GenerateNationalInsuranceNumber(); + var trn = await TestData.GenerateTrn(); + + await journeyInstance.UpdateStateAsync(state => + { + state.SetNationalInsuranceNumber(true, nationalInsuranceNumber); + state.SetTrn(true, trn); + state.HasPendingSupportRequest = true; + }); + + var request = new HttpRequestMessage(HttpMethod.Get, $"/request-submitted?{journeyInstance.GetUniqueIdQueryParameter()}"); + + // Act + var response = await HttpClient.SendAsync(request); + + // Assert + await AssertEx.HtmlResponse(response); + } +} diff --git a/TeachingRecordSystem/tests/TeachingRecordSystem.AuthorizeAccess.Tests/TestBase.cs b/TeachingRecordSystem/tests/TeachingRecordSystem.AuthorizeAccess.Tests/TestBase.cs index 8b8764648..0f9f57cc3 100644 --- a/TeachingRecordSystem/tests/TeachingRecordSystem.AuthorizeAccess.Tests/TestBase.cs +++ b/TeachingRecordSystem/tests/TeachingRecordSystem.AuthorizeAccess.Tests/TestBase.cs @@ -43,6 +43,8 @@ protected TestBase(HostFixture hostFixture) public HostFixture HostFixture { get; } + public CaptureEventObserver EventObserver => _testServices.EventObserver; + public TestableClock Clock => _testServices.Clock; public HttpClient HttpClient { get; }