diff --git a/TeachingRecordSystem/src/TeachingRecordSystem.Core/DataStore/Postgres/Mappings/PersonEmploymentMapping.cs b/TeachingRecordSystem/src/TeachingRecordSystem.Core/DataStore/Postgres/Mappings/PersonEmploymentMapping.cs new file mode 100644 index 000000000..8e8d8d0c8 --- /dev/null +++ b/TeachingRecordSystem/src/TeachingRecordSystem.Core/DataStore/Postgres/Mappings/PersonEmploymentMapping.cs @@ -0,0 +1,21 @@ +using Microsoft.EntityFrameworkCore.Metadata.Builders; +using TeachingRecordSystem.Core.DataStore.Postgres.Models; +using Establishment = TeachingRecordSystem.Core.DataStore.Postgres.Models.Establishment; + +namespace TeachingRecordSystem.Core.DataStore.Postgres.Mappings; + +public class PersonEmploymentMapping : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable("person_employments"); + builder.HasKey(e => e.PersonEmploymentId); + builder.Property(e => e.EstablishmentId).IsRequired(); + builder.Property(e => e.StartDate).IsRequired(); + builder.Property(e => e.EmploymentType).IsRequired(); + builder.Property(e => e.CreatedOn).IsRequired(); + builder.Property(e => e.UpdatedOn).IsRequired(); + builder.HasOne().WithMany().HasForeignKey(e => e.PersonId).HasConstraintName("fk_person_employments_person_id"); + builder.HasOne().WithMany().HasForeignKey(e => e.EstablishmentId).HasConstraintName("fk_person_employments_establishment_id"); + } +} diff --git a/TeachingRecordSystem/src/TeachingRecordSystem.Core/DataStore/Postgres/Mappings/TpsCsvExtractItemMapping.cs b/TeachingRecordSystem/src/TeachingRecordSystem.Core/DataStore/Postgres/Mappings/TpsCsvExtractItemMapping.cs new file mode 100644 index 000000000..86e333ee5 --- /dev/null +++ b/TeachingRecordSystem/src/TeachingRecordSystem.Core/DataStore/Postgres/Mappings/TpsCsvExtractItemMapping.cs @@ -0,0 +1,33 @@ +using Microsoft.EntityFrameworkCore.Metadata.Builders; +using TeachingRecordSystem.Core.DataStore.Postgres.Models; + +namespace TeachingRecordSystem.Core.DataStore.Postgres.Mappings; + +public class TpsCsvExtractItemMapping : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable("tps_csv_extract_items"); + builder.HasKey(x => x.TpsCsvExtractItemId); + builder.Property(x => x.Trn).HasMaxLength(7).IsFixedLength().IsRequired(); + builder.Property(x => x.DateOfBirth).IsRequired(); + builder.Property(x => x.NationalInsuranceNumber).HasMaxLength(9).IsFixedLength().IsRequired(); + builder.Property(x => x.MemberPostcode).HasMaxLength(10); + builder.Property(x => x.MemberEmailAddress).HasMaxLength(200); + builder.Property(x => x.LocalAuthorityCode).HasMaxLength(3).IsFixedLength().IsRequired(); + builder.Property(x => x.EstablishmentNumber).HasMaxLength(4).IsFixedLength(); + builder.Property(x => x.EstablishmentPostcode).HasMaxLength(10); + builder.Property(x => x.EstablishmentEmailAddress).HasMaxLength(200); + builder.Property(x => x.EmploymentStartDate).IsRequired(); + builder.Property(x => x.EmploymentType).IsRequired(); + builder.Property(x => x.WithdrawlIndicator).HasMaxLength(1).IsFixedLength(); + builder.Property(x => x.Gender).HasMaxLength(10).IsRequired(); + builder.Property(x => x.Created).IsRequired(); + builder.HasIndex(x => x.Trn).HasDatabaseName(TpsCsvExtractItem.TrnIndexName); + builder.HasIndex(x => new { x.LocalAuthorityCode, x.EstablishmentNumber }).HasDatabaseName(TpsCsvExtractItem.LaCodeEstablishmentNumberIndexName); + builder.HasIndex(x => x.TpsCsvExtractId).HasDatabaseName(TpsCsvExtractItem.TpsCsvExtractIdIndexName); + builder.HasIndex(x => x.TpsCsvExtractLoadItemId).HasDatabaseName(TpsCsvExtractItem.TpsCsvExtractLoadItemIdIndexName); + builder.HasOne().WithMany().HasForeignKey(x => x.TpsCsvExtractId).HasConstraintName(TpsCsvExtractItem.TpsCsvExtractForeignKeyName); + builder.HasOne().WithMany().HasForeignKey(x => x.TpsCsvExtractLoadItemId).HasConstraintName(TpsCsvExtractItem.TpsCsvExtractLoadItemIdForeignKeyName); + } +} diff --git a/TeachingRecordSystem/src/TeachingRecordSystem.Core/DataStore/Postgres/Mappings/TpsCsvExtractLoadItemMapping.cs b/TeachingRecordSystem/src/TeachingRecordSystem.Core/DataStore/Postgres/Mappings/TpsCsvExtractLoadItemMapping.cs new file mode 100644 index 000000000..401adebe1 --- /dev/null +++ b/TeachingRecordSystem/src/TeachingRecordSystem.Core/DataStore/Postgres/Mappings/TpsCsvExtractLoadItemMapping.cs @@ -0,0 +1,33 @@ +using Microsoft.EntityFrameworkCore.Metadata.Builders; +using TeachingRecordSystem.Core.DataStore.Postgres.Models; + +namespace TeachingRecordSystem.Core.DataStore.Postgres.Mappings; + +public class TpsCsvExtractLoadItemMapping : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable("tps_csv_extract_load_items"); + builder.HasKey(x => x.TpsCsvExtractLoadItemId); + builder.Property(x => x.TpsCsvExtractId).IsRequired(); + builder.Property(x => x.Trn).HasMaxLength(TpsCsvExtractLoadItem.FieldMaxLength); + builder.Property(x => x.NationalInsuranceNumber).HasMaxLength(TpsCsvExtractLoadItem.FieldMaxLength); + builder.Property(x => x.DateOfBirth).HasMaxLength(TpsCsvExtractLoadItem.FieldMaxLength); + builder.Property(x => x.DateOfDeath).HasMaxLength(TpsCsvExtractLoadItem.FieldMaxLength); + builder.Property(x => x.MemberPostcode).HasMaxLength(TpsCsvExtractLoadItem.FieldMaxLength); + builder.Property(x => x.MemberEmailAddress).HasMaxLength(TpsCsvExtractLoadItem.FieldMaxLength); + builder.Property(x => x.LocalAuthorityCode).HasMaxLength(TpsCsvExtractLoadItem.FieldMaxLength); + builder.Property(x => x.EstablishmentNumber).HasMaxLength(TpsCsvExtractLoadItem.FieldMaxLength); + builder.Property(x => x.EstablishmentPostcode).HasMaxLength(TpsCsvExtractLoadItem.FieldMaxLength); + builder.Property(x => x.EstablishmentEmailAddress).HasMaxLength(TpsCsvExtractLoadItem.FieldMaxLength); + builder.Property(x => x.MemberId).HasMaxLength(TpsCsvExtractLoadItem.FieldMaxLength); + builder.Property(x => x.EmploymentStartDate).HasMaxLength(TpsCsvExtractLoadItem.FieldMaxLength); + builder.Property(x => x.EmploymentEndDate).HasMaxLength(TpsCsvExtractLoadItem.FieldMaxLength); + builder.Property(x => x.FullOrPartTimeIndicator).HasMaxLength(TpsCsvExtractLoadItem.FieldMaxLength); + builder.Property(x => x.WithdrawlIndicator).HasMaxLength(TpsCsvExtractLoadItem.FieldMaxLength); + builder.Property(x => x.ExtractDate).HasMaxLength(TpsCsvExtractLoadItem.FieldMaxLength); + builder.Property(x => x.Gender).HasMaxLength(TpsCsvExtractLoadItem.FieldMaxLength); + builder.Property(x => x.Created).IsRequired(); + builder.HasOne().WithMany().HasForeignKey(x => x.TpsCsvExtractId).HasConstraintName(TpsCsvExtractLoadItem.TpsCsvExtractForeignKeyName); + } +} diff --git a/TeachingRecordSystem/src/TeachingRecordSystem.Core/DataStore/Postgres/Mappings/TpsCsvExtractMapping.cs b/TeachingRecordSystem/src/TeachingRecordSystem.Core/DataStore/Postgres/Mappings/TpsCsvExtractMapping.cs new file mode 100644 index 000000000..4f106db5a --- /dev/null +++ b/TeachingRecordSystem/src/TeachingRecordSystem.Core/DataStore/Postgres/Mappings/TpsCsvExtractMapping.cs @@ -0,0 +1,15 @@ +using Microsoft.EntityFrameworkCore.Metadata.Builders; +using TeachingRecordSystem.Core.DataStore.Postgres.Models; + +namespace TeachingRecordSystem.Core.DataStore.Postgres.Mappings; + +public class TpsCsvExtractMapping : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable("tps_csv_extracts"); + builder.HasKey(e => e.TpsCsvExtractId); + builder.Property(e => e.Filename).IsRequired().HasMaxLength(TpsCsvExtract.FilenameMaxLength); + builder.Property(e => e.CreatedOn).IsRequired(); + } +} diff --git a/TeachingRecordSystem/src/TeachingRecordSystem.Core/DataStore/Postgres/Migrations/20240312125623_TpsCsvExtractAndPersonEmployment.Designer.cs b/TeachingRecordSystem/src/TeachingRecordSystem.Core/DataStore/Postgres/Migrations/20240312125623_TpsCsvExtractAndPersonEmployment.Designer.cs new file mode 100644 index 000000000..e19f22c99 --- /dev/null +++ b/TeachingRecordSystem/src/TeachingRecordSystem.Core/DataStore/Postgres/Migrations/20240312125623_TpsCsvExtractAndPersonEmployment.Designer.cs @@ -0,0 +1,1668 @@ +// +using System; +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("20240312125623_TpsCsvExtractAndPersonEmployment")] + partial class TpsCsvExtractAndPersonEmployment + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.0") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + 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("EstablishmentStatusCode") + .HasColumnType("integer") + .HasColumnName("establishment_status_code"); + + b.Property("EstablishmentStatusName") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("establishment_status_name"); + + b.Property("EstablishmentTypeCode") + .IsRequired() + .HasMaxLength(3) + .HasColumnType("character varying(3)") + .HasColumnName("establishment_type_code"); + + b.Property("EstablishmentTypeGroupCode") + .HasColumnType("integer") + .HasColumnName("establishment_type_group_code"); + + b.Property("EstablishmentTypeGroupName") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("establishment_type_group_name"); + + b.Property("EstablishmentTypeName") + .IsRequired() + .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") + .IsRequired() + .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("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.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(200) + .HasColumnType("character varying(200)") + .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("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.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.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.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") + .IsRequired() + .HasColumnType("varchar[]") + .HasColumnName("api_roles"); + + 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.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("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.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") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .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.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("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/20240312125623_TpsCsvExtractAndPersonEmployment.cs b/TeachingRecordSystem/src/TeachingRecordSystem.Core/DataStore/Postgres/Migrations/20240312125623_TpsCsvExtractAndPersonEmployment.cs new file mode 100644 index 000000000..def73c678 --- /dev/null +++ b/TeachingRecordSystem/src/TeachingRecordSystem.Core/DataStore/Postgres/Migrations/20240312125623_TpsCsvExtractAndPersonEmployment.cs @@ -0,0 +1,175 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace TeachingRecordSystem.Core.DataStore.Postgres.Migrations +{ + /// + public partial class TpsCsvExtractAndPersonEmployment : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "person_employments", + columns: table => new + { + person_employment_id = table.Column(type: "uuid", nullable: false), + person_id = table.Column(type: "uuid", nullable: false), + establishment_id = table.Column(type: "uuid", nullable: false), + start_date = table.Column(type: "date", nullable: false), + end_date = table.Column(type: "date", nullable: true), + employment_type = table.Column(type: "integer", nullable: false), + created_on = table.Column(type: "timestamp with time zone", nullable: false), + updated_on = table.Column(type: "timestamp with time zone", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("pk_person_employments", x => x.person_employment_id); + table.ForeignKey( + name: "fk_person_employments_establishment_id", + column: x => x.establishment_id, + principalTable: "establishments", + principalColumn: "establishment_id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "fk_person_employments_person_id", + column: x => x.person_id, + principalTable: "persons", + principalColumn: "person_id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "tps_csv_extracts", + columns: table => new + { + tps_csv_extract_id = table.Column(type: "uuid", nullable: false), + filename = table.Column(type: "character varying(200)", maxLength: 200, nullable: false), + created_on = table.Column(type: "timestamp with time zone", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("pk_tps_csv_extracts", x => x.tps_csv_extract_id); + }); + + migrationBuilder.CreateTable( + name: "tps_csv_extract_load_items", + columns: table => new + { + tps_csv_extract_load_item_id = table.Column(type: "uuid", nullable: false), + tps_csv_extract_id = table.Column(type: "uuid", nullable: false), + trn = table.Column(type: "character varying(200)", maxLength: 200, nullable: true), + national_insurance_number = table.Column(type: "character varying(200)", maxLength: 200, nullable: true), + date_of_birth = table.Column(type: "character varying(200)", maxLength: 200, nullable: true), + date_of_death = table.Column(type: "character varying(200)", maxLength: 200, nullable: true), + member_postcode = table.Column(type: "character varying(200)", maxLength: 200, nullable: true), + member_email_address = table.Column(type: "character varying(200)", maxLength: 200, nullable: true), + local_authority_code = table.Column(type: "character varying(200)", maxLength: 200, nullable: true), + establishment_number = table.Column(type: "character varying(200)", maxLength: 200, nullable: true), + establishment_postcode = table.Column(type: "character varying(200)", maxLength: 200, nullable: true), + establishment_email_address = table.Column(type: "character varying(200)", maxLength: 200, nullable: true), + member_id = table.Column(type: "character varying(200)", maxLength: 200, nullable: true), + employment_start_date = table.Column(type: "character varying(200)", maxLength: 200, nullable: true), + employment_end_date = table.Column(type: "character varying(200)", maxLength: 200, nullable: true), + full_or_part_time_indicator = table.Column(type: "character varying(200)", maxLength: 200, nullable: true), + withdrawl_indicator = table.Column(type: "character varying(200)", maxLength: 200, nullable: true), + extract_date = table.Column(type: "character varying(200)", maxLength: 200, nullable: true), + gender = table.Column(type: "character varying(200)", maxLength: 200, nullable: true), + created = table.Column(type: "timestamp with time zone", nullable: false), + errors = table.Column(type: "integer", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("pk_tps_csv_extract_load_items", x => x.tps_csv_extract_load_item_id); + table.ForeignKey( + name: "fk_tps_csv_extract_load_items_tps_csv_extract_id", + column: x => x.tps_csv_extract_id, + principalTable: "tps_csv_extracts", + principalColumn: "tps_csv_extract_id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "tps_csv_extract_items", + columns: table => new + { + tps_csv_extract_item_id = table.Column(type: "uuid", nullable: false), + tps_csv_extract_id = table.Column(type: "uuid", nullable: false), + tps_csv_extract_load_item_id = table.Column(type: "uuid", nullable: false), + trn = table.Column(type: "character(7)", fixedLength: true, maxLength: 7, nullable: false), + national_insurance_number = table.Column(type: "character(9)", fixedLength: true, maxLength: 9, nullable: false), + date_of_birth = table.Column(type: "date", nullable: false), + date_of_death = table.Column(type: "date", nullable: true), + gender = table.Column(type: "character varying(10)", maxLength: 10, nullable: false), + member_postcode = table.Column(type: "character varying(10)", maxLength: 10, nullable: true), + member_email_address = table.Column(type: "character varying(200)", maxLength: 200, nullable: true), + local_authority_code = table.Column(type: "character(3)", fixedLength: true, maxLength: 3, nullable: false), + establishment_number = table.Column(type: "character(4)", fixedLength: true, maxLength: 4, nullable: true), + establishment_postcode = table.Column(type: "character varying(10)", maxLength: 10, nullable: true), + establishment_email_address = table.Column(type: "character varying(200)", maxLength: 200, nullable: true), + member_id = table.Column(type: "integer", nullable: true), + employment_start_date = table.Column(type: "date", nullable: false), + employment_end_date = table.Column(type: "date", nullable: true), + employment_type = table.Column(type: "integer", nullable: false), + withdrawl_indicator = table.Column(type: "character(1)", fixedLength: true, maxLength: 1, nullable: true), + extract_date = table.Column(type: "date", nullable: false), + created = table.Column(type: "timestamp with time zone", nullable: false), + result = table.Column(type: "integer", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("pk_tps_csv_extract_items", x => x.tps_csv_extract_item_id); + table.ForeignKey( + name: "fk_tps_csv_extract_items_tps_csv_extract_id", + column: x => x.tps_csv_extract_id, + principalTable: "tps_csv_extracts", + principalColumn: "tps_csv_extract_id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "fk_tps_csv_extract_items_tps_csv_extract_load_item_id", + column: x => x.tps_csv_extract_load_item_id, + principalTable: "tps_csv_extract_load_items", + principalColumn: "tps_csv_extract_load_item_id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "ix_tps_csv_extract_items_la_code_establishment_number", + table: "tps_csv_extract_items", + columns: new[] { "local_authority_code", "establishment_number" }); + + migrationBuilder.CreateIndex( + name: "ix_tps_csv_extract_items_tps_csv_extract_id", + table: "tps_csv_extract_items", + column: "tps_csv_extract_id"); + + migrationBuilder.CreateIndex( + name: "ix_tps_csv_extract_items_tps_csv_extract_load_item_id", + table: "tps_csv_extract_items", + column: "tps_csv_extract_load_item_id"); + + migrationBuilder.CreateIndex( + name: "ix_tps_csv_extract_items_trn", + table: "tps_csv_extract_items", + column: "trn"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "person_employments"); + + migrationBuilder.DropTable( + name: "tps_csv_extract_items"); + + migrationBuilder.DropTable( + name: "tps_csv_extract_load_items"); + + migrationBuilder.DropTable( + name: "tps_csv_extracts"); + } + } +} diff --git a/TeachingRecordSystem/src/TeachingRecordSystem.Core/DataStore/Postgres/Migrations/TrsDbContextModelSnapshot.cs b/TeachingRecordSystem/src/TeachingRecordSystem.Core/DataStore/Postgres/Migrations/TrsDbContextModelSnapshot.cs index a288c9a10..76d24c426 100644 --- a/TeachingRecordSystem/src/TeachingRecordSystem.Core/DataStore/Postgres/Migrations/TrsDbContextModelSnapshot.cs +++ b/TeachingRecordSystem/src/TeachingRecordSystem.Core/DataStore/Postgres/Migrations/TrsDbContextModelSnapshot.cs @@ -806,6 +806,47 @@ protected override void BuildModel(ModelBuilder modelBuilder) 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.ToTable("person_employments", (string)null); + }); + modelBuilder.Entity("TeachingRecordSystem.Core.DataStore.Postgres.Models.PersonSearchAttribute", b => { b.Property("PersonSearchAttributeId") @@ -992,6 +1033,267 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.UseTphMappingStrategy(); }); + 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") @@ -1257,6 +1559,23 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Navigation("Person"); }); + modelBuilder.Entity("TeachingRecordSystem.Core.DataStore.Postgres.Models.PersonEmployment", b => + { + b.HasOne("TeachingRecordSystem.Core.DataStore.Postgres.Models.Establishment", null) + .WithMany() + .HasForeignKey("EstablishmentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .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") @@ -1279,6 +1598,33 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasConstraintName("fk_qualifications_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") diff --git a/TeachingRecordSystem/src/TeachingRecordSystem.Core/DataStore/Postgres/Models/PersonEmployment.cs b/TeachingRecordSystem/src/TeachingRecordSystem.Core/DataStore/Postgres/Models/PersonEmployment.cs new file mode 100644 index 000000000..e88def402 --- /dev/null +++ b/TeachingRecordSystem/src/TeachingRecordSystem.Core/DataStore/Postgres/Models/PersonEmployment.cs @@ -0,0 +1,16 @@ +namespace TeachingRecordSystem.Core.DataStore.Postgres.Models; + +public class PersonEmployment +{ + public const string PersonIdIndexName = "ix_person_employments_person_id"; + public const string EstablishmentIdIndexName = "ix_person_employments_establishment_id"; + + public required Guid PersonEmploymentId { get; set; } + public required Guid PersonId { get; set; } + public required Guid EstablishmentId { get; set; } + public required DateOnly StartDate { get; set; } + public required DateOnly? EndDate { get; set; } + public required EmploymentType EmploymentType { get; set; } + public required DateTime CreatedOn { get; set; } + public required DateTime UpdatedOn { get; set; } +} diff --git a/TeachingRecordSystem/src/TeachingRecordSystem.Core/DataStore/Postgres/Models/TpsCsvExtract.cs b/TeachingRecordSystem/src/TeachingRecordSystem.Core/DataStore/Postgres/Models/TpsCsvExtract.cs new file mode 100644 index 000000000..0ca217c10 --- /dev/null +++ b/TeachingRecordSystem/src/TeachingRecordSystem.Core/DataStore/Postgres/Models/TpsCsvExtract.cs @@ -0,0 +1,10 @@ +namespace TeachingRecordSystem.Core.DataStore.Postgres.Models; + +public class TpsCsvExtract +{ + public const int FilenameMaxLength = 200; + + public required Guid TpsCsvExtractId { get; set; } + public required string Filename { get; set; } + public required DateTime CreatedOn { get; set; } +} diff --git a/TeachingRecordSystem/src/TeachingRecordSystem.Core/DataStore/Postgres/Models/TpsCsvExtractItem.cs b/TeachingRecordSystem/src/TeachingRecordSystem.Core/DataStore/Postgres/Models/TpsCsvExtractItem.cs new file mode 100644 index 000000000..fc6e7c428 --- /dev/null +++ b/TeachingRecordSystem/src/TeachingRecordSystem.Core/DataStore/Postgres/Models/TpsCsvExtractItem.cs @@ -0,0 +1,34 @@ +namespace TeachingRecordSystem.Core.DataStore.Postgres.Models; + +public class TpsCsvExtractItem +{ + public const string TrnIndexName = "ix_tps_csv_extract_items_trn"; + public const string LaCodeEstablishmentNumberIndexName = "ix_tps_csv_extract_items_la_code_establishment_number"; + public const string TpsCsvExtractIdIndexName = "ix_tps_csv_extract_items_tps_csv_extract_id"; + public const string TpsCsvExtractForeignKeyName = "fk_tps_csv_extract_items_tps_csv_extract_id"; + public const string TpsCsvExtractLoadItemIdIndexName = "ix_tps_csv_extract_items_tps_csv_extract_load_item_id"; + public const string TpsCsvExtractLoadItemIdForeignKeyName = "fk_tps_csv_extract_items_tps_csv_extract_load_item_id"; + + public required Guid TpsCsvExtractItemId { get; set; } + public required Guid TpsCsvExtractId { get; set; } + public required Guid TpsCsvExtractLoadItemId { get; set; } + public required string Trn { get; set; } + public required string NationalInsuranceNumber { get; set; } + public required DateOnly DateOfBirth { get; set; } + public required DateOnly? DateOfDeath { get; set; } + public required string Gender { get; set; } + public required string? MemberPostcode { get; set; } + public required string? MemberEmailAddress { get; set; } + public required string LocalAuthorityCode { get; set; } + public required string? EstablishmentNumber { get; set; } + public required string? EstablishmentPostcode { get; set; } + public required string? EstablishmentEmailAddress { get; set; } + public required int? MemberId { get; set; } + public required DateOnly EmploymentStartDate { get; set; } + public required DateOnly? EmploymentEndDate { get; set; } + public required EmploymentType EmploymentType { get; set; } + public required string? WithdrawlIndicator { get; set; } + public required DateOnly ExtractDate { get; set; } + public required DateTime Created { get; set; } + public required TpsCsvExtractItemResult? Result { get; set; } +} diff --git a/TeachingRecordSystem/src/TeachingRecordSystem.Core/DataStore/Postgres/Models/TpsCsvExtractItemLoadErrors.cs b/TeachingRecordSystem/src/TeachingRecordSystem.Core/DataStore/Postgres/Models/TpsCsvExtractItemLoadErrors.cs new file mode 100644 index 000000000..d2ad5a7e0 --- /dev/null +++ b/TeachingRecordSystem/src/TeachingRecordSystem.Core/DataStore/Postgres/Models/TpsCsvExtractItemLoadErrors.cs @@ -0,0 +1,24 @@ +namespace TeachingRecordSystem.Core.DataStore.Postgres.Models; + +[Flags] +public enum TpsCsvExtractItemLoadErrors +{ + None = 0, + + TrnIncorrectFormat = 1 << 0, + NationalInsuranceNumberIncorrectFormat = 1 << 1, + DateOfBirthIncorrectFormat = 1 << 2, + DateOfDeathIncorrectFormat = 1 << 3, + MemberPostcodeIncorrectFormat = 1 << 4, + MemberEmailAddressIncorrectFormat = 1 << 5, + LocalAuthorityCodeIncorrectFormat = 1 << 6, + EstablishmentNumberIncorrectFormat = 1 << 7, + EstablishmentPostcodeIncorrectFormat = 1 << 8, + MemberIdIncorrectFormat = 1 << 9, + EmploymentStartDateIncorrectFormat = 1 << 10, + EmploymentEndDateIncorrectFormat = 1 << 11, + FullOrPartTimeIndicatorIncorrectFormat = 1 << 12, + WithdrawlIndicatorIncorrectFormat = 1 << 13, + ExtractDateIncorrectFormat = 1 << 14, + GenderIncorrectFormat = 1 << 15 +} diff --git a/TeachingRecordSystem/src/TeachingRecordSystem.Core/DataStore/Postgres/Models/TpsCsvExtractItemResult.cs b/TeachingRecordSystem/src/TeachingRecordSystem.Core/DataStore/Postgres/Models/TpsCsvExtractItemResult.cs new file mode 100644 index 000000000..a08df55c3 --- /dev/null +++ b/TeachingRecordSystem/src/TeachingRecordSystem.Core/DataStore/Postgres/Models/TpsCsvExtractItemResult.cs @@ -0,0 +1,10 @@ +namespace TeachingRecordSystem.Core.DataStore.Postgres.Models; + +public enum TpsCsvExtractItemResult +{ + ValidNoChange = 0, + ValidDataAdded = 1, + ValidDataUpdated = 2, + InvalidTrn = 3, + InvalidEstablishment = 4 +} diff --git a/TeachingRecordSystem/src/TeachingRecordSystem.Core/DataStore/Postgres/Models/TpsCsvExtractLoadItem.cs b/TeachingRecordSystem/src/TeachingRecordSystem.Core/DataStore/Postgres/Models/TpsCsvExtractLoadItem.cs new file mode 100644 index 000000000..aba47efe0 --- /dev/null +++ b/TeachingRecordSystem/src/TeachingRecordSystem.Core/DataStore/Postgres/Models/TpsCsvExtractLoadItem.cs @@ -0,0 +1,30 @@ +namespace TeachingRecordSystem.Core.DataStore.Postgres.Models; + +public class TpsCsvExtractLoadItem +{ + public const int FieldMaxLength = 200; + public const string TpsCsvExtractIdIndexName = "ix_tps_csv_extract_load_items_tps_csv_extract_id"; + public const string TpsCsvExtractForeignKeyName = "fk_tps_csv_extract_load_items_tps_csv_extract_id"; + + public required Guid TpsCsvExtractLoadItemId { get; set; } + public required Guid TpsCsvExtractId { get; set; } + public required string? Trn { get; set; } + public required string? NationalInsuranceNumber { get; set; } + public required string? DateOfBirth { get; set; } + public required string? DateOfDeath { get; set; } + public required string? MemberPostcode { get; set; } + public required string? MemberEmailAddress { get; set; } + public required string? LocalAuthorityCode { get; set; } + public required string? EstablishmentNumber { get; set; } + public required string? EstablishmentPostcode { get; set; } + public required string? EstablishmentEmailAddress { get; set; } + public required string? MemberId { get; set; } + public required string? EmploymentStartDate { get; set; } + public required string? EmploymentEndDate { get; set; } + public required string? FullOrPartTimeIndicator { get; set; } + public required string? WithdrawlIndicator { get; set; } + public required string? ExtractDate { get; set; } + public required string? Gender { get; set; } + public required DateTime Created { get; set; } + public required TpsCsvExtractItemLoadErrors? Errors { get; set; } +} diff --git a/TeachingRecordSystem/src/TeachingRecordSystem.Core/DataStore/Postgres/TrsDbContext.cs b/TeachingRecordSystem/src/TeachingRecordSystem.Core/DataStore/Postgres/TrsDbContext.cs index 231a46b0c..c4b9365d1 100644 --- a/TeachingRecordSystem/src/TeachingRecordSystem.Core/DataStore/Postgres/TrsDbContext.cs +++ b/TeachingRecordSystem/src/TeachingRecordSystem.Core/DataStore/Postgres/TrsDbContext.cs @@ -62,6 +62,14 @@ public static TrsDbContext Create(string connectionString, int? commandTimeout = public DbSet Establishments => Set(); + public DbSet TpsCsvExtracts => Set(); + + public DbSet TpsCsvExtractLoadItems => Set(); + + public DbSet TpsCsvExtractItems => Set(); + + public DbSet PersonEmployments => Set(); + public static void ConfigureOptions(DbContextOptionsBuilder optionsBuilder, string connectionString, int? commandTimeout = null) { Action configureOptions = o => o.CommandTimeout(commandTimeout); diff --git a/TeachingRecordSystem/src/TeachingRecordSystem.Core/Events/Models/PersonEmployment.cs b/TeachingRecordSystem/src/TeachingRecordSystem.Core/Events/Models/PersonEmployment.cs new file mode 100644 index 000000000..bcad4ab19 --- /dev/null +++ b/TeachingRecordSystem/src/TeachingRecordSystem.Core/Events/Models/PersonEmployment.cs @@ -0,0 +1,21 @@ +namespace TeachingRecordSystem.Core.Events.Models; + +public record PersonEmployment +{ + public required Guid PersonEmploymentId { get; init; } + public required Guid PersonId { get; init; } + public required Guid EstablishmentId { get; init; } + public required DateOnly StartDate { get; init; } + public required DateOnly? EndDate { get; init; } + public required EmploymentType EmploymentType { get; init; } + + public static PersonEmployment FromModel(DataStore.Postgres.Models.PersonEmployment model) => new() + { + PersonEmploymentId = model.PersonEmploymentId, + PersonId = model.PersonId, + EstablishmentId = model.EstablishmentId, + StartDate = model.StartDate, + EndDate = model.EndDate, + EmploymentType = model.EmploymentType + }; +} diff --git a/TeachingRecordSystem/src/TeachingRecordSystem.Core/Events/PersonEmploymentCreatedEvent.cs b/TeachingRecordSystem/src/TeachingRecordSystem.Core/Events/PersonEmploymentCreatedEvent.cs new file mode 100644 index 000000000..5de809a3d --- /dev/null +++ b/TeachingRecordSystem/src/TeachingRecordSystem.Core/Events/PersonEmploymentCreatedEvent.cs @@ -0,0 +1,9 @@ +using TeachingRecordSystem.Core.Events.Models; + +namespace TeachingRecordSystem.Core.Events; + +public record PersonEmploymentCreatedEvent : EventBase, IEventWithPersonId +{ + public required Guid PersonId { get; init; } + public required PersonEmployment PersonEmployment { get; init; } +} diff --git a/TeachingRecordSystem/src/TeachingRecordSystem.Core/Events/PersonEmploymentUpdatedEvent.cs b/TeachingRecordSystem/src/TeachingRecordSystem.Core/Events/PersonEmploymentUpdatedEvent.cs new file mode 100644 index 000000000..d06952b49 --- /dev/null +++ b/TeachingRecordSystem/src/TeachingRecordSystem.Core/Events/PersonEmploymentUpdatedEvent.cs @@ -0,0 +1,21 @@ +using TeachingRecordSystem.Core.Events.Models; + +namespace TeachingRecordSystem.Core.Events; + +public record PersonEmploymentUpdatedEvent : EventBase, IEventWithPersonId +{ + public required Guid PersonId { get; init; } + public required PersonEmployment PersonEmployment { get; init; } + public required PersonEmployment OldPersonEmployment { get; init; } + public required PersonEmploymentUpdatedEventChanges Changes { get; init; } +} + +[Flags] +public enum PersonEmploymentUpdatedEventChanges +{ + None = 0, + StartDate = 1 << 0, + EndDate = 1 << 1, + EmploymentType = 1 << 2, + EstablishmentId = 1 << 3 +} diff --git a/TeachingRecordSystem/src/TeachingRecordSystem.Core/Jobs/HostApplicationBuilderExtensions.cs b/TeachingRecordSystem/src/TeachingRecordSystem.Core/Jobs/HostApplicationBuilderExtensions.cs index c2dd57400..82fcc09cd 100644 --- a/TeachingRecordSystem/src/TeachingRecordSystem.Core/Jobs/HostApplicationBuilderExtensions.cs +++ b/TeachingRecordSystem/src/TeachingRecordSystem.Core/Jobs/HostApplicationBuilderExtensions.cs @@ -108,6 +108,11 @@ public static IHostApplicationBuilder AddBackgroundJobs(this IHostApplicationBui job => job.ExecuteAsync(CancellationToken.None), giasOptions!.Value.RefreshEstablishmentsJobSchedule); + recurringJobManager.AddOrUpdate( + nameof(ImportTpsCsvExtractFileJob), + job => job.ExecuteAsync(CancellationToken.None), + Cron.Never); + return Task.CompletedTask; }); } diff --git a/TeachingRecordSystem/src/TeachingRecordSystem.Core/Jobs/ImportTpsCsvExtractFileJob.cs b/TeachingRecordSystem/src/TeachingRecordSystem.Core/Jobs/ImportTpsCsvExtractFileJob.cs new file mode 100644 index 000000000..eedb4ec01 --- /dev/null +++ b/TeachingRecordSystem/src/TeachingRecordSystem.Core/Jobs/ImportTpsCsvExtractFileJob.cs @@ -0,0 +1,28 @@ +using TeachingRecordSystem.Core.Jobs.Scheduling; +using TeachingRecordSystem.Core.Services.WorkforceData; + +namespace TeachingRecordSystem.Core.Jobs; + +public class ImportTpsCsvExtractFileJob( + ITpsExtractStorageService tpsExtractStorageService, + IBackgroundJobScheduler backgroundJobScheduler) +{ + public async Task ExecuteAsync(CancellationToken cancellationToken) + { + var pendingImportFileNames = await tpsExtractStorageService.GetPendingImportFileNames(cancellationToken); + if (pendingImportFileNames.Length == 0) + { + return; + } + + // If we ever need to process more than one file then we can always manually trigger this job again or add a loop here + var tpsCsvExtractId = Guid.NewGuid(); + var importJobId = await backgroundJobScheduler.Enqueue(j => j.ImportFile(tpsCsvExtractId, pendingImportFileNames[0], cancellationToken)); + //var archiveJobId = await backgroundJobScheduler.ContinueJobWith(importJobId, j => j.ArchiveFile(pendingImportFileNames[0], cancellationToken)); + var copyJobId = await backgroundJobScheduler.ContinueJobWith(importJobId, j => j.CopyValidFormatDataToStaging(tpsCsvExtractId, cancellationToken)); + //var processInvalidTrnsJobId = await backgroundJobScheduler.ContinueJobWith(copyJobId, j => j.ProcessNonMatchingTrns(tpsCsvExtractId, cancellationToken)); + //var processInvalidEstablishmentsJobId = await backgroundJobScheduler.ContinueJobWith(processInvalidTrnsJobId, j => j.ProcessNonMatchingEstablishments(tpsCsvExtractId, cancellationToken)); + //var processNewEmploymentHistoryJobId = await backgroundJobScheduler.ContinueJobWith(processInvalidEstablishmentsJobId, j => j.ProcessNewEmploymentHistory(tpsCsvExtractId, cancellationToken)); + //var processUpdatedEmploymentHistoryJobId = await backgroundJobScheduler.ContinueJobWith(processNewEmploymentHistoryJobId, j => j.ProcessUpdatedEmploymentHistory(tpsCsvExtractId, cancellationToken)); + } +} diff --git a/TeachingRecordSystem/src/TeachingRecordSystem.Core/Jobs/Scheduling/ExecuteImmediatelyJobScheduler.cs b/TeachingRecordSystem/src/TeachingRecordSystem.Core/Jobs/Scheduling/ExecuteImmediatelyJobScheduler.cs index 23de238f1..446779931 100644 --- a/TeachingRecordSystem/src/TeachingRecordSystem.Core/Jobs/Scheduling/ExecuteImmediatelyJobScheduler.cs +++ b/TeachingRecordSystem/src/TeachingRecordSystem.Core/Jobs/Scheduling/ExecuteImmediatelyJobScheduler.cs @@ -12,11 +12,17 @@ public ExecuteImmediatelyJobScheduler(IServiceProvider serviceProvider) _serviceProvider = serviceProvider; } - public async Task Enqueue(Expression> expression) where T : notnull + public async Task Enqueue(Expression> expression) where T : notnull { using var scope = _serviceProvider.CreateScope(); var service = scope.ServiceProvider.GetRequiredService(); var task = expression.Compile()(service); await task; + return Guid.NewGuid().ToString(); + } + + public Task ContinueJobWith(string parentId, Expression> expression) where T : notnull + { + return Enqueue(expression); } } diff --git a/TeachingRecordSystem/src/TeachingRecordSystem.Core/Jobs/Scheduling/HangfireBackgroundJobScheduler.cs b/TeachingRecordSystem/src/TeachingRecordSystem.Core/Jobs/Scheduling/HangfireBackgroundJobScheduler.cs index 7bf8b0687..401097719 100644 --- a/TeachingRecordSystem/src/TeachingRecordSystem.Core/Jobs/Scheduling/HangfireBackgroundJobScheduler.cs +++ b/TeachingRecordSystem/src/TeachingRecordSystem.Core/Jobs/Scheduling/HangfireBackgroundJobScheduler.cs @@ -12,9 +12,15 @@ public HangfireBackgroundJobScheduler(IBackgroundJobClient jobClient) _jobClient = jobClient; } - public Task Enqueue(Expression> expression) where T : notnull + public Task Enqueue(Expression> expression) where T : notnull { - _jobClient.Enqueue(expression); - return Task.CompletedTask; + var jobId = _jobClient.Enqueue(expression); + return Task.FromResult(jobId); + } + + public Task ContinueJobWith(string parentId, Expression> expression) where T : notnull + { + var jobId = _jobClient.ContinueJobWith(parentId, expression); + return Task.FromResult(jobId); } } diff --git a/TeachingRecordSystem/src/TeachingRecordSystem.Core/Jobs/Scheduling/IBackgroundJobScheduler.cs b/TeachingRecordSystem/src/TeachingRecordSystem.Core/Jobs/Scheduling/IBackgroundJobScheduler.cs index 6ec960a37..02098d13b 100644 --- a/TeachingRecordSystem/src/TeachingRecordSystem.Core/Jobs/Scheduling/IBackgroundJobScheduler.cs +++ b/TeachingRecordSystem/src/TeachingRecordSystem.Core/Jobs/Scheduling/IBackgroundJobScheduler.cs @@ -4,5 +4,7 @@ namespace TeachingRecordSystem.Core.Jobs.Scheduling; public interface IBackgroundJobScheduler { - Task Enqueue(Expression> expression) where T : notnull; + Task Enqueue(Expression> expression) where T : notnull; + + Task ContinueJobWith(string parentId, Expression> expression) where T : notnull; } diff --git a/TeachingRecordSystem/src/TeachingRecordSystem.Core/Models/EmploymentType.cs b/TeachingRecordSystem/src/TeachingRecordSystem.Core/Models/EmploymentType.cs new file mode 100644 index 000000000..b74ea0705 --- /dev/null +++ b/TeachingRecordSystem/src/TeachingRecordSystem.Core/Models/EmploymentType.cs @@ -0,0 +1,22 @@ +namespace TeachingRecordSystem.Core.Models; + +public enum EmploymentType +{ + FullTime = 0, + PartTimeRegular = 1, + PartTimeIrregular = 2 +} + +public static class EmploymentTypeHelper +{ + public static EmploymentType FromFullOrPartTimeIndicator(string fullOrPartTimeIndicator) + { + return fullOrPartTimeIndicator switch + { + "FT" => EmploymentType.FullTime, + "PTR" => EmploymentType.PartTimeRegular, + "PTI" => EmploymentType.PartTimeIrregular, + _ => throw new ArgumentOutOfRangeException(nameof(fullOrPartTimeIndicator)) + }; + } +} diff --git a/TeachingRecordSystem/src/TeachingRecordSystem.Core/Services/WorkforceData/BlobStorageTpsExtractStorageService.cs b/TeachingRecordSystem/src/TeachingRecordSystem.Core/Services/WorkforceData/BlobStorageTpsExtractStorageService.cs new file mode 100644 index 000000000..0a56a0a52 --- /dev/null +++ b/TeachingRecordSystem/src/TeachingRecordSystem.Core/Services/WorkforceData/BlobStorageTpsExtractStorageService.cs @@ -0,0 +1,84 @@ +using Azure.Storage.Blobs; +using Azure.Storage.Blobs.Models; +using Azure.Storage.Blobs.Specialized; + +namespace TeachingRecordSystem.Core.Services.WorkforceData; + +public class BlobStorageTpsExtractStorageService(BlobServiceClient blobServiceClient) : ITpsExtractStorageService +{ + private const string TpsExtractsContainerName = "tps-extracts"; + private const string PendingFolderName = "pending"; + private const string ImportedFolderName = "imported"; + + public async Task GetPendingImportFileNames(CancellationToken cancellationToken) + { + var blobContainerClient = blobServiceClient.GetBlobContainerClient(TpsExtractsContainerName); + var fileNames = new List(); + await GetFileNamesAsync(blobContainerClient, PendingFolderName, true, fileNames, cancellationToken); + return fileNames.ToArray(); + } + + public async Task GetFile(string fileName, CancellationToken cancellationToken) + { + var blobContainerClient = blobServiceClient.GetBlobContainerClient(TpsExtractsContainerName); + var blobClient = blobContainerClient.GetBlobClient(fileName); + return await blobClient.OpenReadAsync(cancellationToken: cancellationToken); + } + + public async Task ArchiveFile(string fileName, CancellationToken cancellationToken) + { + var blobContainerClient = blobServiceClient.GetBlobContainerClient(TpsExtractsContainerName); + + var sourceBlobClient = blobContainerClient.GetBlobClient(fileName); + if (await sourceBlobClient.ExistsAsync(cancellationToken)) + { + var fileNameParts = fileName.Split("/"); + var fileNameWithoutFolder = fileNameParts.Last(); + var targetFileName = $"{ImportedFolderName}/{fileNameWithoutFolder}"; + + // Acquire a lease to prevent another client modifying the source blob + var lease = sourceBlobClient.GetBlobLeaseClient(); + await lease.AcquireAsync(TimeSpan.FromSeconds(60), cancellationToken: cancellationToken); + + var targetBlobClient = blobContainerClient.GetBlobClient(targetFileName); + var copyOperation = await targetBlobClient.StartCopyFromUriAsync(sourceBlobClient.Uri, cancellationToken: cancellationToken); + await copyOperation.WaitForCompletionAsync(); + + // Release the lease + var sourceProperties = await sourceBlobClient.GetPropertiesAsync(cancellationToken: cancellationToken); + if (sourceProperties.Value.LeaseState == LeaseState.Leased) + { + await lease.ReleaseAsync(cancellationToken: cancellationToken); + } + + // Now remove the original blob + await sourceBlobClient.DeleteAsync(DeleteSnapshotsOption.IncludeSnapshots, cancellationToken: cancellationToken); + } + } + + private async Task GetFileNamesAsync(BlobContainerClient containerClient, string prefix, bool includeSubfolders, List fileNames, CancellationToken cancellationToken) + { + var resultSegment = containerClient.GetBlobsByHierarchyAsync(prefix: prefix, delimiter: "/", cancellationToken: cancellationToken).AsPages(); + + // Enumerate the blobs returned for each page. + await foreach (Azure.Page blobPage in resultSegment) + { + foreach (BlobHierarchyItem blobhierarchyItem in blobPage.Values) + { + // A hierarchical listing may return both virtual directories and blobs. + if (blobhierarchyItem.IsPrefix) + { + if (includeSubfolders) + { + // Call recursively with the prefix to traverse the virtual directory. + await GetFileNamesAsync(containerClient, blobhierarchyItem.Prefix, true, fileNames, cancellationToken); + } + } + else + { + fileNames.Add(blobhierarchyItem.Blob.Name); + } + } + } + } +} diff --git a/TeachingRecordSystem/src/TeachingRecordSystem.Core/Services/WorkforceData/ITpsExtractStorageService.cs b/TeachingRecordSystem/src/TeachingRecordSystem.Core/Services/WorkforceData/ITpsExtractStorageService.cs new file mode 100644 index 000000000..646737881 --- /dev/null +++ b/TeachingRecordSystem/src/TeachingRecordSystem.Core/Services/WorkforceData/ITpsExtractStorageService.cs @@ -0,0 +1,10 @@ +namespace TeachingRecordSystem.Core.Services.WorkforceData; + +public interface ITpsExtractStorageService +{ + Task GetPendingImportFileNames(CancellationToken cancellationToken); + + Task GetFile(string fileName, CancellationToken cancellationToken); + + Task ArchiveFile(string fileName, CancellationToken cancellationToken); +} diff --git a/TeachingRecordSystem/src/TeachingRecordSystem.Core/Services/WorkforceData/NewPersonEmployment.cs b/TeachingRecordSystem/src/TeachingRecordSystem.Core/Services/WorkforceData/NewPersonEmployment.cs new file mode 100644 index 000000000..e69924e83 --- /dev/null +++ b/TeachingRecordSystem/src/TeachingRecordSystem.Core/Services/WorkforceData/NewPersonEmployment.cs @@ -0,0 +1,11 @@ +namespace TeachingRecordSystem.Core.Services.WorkforceData; + +public record NewPersonEmployment +{ + public required Guid TpsCsvExtractItemId { get; set; } + public required Guid PersonId { get; init; } + public required Guid EstablishmentId { get; init; } + public required DateOnly StartDate { get; init; } + public required DateOnly? EndDate { get; init; } + public required EmploymentType EmploymentType { get; init; } +} diff --git a/TeachingRecordSystem/src/TeachingRecordSystem.Core/Services/WorkforceData/ServiceCollectionExtensions.cs b/TeachingRecordSystem/src/TeachingRecordSystem.Core/Services/WorkforceData/ServiceCollectionExtensions.cs new file mode 100644 index 000000000..a5bdd2038 --- /dev/null +++ b/TeachingRecordSystem/src/TeachingRecordSystem.Core/Services/WorkforceData/ServiceCollectionExtensions.cs @@ -0,0 +1,15 @@ +using Microsoft.Extensions.DependencyInjection; + +namespace TeachingRecordSystem.Core.Services.WorkforceData; + +public static class ServiceCollectionExtensions +{ + public static IServiceCollection AddWorkforceData(this IServiceCollection services) + { + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + + return services; + } +} diff --git a/TeachingRecordSystem/src/TeachingRecordSystem.Core/Services/WorkforceData/TpsCsvExtractFileImporter.cs b/TeachingRecordSystem/src/TeachingRecordSystem.Core/Services/WorkforceData/TpsCsvExtractFileImporter.cs new file mode 100644 index 000000000..d7ce020df --- /dev/null +++ b/TeachingRecordSystem/src/TeachingRecordSystem.Core/Services/WorkforceData/TpsCsvExtractFileImporter.cs @@ -0,0 +1,239 @@ +using System.Globalization; +using System.Text.RegularExpressions; +using CsvHelper; +using CsvHelper.Configuration; +using Npgsql; +using NpgsqlTypes; +using TeachingRecordSystem.Core.DataStore.Postgres; +using TeachingRecordSystem.Core.DataStore.Postgres.Models; + +namespace TeachingRecordSystem.Core.Services.WorkforceData; + +public class TpsCsvExtractFileImporter( + ITpsExtractStorageService tpsExtractStorageService, + IDbContextFactory dbContextFactory, + IClock clock) +{ + public async Task ImportFile(Guid tpsCsvExtractId, string fileName, CancellationToken cancellationToken) + { + var fileNameParts = fileName.Split("/"); + var fileNameWithoutFolder = fileNameParts.Last(); + + using var dbContext = dbContextFactory.CreateDbContext(); + var connection = (NpgsqlConnection)dbContext.Database.GetDbConnection(); + await connection.OpenAsync(cancellationToken); + using var transaction = await connection.BeginTransactionAsync(cancellationToken); + + var insertTpsCsvExtractSql = + $""" + INSERT INTO tps_csv_extracts ( + tps_csv_extract_id, + filename, + created_on + ) + VALUES ( + '{tpsCsvExtractId}', + '{fileNameWithoutFolder}', + '{clock.UtcNow}' + ) + """; + await using var command = new NpgsqlCommand(insertTpsCsvExtractSql, connection, transaction); + await command.ExecuteNonQueryAsync(cancellationToken); + + using var writer = await connection.BeginBinaryImportAsync( + $""" + COPY + tps_csv_extract_load_items ( + tps_csv_extract_load_item_id, + tps_csv_extract_id, + trn, + national_insurance_number, + date_of_birth, + date_of_death, + member_postcode, + member_email_address, + local_authority_code, + establishment_number, + establishment_postcode, + establishment_email_address, + employment_start_date, + employment_end_date, + full_or_part_time_indicator, + withdrawl_indicator, + extract_date, + gender, + created, + errors + ) + FROM + STDIN (FORMAT BINARY) + """); + + writer.Timeout = TimeSpan.FromMinutes(5); + + using var stream = await tpsExtractStorageService.GetFile(fileName, cancellationToken); + using var streamReader = new StreamReader(stream); + using var csvReader = new CsvReader(streamReader, new CsvConfiguration(CultureInfo.CurrentCulture) { HasHeaderRecord = true }); + + var validGenderValues = new List() { "Male", "Female" }; + var validFullOrPartTimeIndicatorValues = new List() { "FT", "PTI", "PTR" }; + + await foreach (var row in csvReader.GetRecordsAsync()) + { + var loadErrors = TpsCsvExtractItemLoadErrors.None; + if (row.Trn is null || !Regex.IsMatch(row.Trn, @"^\d{7}$")) + { + loadErrors = loadErrors | TpsCsvExtractItemLoadErrors.TrnIncorrectFormat; + } + + if (row.NationalInsuranceNumber is null || !NationalInsuranceNumberHelper.IsValid(row.NationalInsuranceNumber)) + { + loadErrors = loadErrors | TpsCsvExtractItemLoadErrors.NationalInsuranceNumberIncorrectFormat; + } + + if (row.DateOfBirth is null || !DateOnly.TryParseExact(row.DateOfBirth, "dd/MM/yyyy", out _)) + { + loadErrors = loadErrors | TpsCsvExtractItemLoadErrors.DateOfBirthIncorrectFormat; + } + + if (row.DateOfDeath is not null && !DateOnly.TryParseExact(row.DateOfDeath, "dd/MM/yyyy", out _)) + { + loadErrors = loadErrors | TpsCsvExtractItemLoadErrors.DateOfDeathIncorrectFormat; + } + + if (row.LocalAuthorityCode is null || !Regex.IsMatch(row.LocalAuthorityCode, @"^\d{3}$")) + { + loadErrors = loadErrors | TpsCsvExtractItemLoadErrors.LocalAuthorityCodeIncorrectFormat; + } + + if (row.EstablishmentCode is not null && !Regex.IsMatch(row.EstablishmentCode, @"^\d{4}$")) + { + loadErrors = loadErrors | TpsCsvExtractItemLoadErrors.EstablishmentNumberIncorrectFormat; + } + + if (row.EmploymentStartDate is null || !DateOnly.TryParseExact(row.EmploymentStartDate, "dd/MM/yyyy", out _)) + { + loadErrors = loadErrors | TpsCsvExtractItemLoadErrors.EmploymentStartDateIncorrectFormat; + } + + if (row.EmploymentEndDate is not null && !DateOnly.TryParseExact(row.EmploymentEndDate, "dd/MM/yyyy", out _)) + { + loadErrors = loadErrors | TpsCsvExtractItemLoadErrors.EmploymentEndDateIncorrectFormat; + } + + if (row.FullOrPartTimeIndicator is null || !validFullOrPartTimeIndicatorValues.Contains(row.FullOrPartTimeIndicator)) + { + loadErrors = loadErrors | TpsCsvExtractItemLoadErrors.FullOrPartTimeIndicatorIncorrectFormat; + } + + if (row.WithdrawlIndicator is not null && row.WithdrawlIndicator != "W") + { + loadErrors = loadErrors | TpsCsvExtractItemLoadErrors.WithdrawlIndicatorIncorrectFormat; + } + + if (row.ExtractDate is null || !DateOnly.TryParseExact(row.ExtractDate, "dd/MM/yyyy", out _)) + { + loadErrors = loadErrors | TpsCsvExtractItemLoadErrors.ExtractDateIncorrectFormat; + } + + if (row.Gender is null || !validGenderValues.Contains(row.Gender)) + { + loadErrors = loadErrors | TpsCsvExtractItemLoadErrors.GenderIncorrectFormat; + } + + writer.StartRow(); + writer.Write(Guid.NewGuid(), NpgsqlDbType.Uuid); + writer.Write(tpsCsvExtractId, NpgsqlDbType.Uuid); + writer.Write(row.Trn, NpgsqlDbType.Varchar); + writer.Write(row.NationalInsuranceNumber, NpgsqlDbType.Varchar); + writer.Write(row.DateOfBirth, NpgsqlDbType.Varchar); + writer.Write(row.DateOfDeath, NpgsqlDbType.Varchar); + writer.Write(row.MemberPostcode, NpgsqlDbType.Varchar); + writer.Write(row.MemberEmailAddress, NpgsqlDbType.Varchar); + writer.Write(row.LocalAuthorityCode, NpgsqlDbType.Varchar); + writer.Write(row.EstablishmentCode, NpgsqlDbType.Varchar); + writer.Write(row.EstablishmentPostcode, NpgsqlDbType.Varchar); + writer.Write(row.EstablishmentEmailAddress, NpgsqlDbType.Varchar); + writer.Write(row.EmploymentStartDate, NpgsqlDbType.Varchar); + writer.Write(row.EmploymentEndDate, NpgsqlDbType.Varchar); + writer.Write(row.FullOrPartTimeIndicator, NpgsqlDbType.Varchar); + writer.Write(row.WithdrawlIndicator, NpgsqlDbType.Varchar); + writer.Write(row.ExtractDate, NpgsqlDbType.Varchar); + writer.Write(row.Gender, NpgsqlDbType.Varchar); + writer.Write(clock.UtcNow, NpgsqlDbType.TimestampTz); + writer.Write((int)loadErrors, NpgsqlDbType.Integer); + } + + await writer.CompleteAsync(cancellationToken); + await writer.CloseAsync(cancellationToken); + + await transaction.CommitAsync(cancellationToken); + } + + public async Task CopyValidFormatDataToStaging(Guid tpsCsvExtractId, CancellationToken cancellationToken) + { + using var readDbContext = dbContextFactory.CreateDbContext(); + using var writeDbContext = dbContextFactory.CreateDbContext(); + var connection = (NpgsqlConnection)writeDbContext.Database.GetDbConnection(); + await connection.OpenAsync(cancellationToken); + + using var writer = await connection.BeginBinaryImportAsync( + $""" + COPY + tps_csv_extract_items ( + tps_csv_extract_item_id, + tps_csv_extract_id, + tps_csv_extract_load_item_id, + trn, + national_insurance_number, + date_of_birth, + date_of_death, + member_postcode, + member_email_address, + local_authority_code, + establishment_number, + establishment_postcode, + establishment_email_address, + employment_start_date, + employment_end_date, + employment_type, + withdrawl_indicator, + extract_date, + gender, + created + ) + FROM + STDIN (FORMAT BINARY) + """); + + writer.Timeout = TimeSpan.FromMinutes(5); + + await foreach (var item in readDbContext.TpsCsvExtractLoadItems.Where(x => x.TpsCsvExtractId == tpsCsvExtractId && x.Errors == TpsCsvExtractItemLoadErrors.None).AsNoTracking().AsAsyncEnumerable()) + { + writer.StartRow(); + writer.Write(Guid.NewGuid(), NpgsqlDbType.Uuid); + writer.Write(tpsCsvExtractId, NpgsqlDbType.Uuid); + writer.Write(item.TpsCsvExtractLoadItemId, NpgsqlDbType.Uuid); + writer.Write(item.Trn, NpgsqlDbType.Char); + writer.Write(item.NationalInsuranceNumber, NpgsqlDbType.Char); + writer.Write(DateOnly.ParseExact(item.DateOfBirth!, "dd/MM/yyyy"), NpgsqlDbType.Date); + writer.Write(!string.IsNullOrEmpty(item.DateOfDeath) ? DateOnly.ParseExact(item.DateOfDeath, "dd/MM/yyyy") : (DateOnly?)null, NpgsqlDbType.Date); + writer.Write(item.MemberPostcode, NpgsqlDbType.Varchar); + writer.Write(item.MemberEmailAddress, NpgsqlDbType.Varchar); + writer.Write(item.LocalAuthorityCode, NpgsqlDbType.Char); + writer.Write(item.EstablishmentNumber, NpgsqlDbType.Char); + writer.Write(item.EstablishmentPostcode, NpgsqlDbType.Varchar); + writer.Write(item.EstablishmentEmailAddress, NpgsqlDbType.Varchar); + writer.Write(DateOnly.ParseExact(item.EmploymentStartDate!, "dd/MM/yyyy"), NpgsqlDbType.Date); + writer.Write(!string.IsNullOrEmpty(item.EmploymentEndDate) ? DateOnly.ParseExact(item.EmploymentEndDate!, "dd/MM/yyyy") : (DateOnly?)null, NpgsqlDbType.Date); + writer.Write((int)EmploymentTypeHelper.FromFullOrPartTimeIndicator(item.FullOrPartTimeIndicator!), NpgsqlDbType.Integer); + writer.Write(item.WithdrawlIndicator, NpgsqlDbType.Char); + writer.Write(DateOnly.ParseExact(item.ExtractDate!, "dd/MM/yyyy"), NpgsqlDbType.Date); + writer.Write(item.Gender, NpgsqlDbType.Varchar); + writer.Write(clock.UtcNow, NpgsqlDbType.TimestampTz); + } + + await writer.CompleteAsync(cancellationToken); + await writer.CloseAsync(cancellationToken); + } +} diff --git a/TeachingRecordSystem/src/TeachingRecordSystem.Core/Services/WorkforceData/TpsCsvExtractProcessor.cs b/TeachingRecordSystem/src/TeachingRecordSystem.Core/Services/WorkforceData/TpsCsvExtractProcessor.cs new file mode 100644 index 000000000..b37b472ac --- /dev/null +++ b/TeachingRecordSystem/src/TeachingRecordSystem.Core/Services/WorkforceData/TpsCsvExtractProcessor.cs @@ -0,0 +1,352 @@ +using Npgsql; +using TeachingRecordSystem.Core.DataStore.Postgres; +using TeachingRecordSystem.Core.DataStore.Postgres.Models; + +namespace TeachingRecordSystem.Core.Services.WorkforceData; + +public class TpsCsvExtractProcessor( + IDbContextFactory dbContextFactory, + IClock clock) +{ + public async Task ProcessNonMatchingTrns(Guid tpsCsvExtractId, CancellationToken cancellationToken) + { + int i = 0; + using var dbContext = dbContextFactory.CreateDbContext(); + foreach (var item in dbContext.TpsCsvExtractItems.Where(r => r.TpsCsvExtractId == tpsCsvExtractId && !dbContext.Persons.Any(p => p.Trn == r.Trn))) + { + item.Result = TpsCsvExtractItemResult.InvalidTrn; + i++; + if (i % 1000 == 0) + { + await dbContext.SaveChangesAsync(cancellationToken); + } + } + + if (dbContext.ChangeTracker.HasChanges()) + { + await dbContext.SaveChangesAsync(cancellationToken); + } + } + + public async Task ProcessNonMatchingEstablishments(Guid tpsCsvExtractId, CancellationToken cancellationToken) + { + int i = 0; + using var dbContext = dbContextFactory.CreateDbContext(); + foreach (var item in await dbContext.TpsCsvExtractItems.Where(x => x.TpsCsvExtractId == tpsCsvExtractId && dbContext.Persons.Any(p => p.Trn == x.Trn) && !dbContext.Establishments.Any(e => e.LaCode == x.LocalAuthorityCode && e.EstablishmentNumber == x.EstablishmentNumber)).ToListAsync()) + { + item.Result = TpsCsvExtractItemResult.InvalidEstablishment; + i++; + if (i % 1000 == 0) + { + await dbContext.SaveChangesAsync(cancellationToken); + } + } + + if (dbContext.ChangeTracker.HasChanges()) + { + await dbContext.SaveChangesAsync(cancellationToken); + } + } + + public async Task ProcessNewEmploymentHistory(Guid tpsCsvExtractId, CancellationToken cancellationToken) + { + using var readDbContext = dbContextFactory.CreateDbContext(); + readDbContext.Database.SetCommandTimeout(600); + using var writeDbContext = dbContextFactory.CreateDbContext(); + + FormattableString querySql = + $""" + WITH unique_establishments AS ( + SELECT + establishment_id, + la_code, + establishment_number, + establishment_name, + establishment_type_code, + postcode + FROM + (SELECT + establishment_id, + la_code, + establishment_number, + establishment_name, + establishment_type_code, + postcode, + ROW_NUMBER() OVER (PARTITION BY la_code, establishment_number ORDER BY translate(establishment_status_code::text, '1234', '1324')) as row_number + FROM + establishments) e + WHERE + e.row_number = 1 + ) + SELECT + x.tps_csv_extract_item_id, + p.person_id, + e.establishment_id, + x.employment_start_date as start_date, + x.employment_end_date as end_date, + x.employment_type + FROM + tps_csv_extract_items x + JOIN + persons p ON x.trn = p.trn + JOIN + unique_establishments e ON x.local_authority_code = e.la_code + AND (x.establishment_number = e.establishment_number OR + (e.establishment_type_code = '29' AND x.establishment_postcode = e.postcode)) + WHERE + x.tps_csv_extract_id = {tpsCsvExtractId} + AND x.result IS NULL + AND NOT EXISTS (SELECT + 1 + FROM + person_employments pe + WHERE + pe.person_id = p.person_id + AND pe.establishment_id = e.establishment_id + AND pe.start_date = x.employment_start_date) + """; + + int i = 0; + var processedExtractItemIds = new List(); + await foreach (var item in readDbContext.Database.SqlQuery(querySql).AsAsyncEnumerable()) + { + var personEmployment = new PersonEmployment + { + PersonEmploymentId = Guid.NewGuid(), + PersonId = item.PersonId, + EstablishmentId = item.EstablishmentId, + StartDate = item.StartDate, + EndDate = item.EndDate, + EmploymentType = item.EmploymentType, + CreatedOn = clock.UtcNow, + UpdatedOn = clock.UtcNow + }; + + writeDbContext.PersonEmployments.Add(personEmployment); + writeDbContext.AddEvent(new PersonEmploymentCreatedEvent + { + EventId = Guid.NewGuid(), + PersonId = item.PersonId, + PersonEmployment = Core.Events.Models.PersonEmployment.FromModel(personEmployment), + CreatedUtc = clock.UtcNow, + RaisedBy = DataStore.Postgres.Models.SystemUser.SystemUserId + }); + + processedExtractItemIds.Add(item.TpsCsvExtractItemId); + + i++; + if (i % 1000 == 0) + { + await SaveChanges(); + } + } + + if (writeDbContext.ChangeTracker.HasChanges()) + { + await SaveChanges(); + } + + async Task SaveChanges() + { + await writeDbContext.SaveChangesAsync(cancellationToken); + + FormattableString updateSql = + $""" + UPDATE + tps_csv_extract_items + SET + result = {TpsCsvExtractItemResult.ValidDataAdded} + WHERE + tps_csv_extract_item_id = ANY ({processedExtractItemIds}) + """; + await writeDbContext.Database.ExecuteSqlAsync(updateSql, cancellationToken); + processedExtractItemIds!.Clear(); + } + } + + public async Task ProcessUpdatedEmploymentHistory(Guid tpsCsvExtractId, CancellationToken cancellationToken) + { + using var readDbContext = dbContextFactory.CreateDbContext(); + readDbContext.Database.SetCommandTimeout(600); + using var writeDbContext = dbContextFactory.CreateDbContext(); + var connection = (NpgsqlConnection)writeDbContext.Database.GetDbConnection(); + await connection.OpenAsync(CancellationToken.None); + + FormattableString querySql = + $""" + WITH unique_establishments AS ( + SELECT + establishment_id, + la_code, + establishment_number, + establishment_name, + establishment_type_code, + postcode + FROM + (SELECT + establishment_id, + la_code, + establishment_number, + establishment_name, + establishment_type_code, + postcode, + ROW_NUMBER() OVER (PARTITION BY la_code, establishment_number ORDER BY translate(establishment_status_code::text, '1234', '1324')) as row_number + FROM + establishments) e + WHERE + e.row_number = 1 + ) + SELECT + x.tps_csv_extract_item_id, + pe.person_employment_id, + pe.person_id, + pe.establishment_id, + pe.start_date, + pe.end_date as current_end_date, + pe.employment_type as current_employment_type, + x.employment_end_date as end_date, + x.employment_type + FROM + tps_csv_extract_items x + JOIN + persons p ON x.trn = p.trn + JOIN + unique_establishments e ON x.local_authority_code = e.la_code + AND (x.establishment_number = e.establishment_number OR + (e.establishment_type_code = '29' AND x.establishment_postcode = e.postcode)) + JOIN + person_employments pe ON pe.person_id = p.person_id + AND pe.establishment_id = e.establishment_id + AND pe.start_date = x.employment_start_date + WHERE + x.tps_csv_extract_id = {tpsCsvExtractId} + AND x.result IS NULL + """; + + var updatedExtractItemIds = new List(); + var noChangeExtractItemIds = new List(); + var batchCommands = new List(); + + await foreach (var item in readDbContext.Database.SqlQuery(querySql).AsAsyncEnumerable()) + { + var changes = PersonEmploymentUpdatedEventChanges.None | + (item.CurrentEndDate != item.EndDate ? PersonEmploymentUpdatedEventChanges.EndDate : PersonEmploymentUpdatedEventChanges.None) | + (item.CurrentEmploymentType != item.EmploymentType ? PersonEmploymentUpdatedEventChanges.EmploymentType : PersonEmploymentUpdatedEventChanges.None); + + if (changes != PersonEmploymentUpdatedEventChanges.None) + { + var formattedEndDate = item.EndDate.HasValue ? $"to_date('{item.EndDate:yyyyMMdd}','YYYYMMDD')" : "NULL"; + var updatePersonEmploymentsCommand = new NpgsqlBatchCommand( + $""" + UPDATE + person_employments + SET + end_date = {formattedEndDate}, + employment_type = {(int)item.EmploymentType} + WHERE + person_employment_id = '{item.PersonEmploymentId}' + """); + + batchCommands.Add(updatePersonEmploymentsCommand); + writeDbContext.AddEvent(new PersonEmploymentUpdatedEvent + { + EventId = Guid.NewGuid(), + PersonId = item.PersonEmploymentId, + PersonEmployment = new() + { + PersonEmploymentId = item.PersonEmploymentId, + PersonId = item.PersonId, + EstablishmentId = item.EstablishmentId, + StartDate = item.StartDate, + EndDate = item.EndDate, + EmploymentType = item.EmploymentType + }, + OldPersonEmployment = new() + { + PersonEmploymentId = item.PersonEmploymentId, + PersonId = item.PersonId, + EstablishmentId = item.EstablishmentId, + StartDate = item.StartDate, + EndDate = item.CurrentEndDate, + EmploymentType = item.CurrentEmploymentType + }, + Changes = changes, + CreatedUtc = clock.UtcNow, + RaisedBy = DataStore.Postgres.Models.SystemUser.SystemUserId + }); + + updatedExtractItemIds.Add(item.TpsCsvExtractItemId); + if (updatedExtractItemIds.Count == 1000) + { + UpdateResult(updatedExtractItemIds, TpsCsvExtractItemResult.ValidDataUpdated); + updatedExtractItemIds.Clear(); + } + } + else + { + noChangeExtractItemIds.Add(item.TpsCsvExtractItemId); + + if (noChangeExtractItemIds.Count == 1000) + { + UpdateResult(noChangeExtractItemIds, TpsCsvExtractItemResult.ValidNoChange); + noChangeExtractItemIds.Clear(); + } + } + + if (batchCommands.Count == 50) + { + await SaveChanges(); + } + } + + if (updatedExtractItemIds.Any()) + { + UpdateResult(updatedExtractItemIds, TpsCsvExtractItemResult.ValidDataUpdated); + } + + if (noChangeExtractItemIds.Any()) + { + UpdateResult(noChangeExtractItemIds, TpsCsvExtractItemResult.ValidNoChange); + } + + if (batchCommands.Any()) + { + await SaveChanges(); + } + + void UpdateResult(IEnumerable extractItemIds, TpsCsvExtractItemResult result) + { + var formattedExtractItemIds = string.Join(", ", extractItemIds.Select(id => $"'{id}'")); + var updateResultCommand = new NpgsqlBatchCommand( + $""" + UPDATE + tps_csv_extract_items + SET + result = {(int)result} + WHERE + tps_csv_extract_item_id = ANY (ARRAY[{formattedExtractItemIds}]::uuid[]) + """); + batchCommands.Add(updateResultCommand); + } + + async Task SaveChanges() + { + if (writeDbContext.ChangeTracker.HasChanges()) + { + await writeDbContext.SaveChangesAsync(cancellationToken); + } + + if (batchCommands.Count > 0) + { + using var batch = new NpgsqlBatch(connection); + foreach (var command in batchCommands) + { + batch.BatchCommands.Add(command); + } + + await batch.ExecuteNonQueryAsync(cancellationToken); + batchCommands.Clear(); + } + } + } +} diff --git a/TeachingRecordSystem/src/TeachingRecordSystem.Core/Services/WorkforceData/TpsCsvExtractRowRaw.cs b/TeachingRecordSystem/src/TeachingRecordSystem.Core/Services/WorkforceData/TpsCsvExtractRowRaw.cs new file mode 100644 index 000000000..ff3da0e05 --- /dev/null +++ b/TeachingRecordSystem/src/TeachingRecordSystem.Core/Services/WorkforceData/TpsCsvExtractRowRaw.cs @@ -0,0 +1,55 @@ +using CsvHelper.Configuration.Attributes; + +namespace TeachingRecordSystem.Core.Services.WorkforceData; + +public class TpsCsvExtractRowRaw +{ + [Name("Teachers Pensions Reference Number (TRN)")] + [NullValues("")] + public required string? Trn { get; init; } + [Name("National Insurance Number (NINO)")] + [NullValues("")] + public required string? NationalInsuranceNumber { get; init; } + [Name("Date of Birth (DOB)")] + [NullValues("")] + public required string? DateOfBirth { get; init; } + [Name("Date of Death")] + [NullValues("")] + public required string? DateOfDeath { get; init; } + [Name("Postcode")] + [NullValues("")] + public required string? MemberPostcode { get; init; } + [Name("Email Address (Member)")] + [NullValues("")] + public required string? MemberEmailAddress { get; init; } + [Name("Local Authority Code")] + [NullValues("")] + public required string? LocalAuthorityCode { get; init; } + [Name("Establishment Code")] + [NullValues("")] + public required string? EstablishmentCode { get; init; } + [Name("Postcode (Establishment)")] + [NullValues("")] + public required string? EstablishmentPostcode { get; init; } + [Name("Email Address (Establishment)")] + [NullValues("")] + public required string? EstablishmentEmailAddress { get; init; } + [Name("Start Date")] + [NullValues("")] + public required string? EmploymentStartDate { get; init; } + [Name("End Date")] + [NullValues("")] + public required string? EmploymentEndDate { get; init; } + [Name("Full Time / Part Time Indicator")] + [NullValues("")] + public required string? FullOrPartTimeIndicator { get; init; } + [Name("Withdrawal Indicator")] + [NullValues("")] + public required string? WithdrawlIndicator { get; init; } + [Name("Extract Date")] + [NullValues("")] + public required string? ExtractDate { get; init; } + [Name("Gender")] + [NullValues("")] + public required string? Gender { get; init; } +} diff --git a/TeachingRecordSystem/src/TeachingRecordSystem.Core/Services/WorkforceData/UpdatedPersonEmployment.cs b/TeachingRecordSystem/src/TeachingRecordSystem.Core/Services/WorkforceData/UpdatedPersonEmployment.cs new file mode 100644 index 000000000..8a51f03a5 --- /dev/null +++ b/TeachingRecordSystem/src/TeachingRecordSystem.Core/Services/WorkforceData/UpdatedPersonEmployment.cs @@ -0,0 +1,14 @@ +namespace TeachingRecordSystem.Core.Services.WorkforceData; + +public record UpdatedPersonEmployment +{ + public required Guid TpsCsvExtractItemId { get; set; } + public required Guid PersonEmploymentId { get; init; } + public required Guid PersonId { get; init; } + public required Guid EstablishmentId { get; init; } + public required DateOnly StartDate { get; init; } + public required DateOnly? CurrentEndDate { get; init; } + public required EmploymentType CurrentEmploymentType { get; init; } + public required DateOnly? EndDate { get; init; } + public required EmploymentType EmploymentType { get; init; } +} diff --git a/TeachingRecordSystem/src/TeachingRecordSystem.Worker/Program.cs b/TeachingRecordSystem/src/TeachingRecordSystem.Worker/Program.cs index 223b74d8a..ec6627603 100644 --- a/TeachingRecordSystem/src/TeachingRecordSystem.Worker/Program.cs +++ b/TeachingRecordSystem/src/TeachingRecordSystem.Worker/Program.cs @@ -13,6 +13,7 @@ using TeachingRecordSystem.Core.Services.Notify; using TeachingRecordSystem.Core.Services.TrnGenerationApi; using TeachingRecordSystem.Core.Services.TrsDataSync; +using TeachingRecordSystem.Core.Services.WorkforceData; using TeachingRecordSystem.Hosting; using TeachingRecordSystem.Worker.Infrastructure.Logging; @@ -56,6 +57,7 @@ builder.Services .AddTrsBaseServices() + .AddWorkforceData() .AddMemoryCache(); // Filter telemetry emitted by DqtReportingService; diff --git a/TeachingRecordSystem/tests/TeachingRecordSystem.Core.Tests/Jobs/RefreshEstablishmentsJobTests.cs b/TeachingRecordSystem/tests/TeachingRecordSystem.Core.Tests/Jobs/RefreshEstablishmentsJobTests.cs index 858c6b5bf..c690d1710 100644 --- a/TeachingRecordSystem/tests/TeachingRecordSystem.Core.Tests/Jobs/RefreshEstablishmentsJobTests.cs +++ b/TeachingRecordSystem/tests/TeachingRecordSystem.Core.Tests/Jobs/RefreshEstablishmentsJobTests.cs @@ -1,23 +1,40 @@ +using Microsoft.PowerPlatform.Dataverse.Client; +using TeachingRecordSystem.Core.Dqt; using TeachingRecordSystem.Core.Jobs; using TeachingRecordSystem.Core.Services.Establishments; +using TeachingRecordSystem.Core.Services.TrsDataSync; using Establishment = TeachingRecordSystem.Core.Models.Establishment; namespace TeachingRecordSystem.Core.Tests.Jobs; -public class RefreshEstablishmentsJobTests : IAsyncLifetime +public class RefreshEstablishmentsJobTests { - public RefreshEstablishmentsJobTests(DbFixture dbFixture) + public RefreshEstablishmentsJobTests( + DbFixture dbFixture, + IOrganizationServiceAsync2 organizationService, + ReferenceDataCache referenceDataCache, + FakeTrnGenerator trnGenerator) { DbFixture = dbFixture; + Clock = new(); + + var dbContextFactory = dbFixture.GetDbContextFactory(); + + Helper = new TrsDataSyncHelper( + dbContextFactory, + organizationService, + referenceDataCache, + Clock); + + TestData = new TestData( + dbContextFactory, + organizationService, + referenceDataCache, + Clock, + trnGenerator, + TestDataSyncConfiguration.Sync(Helper)); } - public DbFixture DbFixture { get; } - - public Task DisposeAsync() => Task.CompletedTask; - - public Task InitializeAsync() => - DbFixture.WithDbContext(dbContext => dbContext.Database.ExecuteSqlAsync($"delete from establishments")); - [Fact] public Task ExecuteAsync_WhenCalledforNewUrn_AddsNewEstablishments() => DbFixture.WithDbContext(async dbContext => @@ -26,7 +43,7 @@ public Task ExecuteAsync_WhenCalledforNewUrn_AddsNewEstablishments() => var establishmentMasterDataService = Mock.Of(); var establishment1 = new Establishment { - Urn = 123456, + Urn = TestData.GenerateEstablishmentUrn(), LaCode = "123", LaName = "Test LA", EstablishmentNumber = "1234", @@ -46,7 +63,7 @@ public Task ExecuteAsync_WhenCalledforNewUrn_AddsNewEstablishments() => }; var establishment2 = new Establishment { - Urn = 123457, + Urn = TestData.GenerateEstablishmentUrn(), LaCode = "123", LaName = "Test LA", EstablishmentNumber = "1235", @@ -78,7 +95,7 @@ public Task ExecuteAsync_WhenCalledforNewUrn_AddsNewEstablishments() => await job.ExecuteAsync(CancellationToken.None); // Assert - var establishmentsActual = await dbContext.Establishments.OrderBy(e => e.Urn).ToListAsync(); + var establishmentsActual = await dbContext.Establishments.Where(e => e.Urn == establishment1.Urn || e.Urn == establishment2.Urn).OrderBy(e => e.Urn).ToListAsync(); Assert.Collection(establishmentsActual, e => { @@ -129,10 +146,11 @@ public Task ExecuteAsync_WhenCalledForExistingUrn_UpdatesEstablishment() => // Arrange var establishmentMasterDataService = Mock.Of(); + var urn = TestData.GenerateEstablishmentUrn(); var dbEstablishment = new Core.DataStore.Postgres.Models.Establishment() { EstablishmentId = Guid.NewGuid(), - Urn = 123456, + Urn = urn, LaCode = "123", LaName = "Test LA", EstablishmentNumber = "1234", @@ -154,7 +172,7 @@ public Task ExecuteAsync_WhenCalledForExistingUrn_UpdatesEstablishment() => var updatedEstablishment = new Establishment { - Urn = 123456, + Urn = urn, LaCode = "124", LaName = "Test2 LA", EstablishmentNumber = "1235", @@ -186,7 +204,9 @@ public Task ExecuteAsync_WhenCalledForExistingUrn_UpdatesEstablishment() => await job.ExecuteAsync(CancellationToken.None); // Assert - var establishmentActual = await dbContext.Establishments.SingleAsync(); + var urnEstablishments = await dbContext.Establishments.Where(e => e.Urn == dbEstablishment.Urn).ToListAsync(); + Assert.Single(urnEstablishments); + var establishmentActual = urnEstablishments.Single(); Assert.Equal(updatedEstablishment.Urn, establishmentActual.Urn); Assert.Equal(updatedEstablishment.LaCode, establishmentActual.LaCode); Assert.Equal(updatedEstablishment.LaName, establishmentActual.LaName); @@ -205,4 +225,12 @@ public Task ExecuteAsync_WhenCalledForExistingUrn_UpdatesEstablishment() => Assert.Equal(updatedEstablishment.County, establishmentActual.County); Assert.Equal(updatedEstablishment.Postcode, establishmentActual.Postcode); }); + + private DbFixture DbFixture { get; } + + private TestData TestData { get; } + + private TestableClock Clock { get; } + + public TrsDataSyncHelper Helper { get; } } diff --git a/TeachingRecordSystem/tests/TeachingRecordSystem.Core.Tests/Services/WorkforceData/TpsCsvExtractFileImporterTests.cs b/TeachingRecordSystem/tests/TeachingRecordSystem.Core.Tests/Services/WorkforceData/TpsCsvExtractFileImporterTests.cs new file mode 100644 index 000000000..2f3be8942 --- /dev/null +++ b/TeachingRecordSystem/tests/TeachingRecordSystem.Core.Tests/Services/WorkforceData/TpsCsvExtractFileImporterTests.cs @@ -0,0 +1,744 @@ +using System.Text; +using TeachingRecordSystem.Core.DataStore.Postgres.Models; +using TeachingRecordSystem.Core.Services.WorkforceData; + +namespace TeachingRecordSystem.Core.Tests.Services.WorkforceData; + +public class TpsCsvExtractFileImporterTests(DbFixture dbFixture) +{ + public static TheoryData GetImportFileTestScenarioData() + { + var validFormatTrn = "1234567"; + var invalidFormatTrn = "12345678"; + var validFormatNationalInsuranceNumber = Faker.Identification.UkNationalInsuranceNumber(); + var invalidFormatNationalInsuranceNumber = "1234"; + var validFormatDateOfBirth = "01/01/1980"; + var invalidFormatDateOfBirth = "1234"; + var validFormatDateOfDeath = "01/02/2024"; + var invalidFormatDateOfDeath = "1234"; + var validFormatLocalAuthorityCode = "123"; + var invalidFormatLocalAuthorityCode = "1234"; + var validFormatEstablishmentNumber = "1234"; + var invalidFormatEstablishmentNumber = "12345"; + var validFormatEmploymentStartDate = "03/02/2023"; + var invalidFormatEmploymentStartDate = "1234"; + var validFormatEmploymentEndDate = "03/05/2024"; + var invalidFormatEmploymentEndDate = "1234"; + var validFullOrPartTimeIndicator = "PTI"; + var invalidFullOrPartTimeIndicator = "PTI1"; + var validWithdrawlIndicator = "W"; + var invalidWithdrawlIndicator = "E1"; + var validFormatExtractDate = "07/03/2024"; + var invalidFormatExtractDate = "1234"; + var validFormatGender = "Male"; + var invalidFormatGender = "None"; + + return new() + { + // Null TRN + new () + { + Row = new() + { + Trn = null, + NationalInsuranceNumber = validFormatNationalInsuranceNumber, + DateOfBirth = validFormatDateOfBirth, + DateOfDeath = validFormatDateOfDeath, + MemberPostcode = null, + MemberEmailAddress = null, + LocalAuthorityCode = validFormatLocalAuthorityCode, + EstablishmentCode = validFormatEstablishmentNumber, + EstablishmentPostcode = null, + EstablishmentEmailAddress = null, + EmploymentStartDate = validFormatEmploymentStartDate, + EmploymentEndDate = validFormatEmploymentEndDate, + FullOrPartTimeIndicator = validFullOrPartTimeIndicator, + WithdrawlIndicator = validWithdrawlIndicator, + ExtractDate = validFormatExtractDate, + Gender = validFormatGender + }, + ExpectedResult = TpsCsvExtractItemLoadErrors.TrnIncorrectFormat, + }, + // Invalid TRN + new () + { + Row = new() + { + Trn = invalidFormatTrn, + NationalInsuranceNumber = validFormatNationalInsuranceNumber, + DateOfBirth = validFormatDateOfBirth, + DateOfDeath = validFormatDateOfDeath, + MemberPostcode = null, + MemberEmailAddress = null, + LocalAuthorityCode = validFormatLocalAuthorityCode, + EstablishmentCode = validFormatEstablishmentNumber, + EstablishmentPostcode = null, + EstablishmentEmailAddress = null, + EmploymentStartDate = validFormatEmploymentStartDate, + EmploymentEndDate = validFormatEmploymentEndDate, + FullOrPartTimeIndicator = validFullOrPartTimeIndicator, + WithdrawlIndicator = validWithdrawlIndicator, + ExtractDate = validFormatExtractDate, + Gender = validFormatGender + }, + ExpectedResult = TpsCsvExtractItemLoadErrors.TrnIncorrectFormat, + }, + // Null National Insurance Number + new () + { + Row = new() + { + Trn = validFormatTrn, + NationalInsuranceNumber = null, + DateOfBirth = validFormatDateOfBirth, + DateOfDeath = validFormatDateOfDeath, + MemberPostcode = null, + MemberEmailAddress = null, + LocalAuthorityCode = validFormatLocalAuthorityCode, + EstablishmentCode = validFormatEstablishmentNumber, + EstablishmentPostcode = null, + EstablishmentEmailAddress = null, + EmploymentStartDate = validFormatEmploymentStartDate, + EmploymentEndDate = validFormatEmploymentEndDate, + FullOrPartTimeIndicator = validFullOrPartTimeIndicator, + WithdrawlIndicator = validWithdrawlIndicator, + ExtractDate = validFormatExtractDate, + Gender = validFormatGender + }, + ExpectedResult = TpsCsvExtractItemLoadErrors.NationalInsuranceNumberIncorrectFormat, + }, + // Invalid National Insurance Number + new () + { + Row = new() + { + Trn = validFormatTrn, + NationalInsuranceNumber = invalidFormatNationalInsuranceNumber, + DateOfBirth = validFormatDateOfBirth, + DateOfDeath = validFormatDateOfDeath, + MemberPostcode = null, + MemberEmailAddress = null, + LocalAuthorityCode = validFormatLocalAuthorityCode, + EstablishmentCode = validFormatEstablishmentNumber, + EstablishmentPostcode = null, + EstablishmentEmailAddress = null, + EmploymentStartDate = validFormatEmploymentStartDate, + EmploymentEndDate = validFormatEmploymentEndDate, + FullOrPartTimeIndicator = validFullOrPartTimeIndicator, + WithdrawlIndicator = validWithdrawlIndicator, + ExtractDate = validFormatExtractDate, + Gender = validFormatGender + }, + ExpectedResult = TpsCsvExtractItemLoadErrors.NationalInsuranceNumberIncorrectFormat, + }, + // Null Date of Birth + new () + { + Row = new() + { + Trn = validFormatTrn, + NationalInsuranceNumber = validFormatNationalInsuranceNumber, + DateOfBirth = null, + DateOfDeath = validFormatDateOfDeath, + MemberPostcode = null, + MemberEmailAddress = null, + LocalAuthorityCode = validFormatLocalAuthorityCode, + EstablishmentCode = validFormatEstablishmentNumber, + EstablishmentPostcode = null, + EstablishmentEmailAddress = null, + EmploymentStartDate = validFormatEmploymentStartDate, + EmploymentEndDate = validFormatEmploymentEndDate, + FullOrPartTimeIndicator = validFullOrPartTimeIndicator, + WithdrawlIndicator = validWithdrawlIndicator, + ExtractDate = validFormatExtractDate, + Gender = validFormatGender + }, + ExpectedResult = TpsCsvExtractItemLoadErrors.DateOfBirthIncorrectFormat, + }, + // Invalid Date of Birth + new () + { + Row = new() + { + Trn = validFormatTrn, + NationalInsuranceNumber = validFormatNationalInsuranceNumber, + DateOfBirth = invalidFormatDateOfBirth, + DateOfDeath = validFormatDateOfDeath, + MemberPostcode = null, + MemberEmailAddress = null, + LocalAuthorityCode = validFormatLocalAuthorityCode, + EstablishmentCode = validFormatEstablishmentNumber, + EstablishmentPostcode = null, + EstablishmentEmailAddress = null, + EmploymentStartDate = validFormatEmploymentStartDate, + EmploymentEndDate = validFormatEmploymentEndDate, + FullOrPartTimeIndicator = validFullOrPartTimeIndicator, + WithdrawlIndicator = validWithdrawlIndicator, + ExtractDate = validFormatExtractDate, + Gender = validFormatGender + }, + ExpectedResult = TpsCsvExtractItemLoadErrors.DateOfBirthIncorrectFormat, + }, + // Null Date of Death + new () + { + Row = new() + { + Trn = validFormatTrn, + NationalInsuranceNumber = validFormatNationalInsuranceNumber, + DateOfBirth = validFormatDateOfBirth, + DateOfDeath = null, + MemberPostcode = null, + MemberEmailAddress = null, + LocalAuthorityCode = validFormatLocalAuthorityCode, + EstablishmentCode = validFormatEstablishmentNumber, + EstablishmentPostcode = null, + EstablishmentEmailAddress = null, + EmploymentStartDate = validFormatEmploymentStartDate, + EmploymentEndDate = validFormatEmploymentEndDate, + FullOrPartTimeIndicator = validFullOrPartTimeIndicator, + WithdrawlIndicator = validWithdrawlIndicator, + ExtractDate = validFormatExtractDate, + Gender = validFormatGender + }, + ExpectedResult = TpsCsvExtractItemLoadErrors.None, + }, + // Invalid Date of Death + new () + { + Row = new() + { + Trn = validFormatTrn, + NationalInsuranceNumber = validFormatNationalInsuranceNumber, + DateOfBirth = validFormatDateOfBirth, + DateOfDeath = invalidFormatDateOfDeath, + MemberPostcode = null, + MemberEmailAddress = null, + LocalAuthorityCode = validFormatLocalAuthorityCode, + EstablishmentCode = validFormatEstablishmentNumber, + EstablishmentPostcode = null, + EstablishmentEmailAddress = null, + EmploymentStartDate = validFormatEmploymentStartDate, + EmploymentEndDate = validFormatEmploymentEndDate, + FullOrPartTimeIndicator = validFullOrPartTimeIndicator, + WithdrawlIndicator = validWithdrawlIndicator, + ExtractDate = validFormatExtractDate, + Gender = validFormatGender + }, + ExpectedResult = TpsCsvExtractItemLoadErrors.DateOfDeathIncorrectFormat, + }, + // Null Local Authority Code + new () + { + Row = new() + { + Trn = validFormatTrn, + NationalInsuranceNumber = validFormatNationalInsuranceNumber, + DateOfBirth = validFormatDateOfBirth, + DateOfDeath = validFormatDateOfDeath, + MemberPostcode = null, + MemberEmailAddress = null, + LocalAuthorityCode = null, + EstablishmentCode = validFormatEstablishmentNumber, + EstablishmentPostcode = null, + EstablishmentEmailAddress = null, + EmploymentStartDate = validFormatEmploymentStartDate, + EmploymentEndDate = validFormatEmploymentEndDate, + FullOrPartTimeIndicator = validFullOrPartTimeIndicator, + WithdrawlIndicator = validWithdrawlIndicator, + ExtractDate = validFormatExtractDate, + Gender = validFormatGender + }, + ExpectedResult = TpsCsvExtractItemLoadErrors.LocalAuthorityCodeIncorrectFormat, + }, + // Invalid Local Authority Code + new () + { + Row = new() + { + Trn = validFormatTrn, + NationalInsuranceNumber = validFormatNationalInsuranceNumber, + DateOfBirth = validFormatDateOfBirth, + DateOfDeath = validFormatDateOfDeath, + MemberPostcode = null, + MemberEmailAddress = null, + LocalAuthorityCode = invalidFormatLocalAuthorityCode, + EstablishmentCode = validFormatEstablishmentNumber, + EstablishmentPostcode = null, + EstablishmentEmailAddress = null, + EmploymentStartDate = validFormatEmploymentStartDate, + EmploymentEndDate = validFormatEmploymentEndDate, + FullOrPartTimeIndicator = validFullOrPartTimeIndicator, + WithdrawlIndicator = validWithdrawlIndicator, + ExtractDate = validFormatExtractDate, + Gender = validFormatGender + }, + ExpectedResult = TpsCsvExtractItemLoadErrors.LocalAuthorityCodeIncorrectFormat, + }, + // Null Establishment Number + new () + { + Row = new() + { + Trn = validFormatTrn, + NationalInsuranceNumber = validFormatNationalInsuranceNumber, + DateOfBirth = validFormatDateOfBirth, + DateOfDeath = validFormatDateOfDeath, + MemberPostcode = null, + MemberEmailAddress = null, + LocalAuthorityCode = validFormatLocalAuthorityCode, + EstablishmentCode = null, + EstablishmentPostcode = null, + EstablishmentEmailAddress = null, + EmploymentStartDate = validFormatEmploymentStartDate, + EmploymentEndDate = validFormatEmploymentEndDate, + FullOrPartTimeIndicator = validFullOrPartTimeIndicator, + WithdrawlIndicator = validWithdrawlIndicator, + ExtractDate = validFormatExtractDate, + Gender = validFormatGender + }, + ExpectedResult = TpsCsvExtractItemLoadErrors.None, + }, + // Invalid Establishment Number + new () + { + Row = new() + { + Trn = validFormatTrn, + NationalInsuranceNumber = validFormatNationalInsuranceNumber, + DateOfBirth = validFormatDateOfBirth, + DateOfDeath = validFormatDateOfDeath, + MemberPostcode = null, + MemberEmailAddress = null, + LocalAuthorityCode = validFormatLocalAuthorityCode, + EstablishmentCode = invalidFormatEstablishmentNumber, + EstablishmentPostcode = null, + EstablishmentEmailAddress = null, + EmploymentStartDate = validFormatEmploymentStartDate, + EmploymentEndDate = validFormatEmploymentEndDate, + FullOrPartTimeIndicator = validFullOrPartTimeIndicator, + WithdrawlIndicator = validWithdrawlIndicator, + ExtractDate = validFormatExtractDate, + Gender = validFormatGender + }, + ExpectedResult = TpsCsvExtractItemLoadErrors.EstablishmentNumberIncorrectFormat, + }, + // Null Employment Start Date + new () + { + Row = new() + { + Trn = validFormatTrn, + NationalInsuranceNumber = validFormatNationalInsuranceNumber, + DateOfBirth = validFormatDateOfBirth, + DateOfDeath = validFormatDateOfDeath, + MemberPostcode = null, + MemberEmailAddress = null, + LocalAuthorityCode = validFormatLocalAuthorityCode, + EstablishmentCode = validFormatEstablishmentNumber, + EstablishmentPostcode = null, + EstablishmentEmailAddress = null, + EmploymentStartDate = null, + EmploymentEndDate = validFormatEmploymentEndDate, + FullOrPartTimeIndicator = validFullOrPartTimeIndicator, + WithdrawlIndicator = validWithdrawlIndicator, + ExtractDate = validFormatExtractDate, + Gender = validFormatGender + }, + ExpectedResult = TpsCsvExtractItemLoadErrors.EmploymentStartDateIncorrectFormat, + }, + // Invalid Employment Start Date + new () + { + Row = new() + { + Trn = validFormatTrn, + NationalInsuranceNumber = validFormatNationalInsuranceNumber, + DateOfBirth = validFormatDateOfBirth, + DateOfDeath = validFormatDateOfDeath, + MemberPostcode = null, + MemberEmailAddress = null, + LocalAuthorityCode = validFormatLocalAuthorityCode, + EstablishmentCode = validFormatEstablishmentNumber, + EstablishmentPostcode = null, + EstablishmentEmailAddress = null, + EmploymentStartDate = invalidFormatEmploymentStartDate, + EmploymentEndDate = validFormatEmploymentEndDate, + FullOrPartTimeIndicator = validFullOrPartTimeIndicator, + WithdrawlIndicator = validWithdrawlIndicator, + ExtractDate = validFormatExtractDate, + Gender = validFormatGender + }, + ExpectedResult = TpsCsvExtractItemLoadErrors.EmploymentStartDateIncorrectFormat, + }, + // Null Employment End Date + new () + { + Row = new() + { + Trn = validFormatTrn, + NationalInsuranceNumber = validFormatNationalInsuranceNumber, + DateOfBirth = validFormatDateOfBirth, + DateOfDeath = validFormatDateOfDeath, + MemberPostcode = null, + MemberEmailAddress = null, + LocalAuthorityCode = validFormatLocalAuthorityCode, + EstablishmentCode = validFormatEstablishmentNumber, + EstablishmentPostcode = null, + EstablishmentEmailAddress = null, + EmploymentStartDate = validFormatEmploymentStartDate, + EmploymentEndDate = null, + FullOrPartTimeIndicator = validFullOrPartTimeIndicator, + WithdrawlIndicator = validWithdrawlIndicator, + ExtractDate = validFormatExtractDate, + Gender = validFormatGender + }, + ExpectedResult = TpsCsvExtractItemLoadErrors.None, + }, + // Invalid Employment End Date + new () + { + Row = new() + { + Trn = validFormatTrn, + NationalInsuranceNumber = validFormatNationalInsuranceNumber, + DateOfBirth = validFormatDateOfBirth, + DateOfDeath = validFormatDateOfDeath, + MemberPostcode = null, + MemberEmailAddress = null, + LocalAuthorityCode = validFormatLocalAuthorityCode, + EstablishmentCode = validFormatEstablishmentNumber, + EstablishmentPostcode = null, + EstablishmentEmailAddress = null, + EmploymentStartDate = validFormatEmploymentStartDate, + EmploymentEndDate = invalidFormatEmploymentEndDate, + FullOrPartTimeIndicator = validFullOrPartTimeIndicator, + WithdrawlIndicator = validWithdrawlIndicator, + ExtractDate = validFormatExtractDate, + Gender = validFormatGender + }, + ExpectedResult = TpsCsvExtractItemLoadErrors.EmploymentEndDateIncorrectFormat, + }, + // Null Full or Part Time Indicator + new () + { + Row = new() + { + Trn = validFormatTrn, + NationalInsuranceNumber = validFormatNationalInsuranceNumber, + DateOfBirth = validFormatDateOfBirth, + DateOfDeath = validFormatDateOfDeath, + MemberPostcode = null, + MemberEmailAddress = null, + LocalAuthorityCode = validFormatLocalAuthorityCode, + EstablishmentCode = validFormatEstablishmentNumber, + EstablishmentPostcode = null, + EstablishmentEmailAddress = null, + EmploymentStartDate = validFormatEmploymentStartDate, + EmploymentEndDate = validFormatEmploymentEndDate, + FullOrPartTimeIndicator = null, + WithdrawlIndicator = validWithdrawlIndicator, + ExtractDate = validFormatExtractDate, + Gender = validFormatGender + }, + ExpectedResult = TpsCsvExtractItemLoadErrors.FullOrPartTimeIndicatorIncorrectFormat, + }, + // Invalid Full or Part Time Indicator + new () + { + Row = new() + { + Trn = validFormatTrn, + NationalInsuranceNumber = validFormatNationalInsuranceNumber, + DateOfBirth = validFormatDateOfBirth, + DateOfDeath = validFormatDateOfDeath, + MemberPostcode = null, + MemberEmailAddress = null, + LocalAuthorityCode = validFormatLocalAuthorityCode, + EstablishmentCode = validFormatEstablishmentNumber, + EstablishmentPostcode = null, + EstablishmentEmailAddress = null, + EmploymentStartDate = validFormatEmploymentStartDate, + EmploymentEndDate = validFormatEmploymentEndDate, + FullOrPartTimeIndicator = invalidFullOrPartTimeIndicator, + WithdrawlIndicator = validWithdrawlIndicator, + ExtractDate = validFormatExtractDate, + Gender = validFormatGender + }, + ExpectedResult = TpsCsvExtractItemLoadErrors.FullOrPartTimeIndicatorIncorrectFormat, + }, + // Null Withdrawl Indicator + new () + { + Row = new() + { + Trn = validFormatTrn, + NationalInsuranceNumber = validFormatNationalInsuranceNumber, + DateOfBirth = validFormatDateOfBirth, + DateOfDeath = validFormatDateOfDeath, + MemberPostcode = null, + MemberEmailAddress = null, + LocalAuthorityCode = validFormatLocalAuthorityCode, + EstablishmentCode = validFormatEstablishmentNumber, + EstablishmentPostcode = null, + EstablishmentEmailAddress = null, + EmploymentStartDate = validFormatEmploymentStartDate, + EmploymentEndDate = validFormatEmploymentEndDate, + FullOrPartTimeIndicator = validFullOrPartTimeIndicator, + WithdrawlIndicator = null, + ExtractDate = validFormatExtractDate, + Gender = validFormatGender + }, + ExpectedResult = TpsCsvExtractItemLoadErrors.None, + }, + // Invalid Withdrawl Indicator + new () + { + Row = new() + { + Trn = validFormatTrn, + NationalInsuranceNumber = validFormatNationalInsuranceNumber, + DateOfBirth = validFormatDateOfBirth, + DateOfDeath = validFormatDateOfDeath, + MemberPostcode = null, + MemberEmailAddress = null, + LocalAuthorityCode = validFormatLocalAuthorityCode, + EstablishmentCode = validFormatEstablishmentNumber, + EstablishmentPostcode = null, + EstablishmentEmailAddress = null, + EmploymentStartDate = validFormatEmploymentStartDate, + EmploymentEndDate = validFormatEmploymentEndDate, + FullOrPartTimeIndicator = validFullOrPartTimeIndicator, + WithdrawlIndicator = invalidWithdrawlIndicator, + ExtractDate = validFormatExtractDate, + Gender = validFormatGender + }, + ExpectedResult = TpsCsvExtractItemLoadErrors.WithdrawlIndicatorIncorrectFormat, + }, + // Null Extract Date + new () + { + Row = new() + { + Trn = validFormatTrn, + NationalInsuranceNumber = validFormatNationalInsuranceNumber, + DateOfBirth = validFormatDateOfBirth, + DateOfDeath = validFormatDateOfDeath, + MemberPostcode = null, + MemberEmailAddress = null, + LocalAuthorityCode = validFormatLocalAuthorityCode, + EstablishmentCode = validFormatEstablishmentNumber, + EstablishmentPostcode = null, + EstablishmentEmailAddress = null, + EmploymentStartDate = validFormatEmploymentStartDate, + EmploymentEndDate = validFormatEmploymentEndDate, + FullOrPartTimeIndicator = validFullOrPartTimeIndicator, + WithdrawlIndicator = validWithdrawlIndicator, + ExtractDate = null, + Gender = validFormatGender + }, + ExpectedResult = TpsCsvExtractItemLoadErrors.ExtractDateIncorrectFormat, + }, + // Invalid Extract Date + new () + { + Row = new() + { + Trn = validFormatTrn, + NationalInsuranceNumber = validFormatNationalInsuranceNumber, + DateOfBirth = validFormatDateOfBirth, + DateOfDeath = validFormatDateOfDeath, + MemberPostcode = null, + MemberEmailAddress = null, + LocalAuthorityCode = validFormatLocalAuthorityCode, + EstablishmentCode = validFormatEstablishmentNumber, + EstablishmentPostcode = null, + EstablishmentEmailAddress = null, + EmploymentStartDate = validFormatEmploymentStartDate, + EmploymentEndDate = validFormatEmploymentEndDate, + FullOrPartTimeIndicator = validFullOrPartTimeIndicator, + WithdrawlIndicator = validWithdrawlIndicator, + ExtractDate = invalidFormatExtractDate, + Gender = validFormatGender + }, + ExpectedResult = TpsCsvExtractItemLoadErrors.ExtractDateIncorrectFormat, + }, + // Null Gender + new () + { + Row = new() + { + Trn = validFormatTrn, + NationalInsuranceNumber = validFormatNationalInsuranceNumber, + DateOfBirth = validFormatDateOfBirth, + DateOfDeath = validFormatDateOfDeath, + MemberPostcode = null, + MemberEmailAddress = null, + LocalAuthorityCode = validFormatLocalAuthorityCode, + EstablishmentCode = validFormatEstablishmentNumber, + EstablishmentPostcode = null, + EstablishmentEmailAddress = null, + EmploymentStartDate = validFormatEmploymentStartDate, + EmploymentEndDate = validFormatEmploymentEndDate, + FullOrPartTimeIndicator = validFullOrPartTimeIndicator, + WithdrawlIndicator = validWithdrawlIndicator, + ExtractDate = validFormatExtractDate, + Gender = null + }, + ExpectedResult = TpsCsvExtractItemLoadErrors.GenderIncorrectFormat, + }, + // Invalid Gender + new () + { + Row = new() + { + Trn = validFormatTrn, + NationalInsuranceNumber = validFormatNationalInsuranceNumber, + DateOfBirth = validFormatDateOfBirth, + DateOfDeath = validFormatDateOfDeath, + MemberPostcode = null, + MemberEmailAddress = null, + LocalAuthorityCode = validFormatLocalAuthorityCode, + EstablishmentCode = validFormatEstablishmentNumber, + EstablishmentPostcode = null, + EstablishmentEmailAddress = null, + EmploymentStartDate = validFormatEmploymentStartDate, + EmploymentEndDate = validFormatEmploymentEndDate, + FullOrPartTimeIndicator = validFullOrPartTimeIndicator, + WithdrawlIndicator = validWithdrawlIndicator, + ExtractDate = validFormatExtractDate, + Gender = invalidFormatGender + }, + ExpectedResult = TpsCsvExtractItemLoadErrors.GenderIncorrectFormat, + } + }; + } + + [Theory] + [MemberData(nameof(GetImportFileTestScenarioData))] + public async Task ImportFile_WithRowData_InsertsRecordWithExpectedResult(TpsCsvExtractFileImportTestScenarioData testScenarioData) + { + // Arrange + var tpsExtractStorageService = Mock.Of(); + var dbContextFactory = dbFixture.GetDbContextFactory(); + var clock = new TestableClock(); + var tpsCsvExtractId = Guid.NewGuid(); + var filename = "pending/test.csv"; + var csvContent = new StringBuilder(); + csvContent.AppendLine("Teachers Pensions Reference Number (TRN),National Insurance Number (NINO),Date of Birth (DOB),Date of Death,Postcode,Email Address (Member),Local Authority Code,Establishment Code,Postcode (Establishment),Email Address (Establishment),Start Date,End Date,Full Time / Part Time Indicator,Withdrawal Indicator,Extract Date,Gender"); + csvContent.AppendLine($"{testScenarioData.Row.Trn},{testScenarioData.Row.NationalInsuranceNumber},{testScenarioData.Row.DateOfBirth},{testScenarioData.Row.DateOfDeath},{testScenarioData.Row.MemberPostcode},{testScenarioData.Row.MemberEmailAddress},{testScenarioData.Row.LocalAuthorityCode},{testScenarioData.Row.EstablishmentCode},{testScenarioData.Row.EstablishmentPostcode},{testScenarioData.Row.EstablishmentEmailAddress},{testScenarioData.Row.EmploymentStartDate},{testScenarioData.Row.EmploymentEndDate},{testScenarioData.Row.FullOrPartTimeIndicator},{testScenarioData.Row.WithdrawlIndicator},{testScenarioData.Row.ExtractDate},{testScenarioData.Row.Gender}"); + + using var stream = new MemoryStream(Encoding.UTF8.GetBytes(csvContent.ToString())); + Mock.Get(tpsExtractStorageService) + .Setup(x => x.GetFile(filename, CancellationToken.None)) + .ReturnsAsync(stream); + + // Act + var importer = new TpsCsvExtractFileImporter( + tpsExtractStorageService, + dbContextFactory, + clock); + await importer.ImportFile(tpsCsvExtractId, filename, CancellationToken.None); + + // Assert + using var dbContext = dbContextFactory.CreateDbContext(); + var result = await dbContext.TpsCsvExtractLoadItems + .Where(x => x.TpsCsvExtractId == tpsCsvExtractId) + .ToListAsync(); + + Assert.Single(result); + Assert.Equal(testScenarioData.ExpectedResult, result.First().Errors); + + await dbContext.TpsCsvExtracts.Where(x => x.TpsCsvExtractId == tpsCsvExtractId).ExecuteDeleteAsync(); + } + + [Fact] + public async Task CopyValidFormatDataToStaging_WithValidData_InsertsRecordWithExpectedResult() + { + // Arrange + var tpsExtractStorageService = Mock.Of(); + var dbContextFactory = dbFixture.GetDbContextFactory(); + var clock = new TestableClock(); + var tpsCsvExtractId = Guid.NewGuid(); + var tpsCsvExtract = new TpsCsvExtract + { + TpsCsvExtractId = tpsCsvExtractId, + Filename = "pending/test.csv", + CreatedOn = clock.UtcNow + }; + + var validLoadItem = new TpsCsvExtractLoadItem + { + TpsCsvExtractLoadItemId = Guid.NewGuid(), + TpsCsvExtractId = tpsCsvExtractId, + Trn = "1234567", + NationalInsuranceNumber = Faker.Identification.UkNationalInsuranceNumber(), + DateOfBirth = "01/01/1980", + DateOfDeath = "01/02/2024", + MemberPostcode = null, + MemberEmailAddress = null, + LocalAuthorityCode = "123", + EstablishmentNumber = "1234", + EstablishmentPostcode = null, + EstablishmentEmailAddress = null, + MemberId = null, + EmploymentStartDate = "03/02/2023", + EmploymentEndDate = "03/05/2024", + FullOrPartTimeIndicator = "PTI", + WithdrawlIndicator = null, + ExtractDate = "07/03/2024", + Gender = "Male", + Created = clock.UtcNow, + Errors = TpsCsvExtractItemLoadErrors.None + }; + var invalidLoadItem = new TpsCsvExtractLoadItem + { + TpsCsvExtractLoadItemId = Guid.NewGuid(), + TpsCsvExtractId = tpsCsvExtractId, + Trn = "7654321", + NationalInsuranceNumber = Faker.Identification.UkNationalInsuranceNumber(), + DateOfBirth = "01/01/1980", + DateOfDeath = "01/02/2024", + MemberPostcode = null, + MemberEmailAddress = null, + LocalAuthorityCode = "123", + EstablishmentNumber = "1234", + EstablishmentPostcode = null, + EstablishmentEmailAddress = null, + MemberId = null, + EmploymentStartDate = "03/02/2023", + EmploymentEndDate = "03/05/2024", + FullOrPartTimeIndicator = "PTI", + WithdrawlIndicator = null, + ExtractDate = "07/03/2024", + Gender = "Male", + Created = clock.UtcNow, + Errors = TpsCsvExtractItemLoadErrors.GenderIncorrectFormat + }; + + using var dbContext = dbContextFactory.CreateDbContext(); + await dbContext.TpsCsvExtracts.AddAsync(tpsCsvExtract); + await dbContext.TpsCsvExtractLoadItems.AddRangeAsync(validLoadItem, invalidLoadItem); + await dbContext.SaveChangesAsync(); + + // Act + var importer = new TpsCsvExtractFileImporter( + tpsExtractStorageService, + dbContextFactory, + clock); + await importer.CopyValidFormatDataToStaging(tpsCsvExtractId, CancellationToken.None); + + // Assert + var result = await dbContext.TpsCsvExtractItems + .Where(x => x.TpsCsvExtractId == tpsCsvExtractId) + .ToListAsync(); + Assert.Single(result); + Assert.Equal(validLoadItem.TpsCsvExtractLoadItemId, result.First().TpsCsvExtractLoadItemId); + } +} + +public class TpsCsvExtractFileImportTestScenarioData +{ + public required TpsCsvExtractRowRaw Row { get; init; } + public required TpsCsvExtractItemLoadErrors ExpectedResult { get; init; } +} diff --git a/TeachingRecordSystem/tests/TeachingRecordSystem.Core.Tests/Services/WorkforceData/TpsCsvExtractProcessorTests.cs b/TeachingRecordSystem/tests/TeachingRecordSystem.Core.Tests/Services/WorkforceData/TpsCsvExtractProcessorTests.cs new file mode 100644 index 000000000..81094dfd2 --- /dev/null +++ b/TeachingRecordSystem/tests/TeachingRecordSystem.Core.Tests/Services/WorkforceData/TpsCsvExtractProcessorTests.cs @@ -0,0 +1,192 @@ +using Microsoft.PowerPlatform.Dataverse.Client; +using TeachingRecordSystem.Core.DataStore.Postgres.Models; +using TeachingRecordSystem.Core.Dqt; +using TeachingRecordSystem.Core.Services.TrsDataSync; +using TeachingRecordSystem.Core.Services.WorkforceData; + +namespace TeachingRecordSystem.Core.Tests.Services.WorkforceData; + +public class TpsCsvExtractProcessorTests +{ + public TpsCsvExtractProcessorTests( + DbFixture dbFixture, + IOrganizationServiceAsync2 organizationService, + ReferenceDataCache referenceDataCache, + FakeTrnGenerator trnGenerator) + { + DbFixture = dbFixture; + Clock = new(); + + var dbContextFactory = dbFixture.GetDbContextFactory(); + + Helper = new TrsDataSyncHelper( + dbContextFactory, + organizationService, + referenceDataCache, + Clock); + + TestData = new TestData( + dbContextFactory, + organizationService, + referenceDataCache, + Clock, + trnGenerator, + TestDataSyncConfiguration.Sync(Helper)); + } + + [Fact] + public async Task ProcessNonMatchingTrns_WhenCalledWithTrnsNotMatchingPersonsInTrs_SetsResultToInvalidTrn() + { + // Arrange + var establishment1 = await TestData.CreateEstablishment(localAuthorityCode: "123", establishmentNumber: "1234"); + + var trn = await TestData.GenerateTrn(); + var tpsCsvExtractId = Guid.NewGuid(); + await TestData.CreateTpsCsvExtract(b => b.WithTpsCsvExtractId(tpsCsvExtractId).WithItem(trn, establishment1.LaCode, establishment1.EstablishmentNumber, establishment1.Postcode!, new DateOnly(2023, 02, 03))); + + // Act + var processor = new TpsCsvExtractProcessor( + TestData.DbContextFactory, + TestData.Clock); + await processor.ProcessNonMatchingTrns(tpsCsvExtractId, CancellationToken.None); + + // Assert + using var dbContext = TestData.DbContextFactory.CreateDbContext(); + var items = await dbContext.TpsCsvExtractItems.Where(i => i.TpsCsvExtractId == tpsCsvExtractId).ToListAsync(); + Assert.All(items, i => Assert.Equal(TpsCsvExtractItemResult.InvalidTrn, i.Result)); + } + + [Fact] + public async Task ProcessNonMatchingEstablishments_WhenCalledWithEstablishmentsNotMatchingEstablishmentsInTrs_SetsResultToInvalidEstablishment() + { + // Arrange + var person = await TestData.CreatePerson(); + var tpsCsvExtractId = Guid.NewGuid(); + var establishment1 = await TestData.CreateEstablishment(localAuthorityCode: "124", establishmentNumber: "1235"); + var nonExistentEstablishmentNumber = "4321"; + await TestData.CreateTpsCsvExtract(b => b.WithTpsCsvExtractId(tpsCsvExtractId).WithItem(person.Trn!, establishment1.LaCode, nonExistentEstablishmentNumber, establishment1.Postcode!, new DateOnly(2023, 02, 03))); + + // Act + var processor = new TpsCsvExtractProcessor( + TestData.DbContextFactory, + TestData.Clock); + await processor.ProcessNonMatchingEstablishments(tpsCsvExtractId, CancellationToken.None); + + // Assert + using var dbContext = TestData.DbContextFactory.CreateDbContext(); + var items = await dbContext.TpsCsvExtractItems.Where(i => i.TpsCsvExtractId == tpsCsvExtractId).ToListAsync(); + Assert.All(items, i => Assert.Equal(TpsCsvExtractItemResult.InvalidEstablishment, i.Result)); + } + + [Fact] + public async Task ProcessNewEmploymentHistory_WhenCalledWithNewEmploymentHistory_InsertsNewPersonEmploymentRecord() + { + // Arrange + var person = await TestData.CreatePerson(); + var tpsCsvExtractId = Guid.NewGuid(); + var establishment = await TestData.CreateEstablishment(localAuthorityCode: "125", establishmentNumber: "1236"); + await TestData.CreateTpsCsvExtract(b => b.WithTpsCsvExtractId(tpsCsvExtractId).WithItem(person!.Trn!, establishment.LaCode, establishment.EstablishmentNumber, establishment.Postcode!, new DateOnly(2023, 02, 03))); + + // Act + var processor = new TpsCsvExtractProcessor( + TestData.DbContextFactory, + TestData.Clock); + await processor.ProcessNewEmploymentHistory(tpsCsvExtractId, CancellationToken.None); + + // Assert + using var dbContext = TestData.DbContextFactory.CreateDbContext(); + var items = await dbContext.TpsCsvExtractItems.Where(i => i.TpsCsvExtractId == tpsCsvExtractId).ToListAsync(); + Assert.All(items, i => Assert.Equal(TpsCsvExtractItemResult.ValidDataAdded, i.Result)); + var employmentHistory = await dbContext.PersonEmployments.Where(e => e.PersonId == person.PersonId).ToListAsync(); + Assert.Single(employmentHistory); + var personEmployment = employmentHistory.Single(); + Assert.Equal(establishment.EstablishmentId, personEmployment.EstablishmentId); + } + + [Fact] + public async Task ProcessNewEmploymentHistory_ForLaCodeAndEstablishmentNumberWithMultipleEstablishmentEntries_MatchesToTheMostOpenEstablishment() + { + // Arrange + var person = await TestData.CreatePerson(); + var tpsCsvExtractId = Guid.NewGuid(); + var laCode = "321"; + var establishmentNumber = "4321"; + var postcode = Faker.Address.UkPostCode(); + var closedEstablishment = await TestData.CreateEstablishment(laCode, establishmentNumber: establishmentNumber, establishmentStatusCode: 2, postcode: postcode); + var openEstablishment = await TestData.CreateEstablishment(laCode, establishmentNumber: establishmentNumber, establishmentStatusCode: 1, postcode: postcode); + var proposedToOpenEstablishment = await TestData.CreateEstablishment(laCode, establishmentNumber: establishmentNumber, establishmentStatusCode: 4, postcode: postcode); + var openButProposedToCloseEstablishment = await TestData.CreateEstablishment(laCode, establishmentNumber: establishmentNumber, establishmentStatusCode: 3, postcode: postcode); + await TestData.CreateTpsCsvExtract(b => b.WithTpsCsvExtractId(tpsCsvExtractId).WithItem(person!.Trn!, laCode, establishmentNumber, postcode, new DateOnly(2023, 02, 03))); + + // Act + var processor = new TpsCsvExtractProcessor( + TestData.DbContextFactory, + TestData.Clock); + await processor.ProcessNewEmploymentHistory(tpsCsvExtractId, CancellationToken.None); + + // Assert + using var dbContext = TestData.DbContextFactory.CreateDbContext(); + var items = await dbContext.TpsCsvExtractItems.Where(i => i.TpsCsvExtractId == tpsCsvExtractId).ToListAsync(); + Assert.All(items, i => Assert.Equal(TpsCsvExtractItemResult.ValidDataAdded, i.Result)); + var employmentHistory = await dbContext.PersonEmployments.Where(e => e.PersonId == person.PersonId).ToListAsync(); + Assert.Single(employmentHistory); + var personEmployment = employmentHistory.Single(); + Assert.Equal(openEstablishment.EstablishmentId, personEmployment.EstablishmentId); + } + + [Fact] + public async Task ProcessUpdatedEmploymentHistory_WhenCalledWithUpdatedEmploymentHistory_UpdatesPersonEmploymentRecord() + { + // Arrange + var person = await TestData.CreatePerson(); + var tpsCsvExtractId = Guid.NewGuid(); + var establishment1 = await TestData.CreateEstablishment(localAuthorityCode: "126", establishmentNumber: "1237"); + var existingPersonEmployment = await TestData.CreatePersonEmployment(person.PersonId, establishment1.EstablishmentId, new DateOnly(2023, 02, 02), EmploymentType.FullTime); + var updatedEndDate = new DateOnly(2024, 03, 06); + await TestData.CreateTpsCsvExtract(b => b.WithTpsCsvExtractId(tpsCsvExtractId).WithItem(person!.Trn!, establishment1.LaCode, establishment1.EstablishmentNumber, establishment1.Postcode!, new DateOnly(2023, 02, 02), updatedEndDate)); + + // Act + var processor = new TpsCsvExtractProcessor( + TestData.DbContextFactory, + TestData.Clock); + await processor.ProcessUpdatedEmploymentHistory(tpsCsvExtractId, CancellationToken.None); + + // Assert + using var dbContext = TestData.DbContextFactory.CreateDbContext(); + var items = await dbContext.TpsCsvExtractItems.Where(i => i.TpsCsvExtractId == tpsCsvExtractId).ToListAsync(); + Assert.All(items, i => Assert.Equal(TpsCsvExtractItemResult.ValidDataUpdated, i.Result)); + var updatedPersonEmployment = await dbContext.PersonEmployments.SingleAsync(e => e.PersonEmploymentId == existingPersonEmployment.PersonEmploymentId); + Assert.Equal(updatedEndDate, updatedPersonEmployment.EndDate); + } + + [Fact] + public async Task ProcessUpdatedEmploymentHistory_WhenCalledWithUpdatedEmploymentHistoryWithNoChanges_SetsResultToValidNoChanges() + { + // Arrange + var person = await TestData.CreatePerson(); + var tpsCsvExtractId = Guid.NewGuid(); + var establishment1 = await TestData.CreateEstablishment(localAuthorityCode: "126", establishmentNumber: "1237"); + var existingPersonEmployment = await TestData.CreatePersonEmployment(person.PersonId, establishment1.EstablishmentId, new DateOnly(2023, 02, 02), EmploymentType.FullTime); + var updatedEndDate = new DateOnly(2024, 03, 06); + await TestData.CreateTpsCsvExtract(b => b.WithTpsCsvExtractId(tpsCsvExtractId).WithItem(person!.Trn!, establishment1.LaCode, establishment1.EstablishmentNumber, establishment1.Postcode!, existingPersonEmployment.StartDate, existingPersonEmployment.EndDate, "FT")); + + // Act + var processor = new TpsCsvExtractProcessor( + TestData.DbContextFactory, + TestData.Clock); + await processor.ProcessUpdatedEmploymentHistory(tpsCsvExtractId, CancellationToken.None); + + // Assert + using var dbContext = TestData.DbContextFactory.CreateDbContext(); + var items = await dbContext.TpsCsvExtractItems.Where(i => i.TpsCsvExtractId == tpsCsvExtractId).ToListAsync(); + Assert.All(items, i => Assert.Equal(TpsCsvExtractItemResult.ValidNoChange, i.Result)); + } + + private DbFixture DbFixture { get; } + + private TestData TestData { get; } + + private TestableClock Clock { get; } + + public TrsDataSyncHelper Helper { get; } +} diff --git a/TeachingRecordSystem/tests/TeachingRecordSystem.TestCommon/TestData.CreateEstablishment.cs b/TeachingRecordSystem/tests/TeachingRecordSystem.TestCommon/TestData.CreateEstablishment.cs new file mode 100644 index 000000000..1d7132eaa --- /dev/null +++ b/TeachingRecordSystem/tests/TeachingRecordSystem.TestCommon/TestData.CreateEstablishment.cs @@ -0,0 +1,79 @@ +namespace TeachingRecordSystem.TestCommon; + +public partial class TestData +{ + private static int _lastEstablishmentUrn = 100000; + + public int GenerateEstablishmentUrn() => Interlocked.Increment(ref _lastEstablishmentUrn); + + public async Task CreateEstablishment( + string localAuthorityCode, + string? localAuthorityName = null, + string? establishmentNumber = null, + string? establishmentName = null, + string? postcode = null, + int? urn = null, + bool isHigherEducationInstitution = false, + int establishmentStatusCode = 1) + { + var establishmentTypeCode = isHigherEducationInstitution ? "29" : "01"; + var establishmentTypeName = isHigherEducationInstitution ? "Higher education institutions" : "Community school"; + var establishmentTypeGroupCode = isHigherEducationInstitution ? 2 : 4; + var establishmentTypeGroupName = isHigherEducationInstitution ? "Universities" : "Local authority maintained schools"; + localAuthorityName ??= Faker.Address.City(); + establishmentName ??= Faker.Company.Name(); + postcode ??= Faker.Address.UkPostCode(); + urn ??= GenerateEstablishmentUrn(); + var establishmentStatusName = "Open"; + switch (establishmentStatusCode) + { + case 1: + establishmentStatusName = "Open"; + break; + case 2: + establishmentStatusName = "Closed"; + break; + case 3: + establishmentStatusName = "Open, but proposed to close"; + break; + case 4: + establishmentStatusName = "Proposed to open"; + break; + default: + establishmentStatusCode = 1; + break; + } + + var establishment = await WithDbContext(async dbContext => + { + var establishment = new Core.DataStore.Postgres.Models.Establishment + { + EstablishmentId = Guid.NewGuid(), + Urn = urn.Value, + LaCode = localAuthorityCode, + LaName = localAuthorityName, + EstablishmentNumber = establishmentNumber, + EstablishmentName = establishmentName, + EstablishmentTypeCode = establishmentTypeCode, + EstablishmentTypeName = establishmentTypeName, + EstablishmentTypeGroupCode = establishmentTypeGroupCode, + EstablishmentTypeGroupName = establishmentTypeGroupName, + EstablishmentStatusCode = establishmentStatusCode, + EstablishmentStatusName = establishmentStatusName, + Street = null, + Locality = null, + Address3 = null, + Town = null, + County = null, + Postcode = postcode, + }; + + dbContext.Establishments.Add(establishment); + await dbContext.SaveChangesAsync(); + + return establishment; + }); + + return establishment; + } +} diff --git a/TeachingRecordSystem/tests/TeachingRecordSystem.TestCommon/TestData.CreatePersonEmployment.cs b/TeachingRecordSystem/tests/TeachingRecordSystem.TestCommon/TestData.CreatePersonEmployment.cs new file mode 100644 index 000000000..77ce0b87a --- /dev/null +++ b/TeachingRecordSystem/tests/TeachingRecordSystem.TestCommon/TestData.CreatePersonEmployment.cs @@ -0,0 +1,36 @@ +using TeachingRecordSystem.Core.DataStore.Postgres.Models; + +namespace TeachingRecordSystem.TestCommon; + +public partial class TestData +{ + public async Task CreatePersonEmployment( + Guid personId, + Guid establishmentId, + DateOnly startDate, + EmploymentType employmentType, + DateOnly? endDate = null) + { + var personEmployment = await WithDbContext(async dbContext => + { + var personEmployment = new PersonEmployment + { + PersonEmploymentId = Guid.NewGuid(), + PersonId = personId, + EstablishmentId = establishmentId, + StartDate = startDate, + EndDate = endDate, + EmploymentType = employmentType, + CreatedOn = Clock.UtcNow, + UpdatedOn = Clock.UtcNow + }; + + dbContext.PersonEmployments.Add(personEmployment); + await dbContext.SaveChangesAsync(); + + return personEmployment; + }); + + return personEmployment; + } +} diff --git a/TeachingRecordSystem/tests/TeachingRecordSystem.TestCommon/TestData.CreateTpsCsvExtract.cs b/TeachingRecordSystem/tests/TeachingRecordSystem.TestCommon/TestData.CreateTpsCsvExtract.cs new file mode 100644 index 000000000..cc576ad01 --- /dev/null +++ b/TeachingRecordSystem/tests/TeachingRecordSystem.TestCommon/TestData.CreateTpsCsvExtract.cs @@ -0,0 +1,150 @@ +using TeachingRecordSystem.Core.DataStore.Postgres.Models; + +namespace TeachingRecordSystem.TestCommon; + +public partial class TestData +{ + public Task CreateTpsCsvExtract(Action? configure = null) + { + var builder = new TpsCsvExtractBuilder(); + configure?.Invoke(builder); + return builder.Execute(this); + } + + public class TpsCsvExtractBuilder + { + private readonly string[] validGenderValues = ["Male", "Female"]; + private readonly string[] validFullOrPartTimeIndicatorValues = ["FT", "PTI", "PTR"]; + private readonly List _items = new(); + private Guid? _tpsCsvExtractId; + private string? _filename; + + public TpsCsvExtractBuilder WithTpsCsvExtractId(Guid tpsCsvExtractId) + { + if (_tpsCsvExtractId.HasValue && _tpsCsvExtractId.Value != tpsCsvExtractId) + { + throw new InvalidOperationException("TpsCsvExtractId already set"); + } + + _tpsCsvExtractId = tpsCsvExtractId; + return this; + } + + public TpsCsvExtractBuilder WithFilename(string filename) + { + if (_filename != null && _filename != filename) + { + throw new InvalidOperationException("Filename already set"); + } + + _filename = filename; + return this; + } + + public TpsCsvExtractBuilder WithItem( + string trn, + string localAuthorityCode, + string? establishmentNumber, + string establishmentPostcode, + DateOnly startDate, + DateOnly? endDate = null, + string? fullOrPartTimeIndicator = null, + string? nationalInsuranceNumber = null, + DateOnly? dateOfBirth = null) + { + nationalInsuranceNumber ??= Faker.Identification.UkNationalInsuranceNumber(); + dateOfBirth ??= DateOnly.FromDateTime(Faker.Identification.DateOfBirth()); + fullOrPartTimeIndicator ??= validFullOrPartTimeIndicatorValues[Faker.RandomNumber.Next(0, 2)]; + + _items.Add(new TpsCsvExtractItem(trn, nationalInsuranceNumber, dateOfBirth.Value, localAuthorityCode, establishmentPostcode, establishmentNumber, startDate, endDate, fullOrPartTimeIndicator)); + return this; + } + + internal async Task Execute(TestData testData) + { + if (_tpsCsvExtractId is null) + { + throw new InvalidOperationException("TpsCsvExtractId has not been set"); + } + + _tpsCsvExtractId ??= Guid.NewGuid(); + _filename ??= "test.csv"; + var extractDate = testData.Clock.Today; + var createdOn = testData.Clock.UtcNow; + + var tpsCsvExtract = new TpsCsvExtract + { + TpsCsvExtractId = _tpsCsvExtractId.Value, + Filename = _filename, + CreatedOn = createdOn + }; + + await testData.WithDbContext(async dbContext => + { + dbContext.TpsCsvExtracts.Add(tpsCsvExtract); + int memberId = 10000; + foreach (var item in _items) + { + var loadItem = new TpsCsvExtractLoadItem + { + TpsCsvExtractLoadItemId = Guid.NewGuid(), + TpsCsvExtractId = tpsCsvExtract.TpsCsvExtractId, + Trn = item.Trn, + NationalInsuranceNumber = item.NationalInsuranceNumber, + DateOfBirth = item.DateOfBirth.ToString("dd/MM/yyyy"), + DateOfDeath = null, + MemberPostcode = null, + MemberEmailAddress = null, + LocalAuthorityCode = item.LocalAuthorityCode, + EstablishmentNumber = item.EstablishmentNumber, + EstablishmentPostcode = item.EstablishmentPostcode, + EstablishmentEmailAddress = null, + MemberId = memberId++.ToString(), + EmploymentStartDate = item.StartDate.ToString("dd/MM/yyyy"), + EmploymentEndDate = item.EndDate?.ToString("dd/MM/yyyy"), + FullOrPartTimeIndicator = item.FullOrPartTimeIndicator, + WithdrawlIndicator = null, + ExtractDate = extractDate.ToString("dd/MM/yyyy"), + Gender = validGenderValues[Faker.RandomNumber.Next(0, 1)], + Errors = TpsCsvExtractItemLoadErrors.None, + Created = DateTime.UtcNow + }; + + dbContext.TpsCsvExtractLoadItems.Add(loadItem); + + var validItem = new Core.DataStore.Postgres.Models.TpsCsvExtractItem + { + TpsCsvExtractItemId = Guid.NewGuid(), + TpsCsvExtractId = tpsCsvExtract.TpsCsvExtractId, + TpsCsvExtractLoadItemId = loadItem.TpsCsvExtractLoadItemId, + Trn = item.Trn, + NationalInsuranceNumber = item.NationalInsuranceNumber, + DateOfBirth = item.DateOfBirth, + DateOfDeath = null, + Gender = loadItem.Gender, + MemberPostcode = loadItem.MemberPostcode, + MemberEmailAddress = loadItem.MemberEmailAddress, + LocalAuthorityCode = item.LocalAuthorityCode, + EstablishmentNumber = item.EstablishmentNumber, + EstablishmentPostcode = item.EstablishmentPostcode, + EstablishmentEmailAddress = loadItem.EstablishmentEmailAddress, + MemberId = int.Parse(loadItem.MemberId), + EmploymentStartDate = item.StartDate, + EmploymentEndDate = item.EndDate, + EmploymentType = EmploymentTypeHelper.FromFullOrPartTimeIndicator(loadItem.FullOrPartTimeIndicator), + WithdrawlIndicator = loadItem.WithdrawlIndicator, + ExtractDate = extractDate, + Created = createdOn, + Result = null + }; + + dbContext.TpsCsvExtractItems.Add(validItem); + } + + await dbContext.SaveChangesAsync(); + }); + } + } + + public record TpsCsvExtractItem(string Trn, string NationalInsuranceNumber, DateOnly DateOfBirth, string LocalAuthorityCode, string EstablishmentPostcode, string? EstablishmentNumber, DateOnly StartDate, DateOnly? EndDate, string FullOrPartTimeIndicator); +}