From 506edbcd2cd368404e684c9fe41b2e938bf4238d Mon Sep 17 00:00:00 2001 From: alex-clayton-1 Date: Mon, 19 May 2025 14:45:51 +0100 Subject: [PATCH 1/9] feat: Introduced NbssAppointmentEvent model and migration --- ...1_AddNbssAppointmentEventTable.Designer.cs | 223 ++++++++++++++++++ ...0519134431_AddNbssAppointmentEventTable.cs | 73 ++++++ .../ServiceLayerDbContextModelSnapshot.cs | 161 ++++++++++++- .../Data/Models/NbssAppointmentEvent.cs | 75 ++++++ .../Data/ServiceLayerDbContext.cs | 7 + 5 files changed, 537 insertions(+), 2 deletions(-) create mode 100644 src/ServiceLayer.Common/Data/Migrations/20250519134431_AddNbssAppointmentEventTable.Designer.cs create mode 100644 src/ServiceLayer.Common/Data/Migrations/20250519134431_AddNbssAppointmentEventTable.cs create mode 100644 src/ServiceLayer.Common/Data/Models/NbssAppointmentEvent.cs diff --git a/src/ServiceLayer.Common/Data/Migrations/20250519134431_AddNbssAppointmentEventTable.Designer.cs b/src/ServiceLayer.Common/Data/Migrations/20250519134431_AddNbssAppointmentEventTable.Designer.cs new file mode 100644 index 0000000..b1e0e1f --- /dev/null +++ b/src/ServiceLayer.Common/Data/Migrations/20250519134431_AddNbssAppointmentEventTable.Designer.cs @@ -0,0 +1,223 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using ServiceLayer.Data; + +#nullable disable + +namespace ServiceLayer.Mesh.Migrations +{ + [DbContext(typeof(ServiceLayerDbContext))] + [Migration("20250519134431_AddNbssAppointmentEventTable")] + partial class AddNbssAppointmentEventTable + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.5") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("ServiceLayer.Data.Models.MeshFile", b => + { + b.Property("FileId") + .HasMaxLength(255) + .HasColumnType("nvarchar(255)"); + + b.Property("BlobPath") + .HasMaxLength(1024) + .HasColumnType("nvarchar(1024)"); + + b.Property("FileType") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("FirstSeenUtc") + .HasColumnType("datetime2"); + + b.Property("LastUpdatedUtc") + .HasColumnType("datetime2"); + + b.Property("MailboxId") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("nvarchar(20)"); + + b.HasKey("FileId"); + + b.ToTable("MeshFiles"); + }); + + modelBuilder.Entity("ServiceLayer.Data.Models.NbssAppointmentEvent", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("Action") + .IsRequired() + .HasMaxLength(1) + .HasColumnType("char(1)"); + + b.Property("ActionTimestamp") + .HasColumnType("datetime2"); + + b.Property("AppointmenId") + .IsRequired() + .HasMaxLength(27) + .HasColumnType("nvarchar(27)"); + + b.Property("AppointmentDateTime") + .HasColumnType("datetime2"); + + b.Property("AppointmentType") + .IsRequired() + .HasMaxLength(1) + .HasColumnType("char(1)"); + + b.Property("AttendedNotScreened") + .HasMaxLength(1) + .HasColumnType("char(1)"); + + b.Property("BSO") + .IsRequired() + .HasMaxLength(3) + .HasColumnType("char(3)"); + + b.Property("BatchId") + .IsRequired() + .HasMaxLength(9) + .HasColumnType("nvarchar(9)"); + + b.Property("BookedBy") + .IsRequired() + .HasMaxLength(1) + .HasColumnType("char(1)"); + + b.Property("CancelledBy") + .IsRequired() + .HasMaxLength(1) + .HasColumnType("char(1)"); + + b.Property("ClinicAddressLine1") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("nvarchar(30)"); + + b.Property("ClinicAddressLine2") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("nvarchar(30)"); + + b.Property("ClinicAddressLine3") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("nvarchar(30)"); + + b.Property("ClinicAddressLine4") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("nvarchar(30)"); + + b.Property("ClinicAddressLine5") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("nvarchar(30)"); + + b.Property("ClinicCode") + .IsRequired() + .HasMaxLength(5) + .HasColumnType("nvarchar(5)"); + + b.Property("ClinicName") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("nvarchar(40)"); + + b.Property("ClinicNameOnLetters") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("ClinicPostcode") + .IsRequired() + .HasMaxLength(8) + .HasColumnType("nvarchar(8)"); + + b.Property("EpisodeStart") + .HasColumnType("date"); + + b.Property("EpisodeType") + .IsRequired() + .HasMaxLength(1) + .HasColumnType("char(1)"); + + b.Property("ExtractId") + .IsRequired() + .HasMaxLength(8) + .HasColumnType("char(8)"); + + b.Property("HoldingClinic") + .HasMaxLength(1) + .HasColumnType("char(1)"); + + b.Property("Location") + .IsRequired() + .HasMaxLength(5) + .HasColumnType("nvarchar(5)"); + + b.Property("MeshFileId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("nvarchar(255)"); + + b.Property("NhsNumber") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("char(10)"); + + b.Property("ScreeningAppointmentNumber") + .HasColumnType("tinyint"); + + b.Property("Sequence") + .IsRequired() + .HasMaxLength(6) + .HasColumnType("char(6)"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(1) + .HasColumnType("char(1)"); + + b.HasKey("Id"); + + b.HasIndex("MeshFileId"); + + b.ToTable("NbssAppointmentEvents"); + }); + + modelBuilder.Entity("ServiceLayer.Data.Models.NbssAppointmentEvent", b => + { + b.HasOne("ServiceLayer.Data.Models.MeshFile", null) + .WithMany() + .HasForeignKey("MeshFileId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/ServiceLayer.Common/Data/Migrations/20250519134431_AddNbssAppointmentEventTable.cs b/src/ServiceLayer.Common/Data/Migrations/20250519134431_AddNbssAppointmentEventTable.cs new file mode 100644 index 0000000..0b72c68 --- /dev/null +++ b/src/ServiceLayer.Common/Data/Migrations/20250519134431_AddNbssAppointmentEventTable.cs @@ -0,0 +1,73 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace ServiceLayer.Mesh.Migrations +{ + /// + public partial class AddNbssAppointmentEventTable : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "NbssAppointmentEvents", + columns: table => new + { + Id = table.Column(type: "uniqueidentifier", nullable: false), + MeshFileId = table.Column(type: "nvarchar(255)", maxLength: 255, nullable: false), + BSO = table.Column(type: "char(3)", maxLength: 3, nullable: false), + ExtractId = table.Column(type: "char(8)", maxLength: 8, nullable: false), + Sequence = table.Column(type: "char(6)", maxLength: 6, nullable: false), + Action = table.Column(type: "char(1)", maxLength: 1, nullable: false), + ClinicCode = table.Column(type: "nvarchar(5)", maxLength: 5, nullable: false), + HoldingClinic = table.Column(type: "char(1)", maxLength: 1, nullable: true), + Status = table.Column(type: "char(1)", maxLength: 1, nullable: false), + AttendedNotScreened = table.Column(type: "char(1)", maxLength: 1, nullable: true), + AppointmenId = table.Column(type: "nvarchar(27)", maxLength: 27, nullable: false), + NhsNumber = table.Column(type: "char(10)", maxLength: 10, nullable: false), + EpisodeType = table.Column(type: "char(1)", maxLength: 1, nullable: false), + EpisodeStart = table.Column(type: "date", nullable: false), + BatchId = table.Column(type: "nvarchar(9)", maxLength: 9, nullable: false), + AppointmentType = table.Column(type: "char(1)", maxLength: 1, nullable: false), + ScreeningAppointmentNumber = table.Column(type: "tinyint", nullable: true), + BookedBy = table.Column(type: "char(1)", maxLength: 1, nullable: false), + CancelledBy = table.Column(type: "char(1)", maxLength: 1, nullable: false), + AppointmentDateTime = table.Column(type: "datetime2", nullable: false), + Location = table.Column(type: "nvarchar(5)", maxLength: 5, nullable: false), + ClinicName = table.Column(type: "nvarchar(40)", maxLength: 40, nullable: false), + ClinicNameOnLetters = table.Column(type: "nvarchar(50)", maxLength: 50, nullable: false), + ClinicAddressLine1 = table.Column(type: "nvarchar(30)", maxLength: 30, nullable: false), + ClinicAddressLine2 = table.Column(type: "nvarchar(30)", maxLength: 30, nullable: false), + ClinicAddressLine3 = table.Column(type: "nvarchar(30)", maxLength: 30, nullable: false), + ClinicAddressLine4 = table.Column(type: "nvarchar(30)", maxLength: 30, nullable: false), + ClinicAddressLine5 = table.Column(type: "nvarchar(30)", maxLength: 30, nullable: false), + ClinicPostcode = table.Column(type: "nvarchar(8)", maxLength: 8, nullable: false), + ActionTimestamp = table.Column(type: "datetime2", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_NbssAppointmentEvents", x => x.Id); + table.ForeignKey( + name: "FK_NbssAppointmentEvents_MeshFiles_MeshFileId", + column: x => x.MeshFileId, + principalTable: "MeshFiles", + principalColumn: "FileId", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_NbssAppointmentEvents_MeshFileId", + table: "NbssAppointmentEvents", + column: "MeshFileId"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "NbssAppointmentEvents"); + } + } +} diff --git a/src/ServiceLayer.Common/Data/Migrations/ServiceLayerDbContextModelSnapshot.cs b/src/ServiceLayer.Common/Data/Migrations/ServiceLayerDbContextModelSnapshot.cs index f000fcd..bdc529f 100644 --- a/src/ServiceLayer.Common/Data/Migrations/ServiceLayerDbContextModelSnapshot.cs +++ b/src/ServiceLayer.Common/Data/Migrations/ServiceLayerDbContextModelSnapshot.cs @@ -17,12 +17,12 @@ protected override void BuildModel(ModelBuilder modelBuilder) { #pragma warning disable 612, 618 modelBuilder - .HasAnnotation("ProductVersion", "9.0.1") + .HasAnnotation("ProductVersion", "9.0.5") .HasAnnotation("Relational:MaxIdentifierLength", 128); SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); - modelBuilder.Entity("ServiceLayer.Mesh.Models.MeshFile", b => + modelBuilder.Entity("ServiceLayer.Data.Models.MeshFile", b => { b.Property("FileId") .HasMaxLength(255) @@ -57,6 +57,163 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("MeshFiles"); }); + + modelBuilder.Entity("ServiceLayer.Data.Models.NbssAppointmentEvent", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("Action") + .IsRequired() + .HasMaxLength(1) + .HasColumnType("char(1)"); + + b.Property("ActionTimestamp") + .HasColumnType("datetime2"); + + b.Property("AppointmenId") + .IsRequired() + .HasMaxLength(27) + .HasColumnType("nvarchar(27)"); + + b.Property("AppointmentDateTime") + .HasColumnType("datetime2"); + + b.Property("AppointmentType") + .IsRequired() + .HasMaxLength(1) + .HasColumnType("char(1)"); + + b.Property("AttendedNotScreened") + .HasMaxLength(1) + .HasColumnType("char(1)"); + + b.Property("BSO") + .IsRequired() + .HasMaxLength(3) + .HasColumnType("char(3)"); + + b.Property("BatchId") + .IsRequired() + .HasMaxLength(9) + .HasColumnType("nvarchar(9)"); + + b.Property("BookedBy") + .IsRequired() + .HasMaxLength(1) + .HasColumnType("char(1)"); + + b.Property("CancelledBy") + .IsRequired() + .HasMaxLength(1) + .HasColumnType("char(1)"); + + b.Property("ClinicAddressLine1") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("nvarchar(30)"); + + b.Property("ClinicAddressLine2") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("nvarchar(30)"); + + b.Property("ClinicAddressLine3") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("nvarchar(30)"); + + b.Property("ClinicAddressLine4") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("nvarchar(30)"); + + b.Property("ClinicAddressLine5") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("nvarchar(30)"); + + b.Property("ClinicCode") + .IsRequired() + .HasMaxLength(5) + .HasColumnType("nvarchar(5)"); + + b.Property("ClinicName") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("nvarchar(40)"); + + b.Property("ClinicNameOnLetters") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("ClinicPostcode") + .IsRequired() + .HasMaxLength(8) + .HasColumnType("nvarchar(8)"); + + b.Property("EpisodeStart") + .HasColumnType("date"); + + b.Property("EpisodeType") + .IsRequired() + .HasMaxLength(1) + .HasColumnType("char(1)"); + + b.Property("ExtractId") + .IsRequired() + .HasMaxLength(8) + .HasColumnType("char(8)"); + + b.Property("HoldingClinic") + .HasMaxLength(1) + .HasColumnType("char(1)"); + + b.Property("Location") + .IsRequired() + .HasMaxLength(5) + .HasColumnType("nvarchar(5)"); + + b.Property("MeshFileId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("nvarchar(255)"); + + b.Property("NhsNumber") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("char(10)"); + + b.Property("ScreeningAppointmentNumber") + .HasColumnType("tinyint"); + + b.Property("Sequence") + .IsRequired() + .HasMaxLength(6) + .HasColumnType("char(6)"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(1) + .HasColumnType("char(1)"); + + b.HasKey("Id"); + + b.HasIndex("MeshFileId"); + + b.ToTable("NbssAppointmentEvents"); + }); + + modelBuilder.Entity("ServiceLayer.Data.Models.NbssAppointmentEvent", b => + { + b.HasOne("ServiceLayer.Data.Models.MeshFile", null) + .WithMany() + .HasForeignKey("MeshFileId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); #pragma warning restore 612, 618 } } diff --git a/src/ServiceLayer.Common/Data/Models/NbssAppointmentEvent.cs b/src/ServiceLayer.Common/Data/Models/NbssAppointmentEvent.cs new file mode 100644 index 0000000..966da08 --- /dev/null +++ b/src/ServiceLayer.Common/Data/Models/NbssAppointmentEvent.cs @@ -0,0 +1,75 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace ServiceLayer.Data.Models; + +public class NbssAppointmentEvent +{ + public Guid Id { get; } = Guid.NewGuid(); + [StringLength(255)] + public required string MeshFileId { get; set; } + [StringLength(3, MinimumLength = 3)] + [Column(TypeName = "char(3)")] + public required string BSO { get; set; } + [StringLength(8, MinimumLength = 8)] + [Column(TypeName = "char(8)")] + public required string ExtractId { get; set; } + [StringLength(6, MinimumLength = 6)] + [Column(TypeName = "char(6)")] + public required string Sequence { get; set; } + [StringLength(1, MinimumLength = 1)] + [Column(TypeName = "char(1)")] + public required string Action { get; set; } + [MaxLength(5)] + public required string ClinicCode { get; set; } + [StringLength(1, MinimumLength = 1)] + [Column(TypeName = "char(1)")] + public string? HoldingClinic { get; set; } + [StringLength(1, MinimumLength = 1)] + [Column(TypeName = "char(1)")] + public required string Status { get; set; } + [StringLength(1, MinimumLength = 1)] + [Column(TypeName = "char(1)")] + public string? AttendedNotScreened { get; set; } + [StringLength(27)] + public required string AppointmenId { get; set; } + [StringLength(10, MinimumLength = 10)] + [Column(TypeName = "char(10)")] + public required string NhsNumber { get; set; } + [StringLength(1, MinimumLength = 1)] + [Column(TypeName = "char(1)")] + public required string EpisodeType { get; set; } + public required DateOnly EpisodeStart { get; set; } + [StringLength(9)] + public required string BatchId { get; set; } + [StringLength(1, MinimumLength = 1)] + [Column(TypeName = "char(1)")] + public required string AppointmentType { get; set; } + public byte? ScreeningAppointmentNumber { get; set; } + [StringLength(1, MinimumLength = 1)] + [Column(TypeName = "char(1)")] + public required string BookedBy { get; set; } + [StringLength(1, MinimumLength = 1)] + [Column(TypeName = "char(1)")] + public required string CancelledBy { get; set; } + public required DateTime AppointmentDateTime { get; set; } + [StringLength(5)] + public required string Location { get; set; } + [StringLength(40)] + public required string ClinicName { get; set; } + [StringLength(50)] + public required string ClinicNameOnLetters { get; set; } + [StringLength(30)] + public required string ClinicAddressLine1 { get; set; } + [StringLength(30)] + public required string ClinicAddressLine2 { get; set; } + [StringLength(30)] + public required string ClinicAddressLine3 { get; set; } + [StringLength(30)] + public required string ClinicAddressLine4 { get; set; } + [StringLength(30)] + public required string ClinicAddressLine5 { get; set; } + [StringLength(8)] + public required string ClinicPostcode { get; set; } + public required DateTime ActionTimestamp { get; set; } +} diff --git a/src/ServiceLayer.Common/Data/ServiceLayerDbContext.cs b/src/ServiceLayer.Common/Data/ServiceLayerDbContext.cs index 418ca85..6d39aaa 100644 --- a/src/ServiceLayer.Common/Data/ServiceLayerDbContext.cs +++ b/src/ServiceLayer.Common/Data/ServiceLayerDbContext.cs @@ -6,6 +6,7 @@ namespace ServiceLayer.Data; public class ServiceLayerDbContext(DbContextOptions options) : DbContext(options) { public DbSet MeshFiles { get; set; } + public DbSet NbssAppointmentEvents { get; set; } protected override void OnModelCreating(ModelBuilder modelBuilder) { @@ -13,5 +14,11 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) modelBuilder.Entity().HasKey(p => p.FileId); modelBuilder.Entity().Property(e => e.Status).HasConversion(); modelBuilder.Entity().Property(e => e.FileType).HasConversion(); + + modelBuilder.Entity().HasKey(e => e.Id); + modelBuilder.Entity() + .HasOne() + .WithMany() + .HasForeignKey(e => e.MeshFileId); } } From 6340928af4c2f8f55809fd4b6893c4d4dba3a13a Mon Sep 17 00:00:00 2001 From: alex-clayton-1 Date: Mon, 19 May 2025 16:00:49 +0100 Subject: [PATCH 2/9] feat: Made CancelledBy nullable and recreated the migration --- ...0519134431_AddNbssAppointmentEventTable.cs | 73 ------------------- ..._AddNbssAppointmentEventTable.Designer.cs} | 3 +- ...0519145947_AddNbssAppointmentEventTable.cs | 40 ++++++++++ .../ServiceLayerDbContextModelSnapshot.cs | 1 - .../Data/Models/NbssAppointmentEvent.cs | 2 +- 5 files changed, 42 insertions(+), 77 deletions(-) delete mode 100644 src/ServiceLayer.Common/Data/Migrations/20250519134431_AddNbssAppointmentEventTable.cs rename src/ServiceLayer.Common/Data/Migrations/{20250519134431_AddNbssAppointmentEventTable.Designer.cs => 20250519145947_AddNbssAppointmentEventTable.Designer.cs} (98%) create mode 100644 src/ServiceLayer.Common/Data/Migrations/20250519145947_AddNbssAppointmentEventTable.cs diff --git a/src/ServiceLayer.Common/Data/Migrations/20250519134431_AddNbssAppointmentEventTable.cs b/src/ServiceLayer.Common/Data/Migrations/20250519134431_AddNbssAppointmentEventTable.cs deleted file mode 100644 index 0b72c68..0000000 --- a/src/ServiceLayer.Common/Data/Migrations/20250519134431_AddNbssAppointmentEventTable.cs +++ /dev/null @@ -1,73 +0,0 @@ -using System; -using Microsoft.EntityFrameworkCore.Migrations; - -#nullable disable - -namespace ServiceLayer.Mesh.Migrations -{ - /// - public partial class AddNbssAppointmentEventTable : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.CreateTable( - name: "NbssAppointmentEvents", - columns: table => new - { - Id = table.Column(type: "uniqueidentifier", nullable: false), - MeshFileId = table.Column(type: "nvarchar(255)", maxLength: 255, nullable: false), - BSO = table.Column(type: "char(3)", maxLength: 3, nullable: false), - ExtractId = table.Column(type: "char(8)", maxLength: 8, nullable: false), - Sequence = table.Column(type: "char(6)", maxLength: 6, nullable: false), - Action = table.Column(type: "char(1)", maxLength: 1, nullable: false), - ClinicCode = table.Column(type: "nvarchar(5)", maxLength: 5, nullable: false), - HoldingClinic = table.Column(type: "char(1)", maxLength: 1, nullable: true), - Status = table.Column(type: "char(1)", maxLength: 1, nullable: false), - AttendedNotScreened = table.Column(type: "char(1)", maxLength: 1, nullable: true), - AppointmenId = table.Column(type: "nvarchar(27)", maxLength: 27, nullable: false), - NhsNumber = table.Column(type: "char(10)", maxLength: 10, nullable: false), - EpisodeType = table.Column(type: "char(1)", maxLength: 1, nullable: false), - EpisodeStart = table.Column(type: "date", nullable: false), - BatchId = table.Column(type: "nvarchar(9)", maxLength: 9, nullable: false), - AppointmentType = table.Column(type: "char(1)", maxLength: 1, nullable: false), - ScreeningAppointmentNumber = table.Column(type: "tinyint", nullable: true), - BookedBy = table.Column(type: "char(1)", maxLength: 1, nullable: false), - CancelledBy = table.Column(type: "char(1)", maxLength: 1, nullable: false), - AppointmentDateTime = table.Column(type: "datetime2", nullable: false), - Location = table.Column(type: "nvarchar(5)", maxLength: 5, nullable: false), - ClinicName = table.Column(type: "nvarchar(40)", maxLength: 40, nullable: false), - ClinicNameOnLetters = table.Column(type: "nvarchar(50)", maxLength: 50, nullable: false), - ClinicAddressLine1 = table.Column(type: "nvarchar(30)", maxLength: 30, nullable: false), - ClinicAddressLine2 = table.Column(type: "nvarchar(30)", maxLength: 30, nullable: false), - ClinicAddressLine3 = table.Column(type: "nvarchar(30)", maxLength: 30, nullable: false), - ClinicAddressLine4 = table.Column(type: "nvarchar(30)", maxLength: 30, nullable: false), - ClinicAddressLine5 = table.Column(type: "nvarchar(30)", maxLength: 30, nullable: false), - ClinicPostcode = table.Column(type: "nvarchar(8)", maxLength: 8, nullable: false), - ActionTimestamp = table.Column(type: "datetime2", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_NbssAppointmentEvents", x => x.Id); - table.ForeignKey( - name: "FK_NbssAppointmentEvents_MeshFiles_MeshFileId", - column: x => x.MeshFileId, - principalTable: "MeshFiles", - principalColumn: "FileId", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateIndex( - name: "IX_NbssAppointmentEvents_MeshFileId", - table: "NbssAppointmentEvents", - column: "MeshFileId"); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropTable( - name: "NbssAppointmentEvents"); - } - } -} diff --git a/src/ServiceLayer.Common/Data/Migrations/20250519134431_AddNbssAppointmentEventTable.Designer.cs b/src/ServiceLayer.Common/Data/Migrations/20250519145947_AddNbssAppointmentEventTable.Designer.cs similarity index 98% rename from src/ServiceLayer.Common/Data/Migrations/20250519134431_AddNbssAppointmentEventTable.Designer.cs rename to src/ServiceLayer.Common/Data/Migrations/20250519145947_AddNbssAppointmentEventTable.Designer.cs index b1e0e1f..8456a55 100644 --- a/src/ServiceLayer.Common/Data/Migrations/20250519134431_AddNbssAppointmentEventTable.Designer.cs +++ b/src/ServiceLayer.Common/Data/Migrations/20250519145947_AddNbssAppointmentEventTable.Designer.cs @@ -12,7 +12,7 @@ namespace ServiceLayer.Mesh.Migrations { [DbContext(typeof(ServiceLayerDbContext))] - [Migration("20250519134431_AddNbssAppointmentEventTable")] + [Migration("20250519145947_AddNbssAppointmentEventTable")] partial class AddNbssAppointmentEventTable { /// @@ -108,7 +108,6 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) .HasColumnType("char(1)"); b.Property("CancelledBy") - .IsRequired() .HasMaxLength(1) .HasColumnType("char(1)"); diff --git a/src/ServiceLayer.Common/Data/Migrations/20250519145947_AddNbssAppointmentEventTable.cs b/src/ServiceLayer.Common/Data/Migrations/20250519145947_AddNbssAppointmentEventTable.cs new file mode 100644 index 0000000..b14bd2e --- /dev/null +++ b/src/ServiceLayer.Common/Data/Migrations/20250519145947_AddNbssAppointmentEventTable.cs @@ -0,0 +1,40 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace ServiceLayer.Mesh.Migrations +{ + /// + public partial class AddNbssAppointmentEventTable : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AlterColumn( + name: "CancelledBy", + table: "NbssAppointmentEvents", + type: "char(1)", + maxLength: 1, + nullable: true, + oldClrType: typeof(string), + oldType: "char(1)", + oldMaxLength: 1); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.AlterColumn( + name: "CancelledBy", + table: "NbssAppointmentEvents", + type: "char(1)", + maxLength: 1, + nullable: false, + defaultValue: "", + oldClrType: typeof(string), + oldType: "char(1)", + oldMaxLength: 1, + oldNullable: true); + } + } +} diff --git a/src/ServiceLayer.Common/Data/Migrations/ServiceLayerDbContextModelSnapshot.cs b/src/ServiceLayer.Common/Data/Migrations/ServiceLayerDbContextModelSnapshot.cs index bdc529f..e3d09b2 100644 --- a/src/ServiceLayer.Common/Data/Migrations/ServiceLayerDbContextModelSnapshot.cs +++ b/src/ServiceLayer.Common/Data/Migrations/ServiceLayerDbContextModelSnapshot.cs @@ -105,7 +105,6 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("char(1)"); b.Property("CancelledBy") - .IsRequired() .HasMaxLength(1) .HasColumnType("char(1)"); diff --git a/src/ServiceLayer.Common/Data/Models/NbssAppointmentEvent.cs b/src/ServiceLayer.Common/Data/Models/NbssAppointmentEvent.cs index 966da08..342e1d2 100644 --- a/src/ServiceLayer.Common/Data/Models/NbssAppointmentEvent.cs +++ b/src/ServiceLayer.Common/Data/Models/NbssAppointmentEvent.cs @@ -51,7 +51,7 @@ public class NbssAppointmentEvent public required string BookedBy { get; set; } [StringLength(1, MinimumLength = 1)] [Column(TypeName = "char(1)")] - public required string CancelledBy { get; set; } + public string? CancelledBy { get; set; } public required DateTime AppointmentDateTime { get; set; } [StringLength(5)] public required string Location { get; set; } From 8ba4f3fe225a9bb564c808dc04b2e4074a75dd6d Mon Sep 17 00:00:00 2001 From: alex-clayton-1 Date: Mon, 19 May 2025 18:04:32 +0100 Subject: [PATCH 3/9] feat: Implemented StagingPersister --- .../NbssAppointmentEvents/StagingPersister.cs | 56 +++++++++++++++++-- 1 file changed, 51 insertions(+), 5 deletions(-) diff --git a/src/ServiceLayer.Mesh/FileTypes/NbssAppointmentEvents/StagingPersister.cs b/src/ServiceLayer.Mesh/FileTypes/NbssAppointmentEvents/StagingPersister.cs index ab2efde..34aeaa6 100644 --- a/src/ServiceLayer.Mesh/FileTypes/NbssAppointmentEvents/StagingPersister.cs +++ b/src/ServiceLayer.Mesh/FileTypes/NbssAppointmentEvents/StagingPersister.cs @@ -1,13 +1,59 @@ +using ServiceLayer.Data; +using ServiceLayer.Data.Models; using ServiceLayer.Mesh.FileTypes.NbssAppointmentEvents.Models; namespace ServiceLayer.Mesh.FileTypes.NbssAppointmentEvents; -// TODO - class to take validated AppointmentEventsFile and save the records to NbssAppointmentEvents table -public class StagingPersister : IStagingPersister +public class StagingPersister(ServiceLayerDbContext dbContext) : IStagingPersister { - public Task WriteStagedData(ParsedFile parsedFile) + public async Task WriteStagedData(ParsedFile parsedFile) { - // TODO - implement this - throw new NotImplementedException(); + var nbssAppointmentEvents = MapFileDataRecordsToNbssAppointmentEvents(parsedFile); + + await dbContext.NbssAppointmentEvents.AddRangeAsync(nbssAppointmentEvents); + await dbContext.SaveChangesAsync(); + } + + private static List MapFileDataRecordsToNbssAppointmentEvents(ParsedFile parsedFile) + { + var events = new List(); + + foreach (var record in parsedFile.DataRecords) + { + events.Add(new NbssAppointmentEvent + { + MeshFileId = "", // TODO - Get this from somewhere + BSO = record.Fields["BSO"], + ExtractId = parsedFile.FileHeader.ExtractId, + Sequence = record.Fields["Sequence"], + Action = record.Fields["Action"], + ClinicCode = record.Fields["Clinic Code"], + HoldingClinic = record.Fields["Holding Clinic"], + Status = record.Fields["Status"], + AttendedNotScreened = record.Fields["Attended Not Scr"], + AppointmenId = record.Fields["Appointment ID"], + NhsNumber = record.Fields["NHS Num"], + EpisodeType = record.Fields["Epsiode Type"], + EpisodeStart = DateOnly.ParseExact(record.Fields["Episode Start"], "yyyyMMdd"), + BatchId = record.Fields["BatchID"], + AppointmentType = record.Fields["Screen or Asses"], + ScreeningAppointmentNumber = byte.Parse(record.Fields["Screen Appt num"]), + BookedBy = record.Fields["Booked By"], + CancelledBy = record.Fields["Cancelled By"], + AppointmentDateTime = DateTime.ParseExact(record.Fields["Appt Date"] + record.Fields["Appt Time"], "yyyyMMddHHmm", null), + Location = record.Fields["Location"], + ClinicName = record.Fields["Clinic Name"], + ClinicNameOnLetters = record.Fields["Clinic Name (Let)"], + ClinicAddressLine1 = record.Fields["Clinic Address 1"], + ClinicAddressLine2 = record.Fields["Clinic Address 2"], + ClinicAddressLine3 = record.Fields["Clinic Address 3"], + ClinicAddressLine4 = record.Fields["Clinic Address 4"], + ClinicAddressLine5 = record.Fields["Clinic Address 5"], + ClinicPostcode = record.Fields["Postcode"], + ActionTimestamp = DateTime.ParseExact(record.Fields["Action Timestamp"], "yyyyMMdd-HHmmss", null) + }); + } + + return events; } } From 8aee7b616804707a2f2e746f8fbaf4c00f58b748 Mon Sep 17 00:00:00 2001 From: alex-clayton-1 Date: Tue, 20 May 2025 09:58:40 +0100 Subject: [PATCH 4/9] feat: Include MeshFileId in mapping --- .../FileTypes/NbssAppointmentEvents/FileTransformer.cs | 2 +- .../NbssAppointmentEvents/IStagingPersister.cs | 4 ++-- .../NbssAppointmentEvents/StagingPersister.cs | 10 +++++----- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/ServiceLayer.Mesh/FileTypes/NbssAppointmentEvents/FileTransformer.cs b/src/ServiceLayer.Mesh/FileTypes/NbssAppointmentEvents/FileTransformer.cs index 7386817..da61b08 100644 --- a/src/ServiceLayer.Mesh/FileTypes/NbssAppointmentEvents/FileTransformer.cs +++ b/src/ServiceLayer.Mesh/FileTypes/NbssAppointmentEvents/FileTransformer.cs @@ -27,7 +27,7 @@ public async Task> TransformFileAsync(Stream stream, Mesh var validationErrors = _validationRunner.Validate(parsed); if (!validationErrors.Any()) { - await _stagingPersister.WriteStagedData(parsed); + await _stagingPersister.WriteStagedData(parsed, metaData); } return validationErrors; diff --git a/src/ServiceLayer.Mesh/FileTypes/NbssAppointmentEvents/IStagingPersister.cs b/src/ServiceLayer.Mesh/FileTypes/NbssAppointmentEvents/IStagingPersister.cs index ba38542..4ad2282 100644 --- a/src/ServiceLayer.Mesh/FileTypes/NbssAppointmentEvents/IStagingPersister.cs +++ b/src/ServiceLayer.Mesh/FileTypes/NbssAppointmentEvents/IStagingPersister.cs @@ -1,9 +1,9 @@ +using ServiceLayer.Data.Models; using ServiceLayer.Mesh.FileTypes.NbssAppointmentEvents.Models; namespace ServiceLayer.Mesh.FileTypes.NbssAppointmentEvents; -// TODO - interface for class to take validated AppointmentEventsFile and save the records to NbssAppointmentEvents table public interface IStagingPersister { - Task WriteStagedData(ParsedFile parsedFile); + Task WriteStagedData(ParsedFile parsedFile, MeshFile meshFile); } diff --git a/src/ServiceLayer.Mesh/FileTypes/NbssAppointmentEvents/StagingPersister.cs b/src/ServiceLayer.Mesh/FileTypes/NbssAppointmentEvents/StagingPersister.cs index 34aeaa6..4b60a29 100644 --- a/src/ServiceLayer.Mesh/FileTypes/NbssAppointmentEvents/StagingPersister.cs +++ b/src/ServiceLayer.Mesh/FileTypes/NbssAppointmentEvents/StagingPersister.cs @@ -6,15 +6,15 @@ namespace ServiceLayer.Mesh.FileTypes.NbssAppointmentEvents; public class StagingPersister(ServiceLayerDbContext dbContext) : IStagingPersister { - public async Task WriteStagedData(ParsedFile parsedFile) + public async Task WriteStagedData(ParsedFile parsedFile, MeshFile meshFile) { - var nbssAppointmentEvents = MapFileDataRecordsToNbssAppointmentEvents(parsedFile); + var nbssAppointmentEvents = MapFileDataRecordsToNbssAppointmentEvents(parsedFile, meshFile.FileId); await dbContext.NbssAppointmentEvents.AddRangeAsync(nbssAppointmentEvents); await dbContext.SaveChangesAsync(); } - private static List MapFileDataRecordsToNbssAppointmentEvents(ParsedFile parsedFile) + private static List MapFileDataRecordsToNbssAppointmentEvents(ParsedFile parsedFile, string fileId) { var events = new List(); @@ -22,9 +22,9 @@ private static List MapFileDataRecordsToNbssAppointmentEve { events.Add(new NbssAppointmentEvent { - MeshFileId = "", // TODO - Get this from somewhere + MeshFileId = fileId, BSO = record.Fields["BSO"], - ExtractId = parsedFile.FileHeader.ExtractId, + ExtractId = parsedFile.FileHeader!.ExtractId!, Sequence = record.Fields["Sequence"], Action = record.Fields["Action"], ClinicCode = record.Fields["Clinic Code"], From fec852b3acda16c3e6e3f62add774f5c1817b6bb Mon Sep 17 00:00:00 2001 From: alex-clayton-1 Date: Tue, 20 May 2025 15:04:46 +0100 Subject: [PATCH 5/9] test: Added test for StagingPersister --- .../NbssAppointmentEvents/StagingPersister.cs | 4 +- .../StagingPersisterTests.cs | 77 +++++++++++++++++++ 2 files changed, 79 insertions(+), 2 deletions(-) create mode 100644 tests/ServiceLayer.Mesh.Tests/FileTypes/NbssAppointmentEvents/StagingPersisterTests.cs diff --git a/src/ServiceLayer.Mesh/FileTypes/NbssAppointmentEvents/StagingPersister.cs b/src/ServiceLayer.Mesh/FileTypes/NbssAppointmentEvents/StagingPersister.cs index 4b60a29..299b5bc 100644 --- a/src/ServiceLayer.Mesh/FileTypes/NbssAppointmentEvents/StagingPersister.cs +++ b/src/ServiceLayer.Mesh/FileTypes/NbssAppointmentEvents/StagingPersister.cs @@ -33,9 +33,9 @@ private static List MapFileDataRecordsToNbssAppointmentEve AttendedNotScreened = record.Fields["Attended Not Scr"], AppointmenId = record.Fields["Appointment ID"], NhsNumber = record.Fields["NHS Num"], - EpisodeType = record.Fields["Epsiode Type"], + EpisodeType = record.Fields["Episode Type"], EpisodeStart = DateOnly.ParseExact(record.Fields["Episode Start"], "yyyyMMdd"), - BatchId = record.Fields["BatchID"], + BatchId = record.Fields["Batch ID"], AppointmentType = record.Fields["Screen or Asses"], ScreeningAppointmentNumber = byte.Parse(record.Fields["Screen Appt num"]), BookedBy = record.Fields["Booked By"], diff --git a/tests/ServiceLayer.Mesh.Tests/FileTypes/NbssAppointmentEvents/StagingPersisterTests.cs b/tests/ServiceLayer.Mesh.Tests/FileTypes/NbssAppointmentEvents/StagingPersisterTests.cs new file mode 100644 index 0000000..7159505 --- /dev/null +++ b/tests/ServiceLayer.Mesh.Tests/FileTypes/NbssAppointmentEvents/StagingPersisterTests.cs @@ -0,0 +1,77 @@ +using Microsoft.EntityFrameworkCore; +using ServiceLayer.Data; +using ServiceLayer.Data.Models; +using ServiceLayer.Mesh.FileTypes.NbssAppointmentEvents; + +namespace ServiceLayer.Mesh.Tests.FileTypes.NbssAppointmentEvents; + +public class NbssAppointmentEventsTests +{ + private readonly ServiceLayerDbContext _dbContext; + private readonly StagingPersister _stagingPersister; + + public NbssAppointmentEventsTests() + { + var options = new DbContextOptionsBuilder() + .UseInMemoryDatabase(databaseName: Guid.NewGuid().ToString()) + .ConfigureWarnings(warnings => + warnings.Ignore(Microsoft.EntityFrameworkCore.Diagnostics.InMemoryEventId.TransactionIgnoredWarning)) + .Options; + + _dbContext = new ServiceLayerDbContext(options); + + _stagingPersister = new StagingPersister(_dbContext); + } + + [Fact] + public async Task WriteStagedData_WhenMappingSuceeds_SavesToDb() + { + // Arrange + var parsedFile = TestDataBuilder.BuildValidParsedFile(); + var meshFile = new MeshFile() + { + FileId = "1", + FileType = MeshFileType.NbssAppointmentEvents, + MailboxId = "ABC", + Status = MeshFileStatus.Transforming + }; + + // Act + await _stagingPersister.WriteStagedData(parsedFile, meshFile); + + // Assert + Assert.Equal(3, await _dbContext.NbssAppointmentEvents.CountAsync()); + + var nbssAppointmentEvent = await _dbContext.NbssAppointmentEvents.FirstAsync(); + var dataRecord = parsedFile.DataRecords.First(); + Assert.Equal(meshFile.FileId, nbssAppointmentEvent.MeshFileId); + Assert.Equal(dataRecord["BSO"], nbssAppointmentEvent.BSO); + Assert.Equal(parsedFile.FileTrailer!.ExtractId, nbssAppointmentEvent.ExtractId); + Assert.Equal(dataRecord["Sequence"], nbssAppointmentEvent.Sequence); + Assert.Equal(dataRecord["Action"], nbssAppointmentEvent.Action); + Assert.Equal(dataRecord["Clinic Code"], nbssAppointmentEvent.ClinicCode); + Assert.Equal(dataRecord["Holding Clinic"], nbssAppointmentEvent.HoldingClinic); + Assert.Equal(dataRecord["Status"], nbssAppointmentEvent.Status); + Assert.Equal(dataRecord["Attended Not Scr"], nbssAppointmentEvent.AttendedNotScreened); + Assert.Equal(dataRecord["Appointment ID"], nbssAppointmentEvent.AppointmenId); + Assert.Equal(dataRecord["NHS Num"], nbssAppointmentEvent.NhsNumber); + Assert.Equal(dataRecord["Episode Type"], nbssAppointmentEvent.EpisodeType); + Assert.Equal(DateOnly.ParseExact(dataRecord.Fields["Episode Start"], "yyyyMMdd"), nbssAppointmentEvent.EpisodeStart); + Assert.Equal(dataRecord["Batch ID"], nbssAppointmentEvent.BatchId); + Assert.Equal(dataRecord["Screen or Asses"], nbssAppointmentEvent.AppointmentType); + Assert.Equal(byte.Parse(dataRecord.Fields["Screen Appt num"]), nbssAppointmentEvent.ScreeningAppointmentNumber); + Assert.Equal(dataRecord["Booked By"], nbssAppointmentEvent.BookedBy); + Assert.Equal(dataRecord["Cancelled By"], nbssAppointmentEvent.CancelledBy); + Assert.Equal(DateTime.ParseExact(dataRecord.Fields["Appt Date"] + dataRecord.Fields["Appt Time"], "yyyyMMddHHmm", null), nbssAppointmentEvent.AppointmentDateTime); + Assert.Equal(dataRecord["Location"], nbssAppointmentEvent.Location); + Assert.Equal(dataRecord["Clinic Name"], nbssAppointmentEvent.ClinicName); + Assert.Equal(dataRecord["Clinic Name (Let)"], nbssAppointmentEvent.ClinicNameOnLetters); + Assert.Equal(dataRecord["Clinic Address 1"], nbssAppointmentEvent.ClinicAddressLine1); + Assert.Equal(dataRecord["Clinic Address 2"], nbssAppointmentEvent.ClinicAddressLine2); + Assert.Equal(dataRecord["Clinic Address 3"], nbssAppointmentEvent.ClinicAddressLine3); + Assert.Equal(dataRecord["Clinic Address 4"], nbssAppointmentEvent.ClinicAddressLine4); + Assert.Equal(dataRecord["Clinic Address 5"], nbssAppointmentEvent.ClinicAddressLine5); + Assert.Equal(dataRecord["Postcode"], nbssAppointmentEvent.ClinicPostcode); + Assert.Equal(DateTime.ParseExact(dataRecord.Fields["Action Timestamp"], "yyyyMMdd-HHmmss", null), nbssAppointmentEvent.ActionTimestamp); + } +} From 8b319ef256e8fae63c53c61c45df5ad6d37b61e4 Mon Sep 17 00:00:00 2001 From: alex-clayton-1 Date: Tue, 20 May 2025 16:05:09 +0100 Subject: [PATCH 6/9] test: Added more tests --- .../StagingPersisterTests.cs | 79 ++++++++++++++++++- 1 file changed, 75 insertions(+), 4 deletions(-) diff --git a/tests/ServiceLayer.Mesh.Tests/FileTypes/NbssAppointmentEvents/StagingPersisterTests.cs b/tests/ServiceLayer.Mesh.Tests/FileTypes/NbssAppointmentEvents/StagingPersisterTests.cs index 7159505..3d57a2b 100644 --- a/tests/ServiceLayer.Mesh.Tests/FileTypes/NbssAppointmentEvents/StagingPersisterTests.cs +++ b/tests/ServiceLayer.Mesh.Tests/FileTypes/NbssAppointmentEvents/StagingPersisterTests.cs @@ -13,9 +13,7 @@ public class NbssAppointmentEventsTests public NbssAppointmentEventsTests() { var options = new DbContextOptionsBuilder() - .UseInMemoryDatabase(databaseName: Guid.NewGuid().ToString()) - .ConfigureWarnings(warnings => - warnings.Ignore(Microsoft.EntityFrameworkCore.Diagnostics.InMemoryEventId.TransactionIgnoredWarning)) + .UseInMemoryDatabase(Guid.NewGuid().ToString()) .Options; _dbContext = new ServiceLayerDbContext(options); @@ -24,7 +22,7 @@ public NbssAppointmentEventsTests() } [Fact] - public async Task WriteStagedData_WhenMappingSuceeds_SavesToDb() + public async Task WriteStagedData_WhenFileValid_SavesToDb() { // Arrange var parsedFile = TestDataBuilder.BuildValidParsedFile(); @@ -44,6 +42,7 @@ public async Task WriteStagedData_WhenMappingSuceeds_SavesToDb() var nbssAppointmentEvent = await _dbContext.NbssAppointmentEvents.FirstAsync(); var dataRecord = parsedFile.DataRecords.First(); + Assert.NotEqual(Guid.Empty, nbssAppointmentEvent.Id); Assert.Equal(meshFile.FileId, nbssAppointmentEvent.MeshFileId); Assert.Equal(dataRecord["BSO"], nbssAppointmentEvent.BSO); Assert.Equal(parsedFile.FileTrailer!.ExtractId, nbssAppointmentEvent.ExtractId); @@ -74,4 +73,76 @@ public async Task WriteStagedData_WhenMappingSuceeds_SavesToDb() Assert.Equal(dataRecord["Postcode"], nbssAppointmentEvent.ClinicPostcode); Assert.Equal(DateTime.ParseExact(dataRecord.Fields["Action Timestamp"], "yyyyMMdd-HHmmss", null), nbssAppointmentEvent.ActionTimestamp); } + + [Theory] + [InlineData("BSO")] + [InlineData("Sequence")] + [InlineData("Action")] + [InlineData("Clinic Code")] + [InlineData("Holding Clinic")] + [InlineData("Status")] + [InlineData("Attended Not Scr")] + [InlineData("Appointment ID")] + [InlineData("NHS Num")] + [InlineData("Episode Type")] + [InlineData("Episode Start")] + [InlineData("Batch ID")] + [InlineData("Screen or Asses")] + [InlineData("Screen Appt num")] + [InlineData("Booked By")] + [InlineData("Cancelled By")] + [InlineData("Appt Date")] + [InlineData("Appt Time")] + [InlineData("Location")] + [InlineData("Clinic Name")] + [InlineData("Clinic Name (Let)")] + [InlineData("Clinic Address 1")] + [InlineData("Clinic Address 2")] + [InlineData("Clinic Address 3")] + [InlineData("Clinic Address 4")] + [InlineData("Clinic Address 5")] + [InlineData("Postcode")] + [InlineData("Action Timestamp")] + public async Task WriteStagedData_WhenFieldMissing_DoesNotSaveToDb(string fieldName) + { + // Arrange + var parsedFile = TestDataBuilder.BuildValidParsedFile(); + var recordWithoutBSO = TestDataBuilder.BuildFileDataRecordWithField(fieldName, null, 1); + parsedFile.DataRecords.Add(recordWithoutBSO); + var meshFile = new MeshFile() + { + FileId = "1", + FileType = MeshFileType.NbssAppointmentEvents, + MailboxId = "ABC", + Status = MeshFileStatus.Transforming + }; + + // Act & Assert + var exception = await Assert.ThrowsAsync(async () => await _stagingPersister.WriteStagedData(parsedFile, meshFile)); + Assert.Equal(0, await _dbContext.NbssAppointmentEvents.CountAsync()); + } + + [Theory] + [InlineData("Episode Start")] + [InlineData("Screen Appt num")] + [InlineData("Appt Date")] + [InlineData("Appt Time")] + public async Task WriteStagedData_WhenFieldHoldsInvalidValue_DoesNotSaveToDb(string fieldName) + { + // Arrange + var parsedFile = TestDataBuilder.BuildValidParsedFile(); + var recordWithoutBSO = TestDataBuilder.BuildFileDataRecordWithField(fieldName, "Invalid value", 1); + parsedFile.DataRecords.Add(recordWithoutBSO); + var meshFile = new MeshFile() + { + FileId = "1", + FileType = MeshFileType.NbssAppointmentEvents, + MailboxId = "ABC", + Status = MeshFileStatus.Transforming + }; + + // Act & Assert + var exception = await Assert.ThrowsAsync(async () => await _stagingPersister.WriteStagedData(parsedFile, meshFile)); + Assert.Equal(0, await _dbContext.NbssAppointmentEvents.CountAsync()); + } } From 47f62dc23a267478d9ac21772f7b3b2d107903bd Mon Sep 17 00:00:00 2001 From: alex-clayton-1 Date: Tue, 20 May 2025 16:33:15 +0100 Subject: [PATCH 7/9] refactor: Replaced foreach loop with linq expression and using invariant culture for DateTime Parsing --- .../NbssAppointmentEvents/StagingPersister.cs | 70 +++++++++---------- 1 file changed, 32 insertions(+), 38 deletions(-) diff --git a/src/ServiceLayer.Mesh/FileTypes/NbssAppointmentEvents/StagingPersister.cs b/src/ServiceLayer.Mesh/FileTypes/NbssAppointmentEvents/StagingPersister.cs index 299b5bc..10573f2 100644 --- a/src/ServiceLayer.Mesh/FileTypes/NbssAppointmentEvents/StagingPersister.cs +++ b/src/ServiceLayer.Mesh/FileTypes/NbssAppointmentEvents/StagingPersister.cs @@ -1,3 +1,4 @@ +using System.Globalization; using ServiceLayer.Data; using ServiceLayer.Data.Models; using ServiceLayer.Mesh.FileTypes.NbssAppointmentEvents.Models; @@ -16,44 +17,37 @@ public async Task WriteStagedData(ParsedFile parsedFile, MeshFile meshFile) private static List MapFileDataRecordsToNbssAppointmentEvents(ParsedFile parsedFile, string fileId) { - var events = new List(); - - foreach (var record in parsedFile.DataRecords) + return [.. parsedFile.DataRecords.Select(record => new NbssAppointmentEvent { - events.Add(new NbssAppointmentEvent - { - MeshFileId = fileId, - BSO = record.Fields["BSO"], - ExtractId = parsedFile.FileHeader!.ExtractId!, - Sequence = record.Fields["Sequence"], - Action = record.Fields["Action"], - ClinicCode = record.Fields["Clinic Code"], - HoldingClinic = record.Fields["Holding Clinic"], - Status = record.Fields["Status"], - AttendedNotScreened = record.Fields["Attended Not Scr"], - AppointmenId = record.Fields["Appointment ID"], - NhsNumber = record.Fields["NHS Num"], - EpisodeType = record.Fields["Episode Type"], - EpisodeStart = DateOnly.ParseExact(record.Fields["Episode Start"], "yyyyMMdd"), - BatchId = record.Fields["Batch ID"], - AppointmentType = record.Fields["Screen or Asses"], - ScreeningAppointmentNumber = byte.Parse(record.Fields["Screen Appt num"]), - BookedBy = record.Fields["Booked By"], - CancelledBy = record.Fields["Cancelled By"], - AppointmentDateTime = DateTime.ParseExact(record.Fields["Appt Date"] + record.Fields["Appt Time"], "yyyyMMddHHmm", null), - Location = record.Fields["Location"], - ClinicName = record.Fields["Clinic Name"], - ClinicNameOnLetters = record.Fields["Clinic Name (Let)"], - ClinicAddressLine1 = record.Fields["Clinic Address 1"], - ClinicAddressLine2 = record.Fields["Clinic Address 2"], - ClinicAddressLine3 = record.Fields["Clinic Address 3"], - ClinicAddressLine4 = record.Fields["Clinic Address 4"], - ClinicAddressLine5 = record.Fields["Clinic Address 5"], - ClinicPostcode = record.Fields["Postcode"], - ActionTimestamp = DateTime.ParseExact(record.Fields["Action Timestamp"], "yyyyMMdd-HHmmss", null) - }); - } - - return events; + MeshFileId = fileId, + BSO = record.Fields["BSO"], + ExtractId = parsedFile.FileHeader!.ExtractId!, + Sequence = record.Fields["Sequence"], + Action = record.Fields["Action"], + ClinicCode = record.Fields["Clinic Code"], + HoldingClinic = record.Fields["Holding Clinic"], + Status = record.Fields["Status"], + AttendedNotScreened = record.Fields["Attended Not Scr"], + AppointmenId = record.Fields["Appointment ID"], + NhsNumber = record.Fields["NHS Num"], + EpisodeType = record.Fields["Episode Type"], + EpisodeStart = DateOnly.ParseExact(record.Fields["Episode Start"], "yyyyMMdd", CultureInfo.InvariantCulture), + BatchId = record.Fields["Batch ID"], + AppointmentType = record.Fields["Screen or Asses"], + ScreeningAppointmentNumber = byte.Parse(record.Fields["Screen Appt num"]), + BookedBy = record.Fields["Booked By"], + CancelledBy = record.Fields["Cancelled By"], + AppointmentDateTime = DateTime.ParseExact(record.Fields["Appt Date"] + record.Fields["Appt Time"], "yyyyMMddHHmm", CultureInfo.InvariantCulture), + Location = record.Fields["Location"], + ClinicName = record.Fields["Clinic Name"], + ClinicNameOnLetters = record.Fields["Clinic Name (Let)"], + ClinicAddressLine1 = record.Fields["Clinic Address 1"], + ClinicAddressLine2 = record.Fields["Clinic Address 2"], + ClinicAddressLine3 = record.Fields["Clinic Address 3"], + ClinicAddressLine4 = record.Fields["Clinic Address 4"], + ClinicAddressLine5 = record.Fields["Clinic Address 5"], + ClinicPostcode = record.Fields["Postcode"], + ActionTimestamp = DateTime.ParseExact(record.Fields["Action Timestamp"], "yyyyMMdd-HHmmss", CultureInfo.InvariantCulture) + })]; } } From f4c3aba1d91e9c6ee2f233e453dbb85eb9e1909b Mon Sep 17 00:00:00 2001 From: alex-clayton-1 Date: Wed, 21 May 2025 10:48:55 +0100 Subject: [PATCH 8/9] feat: Updated sql data types for NbssApointmentEvent columns and recreated migration --- ...0519145947_AddNbssAppointmentEventTable.cs | 40 ---------- ..._AddNbssAppointmentEventTable.Designer.cs} | 30 ++++---- ...0521094738_AddNbssAppointmentEventTable.cs | 73 +++++++++++++++++++ .../ServiceLayerDbContextModelSnapshot.cs | 28 +++---- .../Data/Models/NbssAppointmentEvent.cs | 16 +++- 5 files changed, 117 insertions(+), 70 deletions(-) delete mode 100644 src/ServiceLayer.Common/Data/Migrations/20250519145947_AddNbssAppointmentEventTable.cs rename src/ServiceLayer.Common/Data/Migrations/{20250519145947_AddNbssAppointmentEventTable.Designer.cs => 20250521094738_AddNbssAppointmentEventTable.Designer.cs} (89%) create mode 100644 src/ServiceLayer.Common/Data/Migrations/20250521094738_AddNbssAppointmentEventTable.cs diff --git a/src/ServiceLayer.Common/Data/Migrations/20250519145947_AddNbssAppointmentEventTable.cs b/src/ServiceLayer.Common/Data/Migrations/20250519145947_AddNbssAppointmentEventTable.cs deleted file mode 100644 index b14bd2e..0000000 --- a/src/ServiceLayer.Common/Data/Migrations/20250519145947_AddNbssAppointmentEventTable.cs +++ /dev/null @@ -1,40 +0,0 @@ -using Microsoft.EntityFrameworkCore.Migrations; - -#nullable disable - -namespace ServiceLayer.Mesh.Migrations -{ - /// - public partial class AddNbssAppointmentEventTable : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.AlterColumn( - name: "CancelledBy", - table: "NbssAppointmentEvents", - type: "char(1)", - maxLength: 1, - nullable: true, - oldClrType: typeof(string), - oldType: "char(1)", - oldMaxLength: 1); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.AlterColumn( - name: "CancelledBy", - table: "NbssAppointmentEvents", - type: "char(1)", - maxLength: 1, - nullable: false, - defaultValue: "", - oldClrType: typeof(string), - oldType: "char(1)", - oldMaxLength: 1, - oldNullable: true); - } - } -} diff --git a/src/ServiceLayer.Common/Data/Migrations/20250519145947_AddNbssAppointmentEventTable.Designer.cs b/src/ServiceLayer.Common/Data/Migrations/20250521094738_AddNbssAppointmentEventTable.Designer.cs similarity index 89% rename from src/ServiceLayer.Common/Data/Migrations/20250519145947_AddNbssAppointmentEventTable.Designer.cs rename to src/ServiceLayer.Common/Data/Migrations/20250521094738_AddNbssAppointmentEventTable.Designer.cs index 8456a55..b3f4862 100644 --- a/src/ServiceLayer.Common/Data/Migrations/20250519145947_AddNbssAppointmentEventTable.Designer.cs +++ b/src/ServiceLayer.Common/Data/Migrations/20250521094738_AddNbssAppointmentEventTable.Designer.cs @@ -12,7 +12,7 @@ namespace ServiceLayer.Mesh.Migrations { [DbContext(typeof(ServiceLayerDbContext))] - [Migration("20250519145947_AddNbssAppointmentEventTable")] + [Migration("20250521094738_AddNbssAppointmentEventTable")] partial class AddNbssAppointmentEventTable { /// @@ -73,15 +73,15 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) .HasColumnType("char(1)"); b.Property("ActionTimestamp") - .HasColumnType("datetime2"); + .HasColumnType("datetime2(0)"); b.Property("AppointmenId") .IsRequired() .HasMaxLength(27) - .HasColumnType("nvarchar(27)"); + .HasColumnType("varchar(27)"); b.Property("AppointmentDateTime") - .HasColumnType("datetime2"); + .HasColumnType("datetime2(0)"); b.Property("AppointmentType") .IsRequired() @@ -100,7 +100,7 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b.Property("BatchId") .IsRequired() .HasMaxLength(9) - .HasColumnType("nvarchar(9)"); + .HasColumnType("varchar(9)"); b.Property("BookedBy") .IsRequired() @@ -114,47 +114,47 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b.Property("ClinicAddressLine1") .IsRequired() .HasMaxLength(30) - .HasColumnType("nvarchar(30)"); + .HasColumnType("varchar(30)"); b.Property("ClinicAddressLine2") .IsRequired() .HasMaxLength(30) - .HasColumnType("nvarchar(30)"); + .HasColumnType("varchar(30)"); b.Property("ClinicAddressLine3") .IsRequired() .HasMaxLength(30) - .HasColumnType("nvarchar(30)"); + .HasColumnType("varchar(30)"); b.Property("ClinicAddressLine4") .IsRequired() .HasMaxLength(30) - .HasColumnType("nvarchar(30)"); + .HasColumnType("varchar(30)"); b.Property("ClinicAddressLine5") .IsRequired() .HasMaxLength(30) - .HasColumnType("nvarchar(30)"); + .HasColumnType("varchar(30)"); b.Property("ClinicCode") .IsRequired() .HasMaxLength(5) - .HasColumnType("nvarchar(5)"); + .HasColumnType("varchar(5)"); b.Property("ClinicName") .IsRequired() .HasMaxLength(40) - .HasColumnType("nvarchar(40)"); + .HasColumnType("varchar(40)"); b.Property("ClinicNameOnLetters") .IsRequired() .HasMaxLength(50) - .HasColumnType("nvarchar(50)"); + .HasColumnType("varchar(50)"); b.Property("ClinicPostcode") .IsRequired() .HasMaxLength(8) - .HasColumnType("nvarchar(8)"); + .HasColumnType("varchar(8)"); b.Property("EpisodeStart") .HasColumnType("date"); @@ -176,7 +176,7 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b.Property("Location") .IsRequired() .HasMaxLength(5) - .HasColumnType("nvarchar(5)"); + .HasColumnType("varchar(5)"); b.Property("MeshFileId") .IsRequired() diff --git a/src/ServiceLayer.Common/Data/Migrations/20250521094738_AddNbssAppointmentEventTable.cs b/src/ServiceLayer.Common/Data/Migrations/20250521094738_AddNbssAppointmentEventTable.cs new file mode 100644 index 0000000..b10fee0 --- /dev/null +++ b/src/ServiceLayer.Common/Data/Migrations/20250521094738_AddNbssAppointmentEventTable.cs @@ -0,0 +1,73 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace ServiceLayer.Mesh.Migrations +{ + /// + public partial class AddNbssAppointmentEventTable : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "NbssAppointmentEvents", + columns: table => new + { + Id = table.Column(type: "uniqueidentifier", nullable: false), + MeshFileId = table.Column(type: "nvarchar(255)", maxLength: 255, nullable: false), + BSO = table.Column(type: "char(3)", maxLength: 3, nullable: false), + ExtractId = table.Column(type: "char(8)", maxLength: 8, nullable: false), + Sequence = table.Column(type: "char(6)", maxLength: 6, nullable: false), + Action = table.Column(type: "char(1)", maxLength: 1, nullable: false), + ClinicCode = table.Column(type: "varchar(5)", maxLength: 5, nullable: false), + HoldingClinic = table.Column(type: "char(1)", maxLength: 1, nullable: true), + Status = table.Column(type: "char(1)", maxLength: 1, nullable: false), + AttendedNotScreened = table.Column(type: "char(1)", maxLength: 1, nullable: true), + AppointmenId = table.Column(type: "varchar(27)", maxLength: 27, nullable: false), + NhsNumber = table.Column(type: "char(10)", maxLength: 10, nullable: false), + EpisodeType = table.Column(type: "char(1)", maxLength: 1, nullable: false), + EpisodeStart = table.Column(type: "date", nullable: false), + BatchId = table.Column(type: "varchar(9)", maxLength: 9, nullable: false), + AppointmentType = table.Column(type: "char(1)", maxLength: 1, nullable: false), + ScreeningAppointmentNumber = table.Column(type: "tinyint", nullable: true), + BookedBy = table.Column(type: "char(1)", maxLength: 1, nullable: false), + CancelledBy = table.Column(type: "char(1)", maxLength: 1, nullable: true), + AppointmentDateTime = table.Column(type: "datetime2(0)", nullable: false), + Location = table.Column(type: "varchar(5)", maxLength: 5, nullable: false), + ClinicName = table.Column(type: "varchar(40)", maxLength: 40, nullable: false), + ClinicNameOnLetters = table.Column(type: "varchar(50)", maxLength: 50, nullable: false), + ClinicAddressLine1 = table.Column(type: "varchar(30)", maxLength: 30, nullable: false), + ClinicAddressLine2 = table.Column(type: "varchar(30)", maxLength: 30, nullable: false), + ClinicAddressLine3 = table.Column(type: "varchar(30)", maxLength: 30, nullable: false), + ClinicAddressLine4 = table.Column(type: "varchar(30)", maxLength: 30, nullable: false), + ClinicAddressLine5 = table.Column(type: "varchar(30)", maxLength: 30, nullable: false), + ClinicPostcode = table.Column(type: "varchar(8)", maxLength: 8, nullable: false), + ActionTimestamp = table.Column(type: "datetime2(0)", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_NbssAppointmentEvents", x => x.Id); + table.ForeignKey( + name: "FK_NbssAppointmentEvents_MeshFiles_MeshFileId", + column: x => x.MeshFileId, + principalTable: "MeshFiles", + principalColumn: "FileId", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_NbssAppointmentEvents_MeshFileId", + table: "NbssAppointmentEvents", + column: "MeshFileId"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "NbssAppointmentEvents"); + } + } +} diff --git a/src/ServiceLayer.Common/Data/Migrations/ServiceLayerDbContextModelSnapshot.cs b/src/ServiceLayer.Common/Data/Migrations/ServiceLayerDbContextModelSnapshot.cs index e3d09b2..4a05938 100644 --- a/src/ServiceLayer.Common/Data/Migrations/ServiceLayerDbContextModelSnapshot.cs +++ b/src/ServiceLayer.Common/Data/Migrations/ServiceLayerDbContextModelSnapshot.cs @@ -70,15 +70,15 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("char(1)"); b.Property("ActionTimestamp") - .HasColumnType("datetime2"); + .HasColumnType("datetime2(0)"); b.Property("AppointmenId") .IsRequired() .HasMaxLength(27) - .HasColumnType("nvarchar(27)"); + .HasColumnType("varchar(27)"); b.Property("AppointmentDateTime") - .HasColumnType("datetime2"); + .HasColumnType("datetime2(0)"); b.Property("AppointmentType") .IsRequired() @@ -97,7 +97,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("BatchId") .IsRequired() .HasMaxLength(9) - .HasColumnType("nvarchar(9)"); + .HasColumnType("varchar(9)"); b.Property("BookedBy") .IsRequired() @@ -111,47 +111,47 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("ClinicAddressLine1") .IsRequired() .HasMaxLength(30) - .HasColumnType("nvarchar(30)"); + .HasColumnType("varchar(30)"); b.Property("ClinicAddressLine2") .IsRequired() .HasMaxLength(30) - .HasColumnType("nvarchar(30)"); + .HasColumnType("varchar(30)"); b.Property("ClinicAddressLine3") .IsRequired() .HasMaxLength(30) - .HasColumnType("nvarchar(30)"); + .HasColumnType("varchar(30)"); b.Property("ClinicAddressLine4") .IsRequired() .HasMaxLength(30) - .HasColumnType("nvarchar(30)"); + .HasColumnType("varchar(30)"); b.Property("ClinicAddressLine5") .IsRequired() .HasMaxLength(30) - .HasColumnType("nvarchar(30)"); + .HasColumnType("varchar(30)"); b.Property("ClinicCode") .IsRequired() .HasMaxLength(5) - .HasColumnType("nvarchar(5)"); + .HasColumnType("varchar(5)"); b.Property("ClinicName") .IsRequired() .HasMaxLength(40) - .HasColumnType("nvarchar(40)"); + .HasColumnType("varchar(40)"); b.Property("ClinicNameOnLetters") .IsRequired() .HasMaxLength(50) - .HasColumnType("nvarchar(50)"); + .HasColumnType("varchar(50)"); b.Property("ClinicPostcode") .IsRequired() .HasMaxLength(8) - .HasColumnType("nvarchar(8)"); + .HasColumnType("varchar(8)"); b.Property("EpisodeStart") .HasColumnType("date"); @@ -173,7 +173,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("Location") .IsRequired() .HasMaxLength(5) - .HasColumnType("nvarchar(5)"); + .HasColumnType("varchar(5)"); b.Property("MeshFileId") .IsRequired() diff --git a/src/ServiceLayer.Common/Data/Models/NbssAppointmentEvent.cs b/src/ServiceLayer.Common/Data/Models/NbssAppointmentEvent.cs index 342e1d2..7a68760 100644 --- a/src/ServiceLayer.Common/Data/Models/NbssAppointmentEvent.cs +++ b/src/ServiceLayer.Common/Data/Models/NbssAppointmentEvent.cs @@ -20,7 +20,8 @@ public class NbssAppointmentEvent [StringLength(1, MinimumLength = 1)] [Column(TypeName = "char(1)")] public required string Action { get; set; } - [MaxLength(5)] + [StringLength(5)] + [Column(TypeName = "varchar(5)")] public required string ClinicCode { get; set; } [StringLength(1, MinimumLength = 1)] [Column(TypeName = "char(1)")] @@ -32,6 +33,7 @@ public class NbssAppointmentEvent [Column(TypeName = "char(1)")] public string? AttendedNotScreened { get; set; } [StringLength(27)] + [Column(TypeName = "varchar(27)")] public required string AppointmenId { get; set; } [StringLength(10, MinimumLength = 10)] [Column(TypeName = "char(10)")] @@ -41,6 +43,7 @@ public class NbssAppointmentEvent public required string EpisodeType { get; set; } public required DateOnly EpisodeStart { get; set; } [StringLength(9)] + [Column(TypeName = "varchar(9)")] public required string BatchId { get; set; } [StringLength(1, MinimumLength = 1)] [Column(TypeName = "char(1)")] @@ -52,24 +55,35 @@ public class NbssAppointmentEvent [StringLength(1, MinimumLength = 1)] [Column(TypeName = "char(1)")] public string? CancelledBy { get; set; } + [Column(TypeName = "datetime2(0)")] public required DateTime AppointmentDateTime { get; set; } [StringLength(5)] + [Column(TypeName = "varchar(5)")] public required string Location { get; set; } [StringLength(40)] + [Column(TypeName = "varchar(40)")] public required string ClinicName { get; set; } [StringLength(50)] + [Column(TypeName = "varchar(50)")] public required string ClinicNameOnLetters { get; set; } [StringLength(30)] + [Column(TypeName = "varchar(30)")] public required string ClinicAddressLine1 { get; set; } [StringLength(30)] + [Column(TypeName = "varchar(30)")] public required string ClinicAddressLine2 { get; set; } [StringLength(30)] + [Column(TypeName = "varchar(30)")] public required string ClinicAddressLine3 { get; set; } [StringLength(30)] + [Column(TypeName = "varchar(30)")] public required string ClinicAddressLine4 { get; set; } [StringLength(30)] + [Column(TypeName = "varchar(30)")] public required string ClinicAddressLine5 { get; set; } [StringLength(8)] + [Column(TypeName = "varchar(8)")] public required string ClinicPostcode { get; set; } + [Column(TypeName = "datetime2(0)")] public required DateTime ActionTimestamp { get; set; } } From 2a9b54df63a27d67df3250e2df10459bf403273c Mon Sep 17 00:00:00 2001 From: alex-clayton-1 Date: Wed, 21 May 2025 12:47:33 +0100 Subject: [PATCH 9/9] feat: Map empty strings to null for optional fields --- .../NbssAppointmentEvents/StagingPersister.cs | 12 +- .../StagingPersisterTests.cs | 105 ++++++++++++------ 2 files changed, 77 insertions(+), 40 deletions(-) diff --git a/src/ServiceLayer.Mesh/FileTypes/NbssAppointmentEvents/StagingPersister.cs b/src/ServiceLayer.Mesh/FileTypes/NbssAppointmentEvents/StagingPersister.cs index 10573f2..650c9aa 100644 --- a/src/ServiceLayer.Mesh/FileTypes/NbssAppointmentEvents/StagingPersister.cs +++ b/src/ServiceLayer.Mesh/FileTypes/NbssAppointmentEvents/StagingPersister.cs @@ -25,18 +25,18 @@ private static List MapFileDataRecordsToNbssAppointmentEve Sequence = record.Fields["Sequence"], Action = record.Fields["Action"], ClinicCode = record.Fields["Clinic Code"], - HoldingClinic = record.Fields["Holding Clinic"], + HoldingClinic = NullIfWhiteSpace(record.Fields["Holding Clinic"]), Status = record.Fields["Status"], - AttendedNotScreened = record.Fields["Attended Not Scr"], + AttendedNotScreened = NullIfWhiteSpace(record.Fields["Attended Not Scr"]), AppointmenId = record.Fields["Appointment ID"], NhsNumber = record.Fields["NHS Num"], EpisodeType = record.Fields["Episode Type"], EpisodeStart = DateOnly.ParseExact(record.Fields["Episode Start"], "yyyyMMdd", CultureInfo.InvariantCulture), BatchId = record.Fields["Batch ID"], AppointmentType = record.Fields["Screen or Asses"], - ScreeningAppointmentNumber = byte.Parse(record.Fields["Screen Appt num"]), + ScreeningAppointmentNumber = NullByteIfWhiteSpace(record.Fields["Screen Appt num"]), BookedBy = record.Fields["Booked By"], - CancelledBy = record.Fields["Cancelled By"], + CancelledBy = NullIfWhiteSpace(record.Fields["Cancelled By"]), AppointmentDateTime = DateTime.ParseExact(record.Fields["Appt Date"] + record.Fields["Appt Time"], "yyyyMMddHHmm", CultureInfo.InvariantCulture), Location = record.Fields["Location"], ClinicName = record.Fields["Clinic Name"], @@ -50,4 +50,8 @@ private static List MapFileDataRecordsToNbssAppointmentEve ActionTimestamp = DateTime.ParseExact(record.Fields["Action Timestamp"], "yyyyMMdd-HHmmss", CultureInfo.InvariantCulture) })]; } + + private static string? NullIfWhiteSpace(string input) => string.IsNullOrWhiteSpace(input) ? null : input; + + private static byte? NullByteIfWhiteSpace(string input) => string.IsNullOrWhiteSpace(input) ? null : byte.Parse(input); } diff --git a/tests/ServiceLayer.Mesh.Tests/FileTypes/NbssAppointmentEvents/StagingPersisterTests.cs b/tests/ServiceLayer.Mesh.Tests/FileTypes/NbssAppointmentEvents/StagingPersisterTests.cs index 3d57a2b..2b775ec 100644 --- a/tests/ServiceLayer.Mesh.Tests/FileTypes/NbssAppointmentEvents/StagingPersisterTests.cs +++ b/tests/ServiceLayer.Mesh.Tests/FileTypes/NbssAppointmentEvents/StagingPersisterTests.cs @@ -22,10 +22,13 @@ public NbssAppointmentEventsTests() } [Fact] - public async Task WriteStagedData_WhenFileValid_SavesToDb() + public async Task WriteStagedData_WhenFileValid_MapsFieldsAndSavesToDb() { // Arrange - var parsedFile = TestDataBuilder.BuildValidParsedFile(); + var parsedFile = TestDataBuilder.BuildValidParsedFile(3); + var firstRecord = parsedFile.DataRecords[0]; + firstRecord.Fields["Attended Not Scr"] = "Y"; + firstRecord.Fields["Cancelled By"] = "C"; var meshFile = new MeshFile() { FileId = "1", @@ -41,37 +44,66 @@ public async Task WriteStagedData_WhenFileValid_SavesToDb() Assert.Equal(3, await _dbContext.NbssAppointmentEvents.CountAsync()); var nbssAppointmentEvent = await _dbContext.NbssAppointmentEvents.FirstAsync(); - var dataRecord = parsedFile.DataRecords.First(); Assert.NotEqual(Guid.Empty, nbssAppointmentEvent.Id); Assert.Equal(meshFile.FileId, nbssAppointmentEvent.MeshFileId); - Assert.Equal(dataRecord["BSO"], nbssAppointmentEvent.BSO); + Assert.Equal(firstRecord["BSO"], nbssAppointmentEvent.BSO); Assert.Equal(parsedFile.FileTrailer!.ExtractId, nbssAppointmentEvent.ExtractId); - Assert.Equal(dataRecord["Sequence"], nbssAppointmentEvent.Sequence); - Assert.Equal(dataRecord["Action"], nbssAppointmentEvent.Action); - Assert.Equal(dataRecord["Clinic Code"], nbssAppointmentEvent.ClinicCode); - Assert.Equal(dataRecord["Holding Clinic"], nbssAppointmentEvent.HoldingClinic); - Assert.Equal(dataRecord["Status"], nbssAppointmentEvent.Status); - Assert.Equal(dataRecord["Attended Not Scr"], nbssAppointmentEvent.AttendedNotScreened); - Assert.Equal(dataRecord["Appointment ID"], nbssAppointmentEvent.AppointmenId); - Assert.Equal(dataRecord["NHS Num"], nbssAppointmentEvent.NhsNumber); - Assert.Equal(dataRecord["Episode Type"], nbssAppointmentEvent.EpisodeType); - Assert.Equal(DateOnly.ParseExact(dataRecord.Fields["Episode Start"], "yyyyMMdd"), nbssAppointmentEvent.EpisodeStart); - Assert.Equal(dataRecord["Batch ID"], nbssAppointmentEvent.BatchId); - Assert.Equal(dataRecord["Screen or Asses"], nbssAppointmentEvent.AppointmentType); - Assert.Equal(byte.Parse(dataRecord.Fields["Screen Appt num"]), nbssAppointmentEvent.ScreeningAppointmentNumber); - Assert.Equal(dataRecord["Booked By"], nbssAppointmentEvent.BookedBy); - Assert.Equal(dataRecord["Cancelled By"], nbssAppointmentEvent.CancelledBy); - Assert.Equal(DateTime.ParseExact(dataRecord.Fields["Appt Date"] + dataRecord.Fields["Appt Time"], "yyyyMMddHHmm", null), nbssAppointmentEvent.AppointmentDateTime); - Assert.Equal(dataRecord["Location"], nbssAppointmentEvent.Location); - Assert.Equal(dataRecord["Clinic Name"], nbssAppointmentEvent.ClinicName); - Assert.Equal(dataRecord["Clinic Name (Let)"], nbssAppointmentEvent.ClinicNameOnLetters); - Assert.Equal(dataRecord["Clinic Address 1"], nbssAppointmentEvent.ClinicAddressLine1); - Assert.Equal(dataRecord["Clinic Address 2"], nbssAppointmentEvent.ClinicAddressLine2); - Assert.Equal(dataRecord["Clinic Address 3"], nbssAppointmentEvent.ClinicAddressLine3); - Assert.Equal(dataRecord["Clinic Address 4"], nbssAppointmentEvent.ClinicAddressLine4); - Assert.Equal(dataRecord["Clinic Address 5"], nbssAppointmentEvent.ClinicAddressLine5); - Assert.Equal(dataRecord["Postcode"], nbssAppointmentEvent.ClinicPostcode); - Assert.Equal(DateTime.ParseExact(dataRecord.Fields["Action Timestamp"], "yyyyMMdd-HHmmss", null), nbssAppointmentEvent.ActionTimestamp); + Assert.Equal(firstRecord["Sequence"], nbssAppointmentEvent.Sequence); + Assert.Equal(firstRecord["Action"], nbssAppointmentEvent.Action); + Assert.Equal(firstRecord["Clinic Code"], nbssAppointmentEvent.ClinicCode); + Assert.Equal(firstRecord["Holding Clinic"], nbssAppointmentEvent.HoldingClinic); + Assert.Equal(firstRecord["Status"], nbssAppointmentEvent.Status); + Assert.Equal(firstRecord["Attended Not Scr"], nbssAppointmentEvent.AttendedNotScreened); + Assert.Equal(firstRecord["Appointment ID"], nbssAppointmentEvent.AppointmenId); + Assert.Equal(firstRecord["NHS Num"], nbssAppointmentEvent.NhsNumber); + Assert.Equal(firstRecord["Episode Type"], nbssAppointmentEvent.EpisodeType); + Assert.Equal(DateOnly.ParseExact(firstRecord.Fields["Episode Start"], "yyyyMMdd"), nbssAppointmentEvent.EpisodeStart); + Assert.Equal(firstRecord["Batch ID"], nbssAppointmentEvent.BatchId); + Assert.Equal(firstRecord["Screen or Asses"], nbssAppointmentEvent.AppointmentType); + Assert.Equal(byte.Parse(firstRecord.Fields["Screen Appt num"]), nbssAppointmentEvent.ScreeningAppointmentNumber); + Assert.Equal(firstRecord["Booked By"], nbssAppointmentEvent.BookedBy); + Assert.Equal(firstRecord["Cancelled By"], nbssAppointmentEvent.CancelledBy); + Assert.Equal(DateTime.ParseExact(firstRecord.Fields["Appt Date"] + firstRecord.Fields["Appt Time"], "yyyyMMddHHmm", null), nbssAppointmentEvent.AppointmentDateTime); + Assert.Equal(firstRecord["Location"], nbssAppointmentEvent.Location); + Assert.Equal(firstRecord["Clinic Name"], nbssAppointmentEvent.ClinicName); + Assert.Equal(firstRecord["Clinic Name (Let)"], nbssAppointmentEvent.ClinicNameOnLetters); + Assert.Equal(firstRecord["Clinic Address 1"], nbssAppointmentEvent.ClinicAddressLine1); + Assert.Equal(firstRecord["Clinic Address 2"], nbssAppointmentEvent.ClinicAddressLine2); + Assert.Equal(firstRecord["Clinic Address 3"], nbssAppointmentEvent.ClinicAddressLine3); + Assert.Equal(firstRecord["Clinic Address 4"], nbssAppointmentEvent.ClinicAddressLine4); + Assert.Equal(firstRecord["Clinic Address 5"], nbssAppointmentEvent.ClinicAddressLine5); + Assert.Equal(firstRecord["Postcode"], nbssAppointmentEvent.ClinicPostcode); + Assert.Equal(DateTime.ParseExact(firstRecord.Fields["Action Timestamp"], "yyyyMMdd-HHmmss", null), nbssAppointmentEvent.ActionTimestamp); + } + + [Fact] + public async Task WriteStagedData_WhenOptionalFieldsAreEmpty_MapsFieldToNullAndSavesToDb() + { + // Arrange + var parsedFile = TestDataBuilder.BuildValidParsedFile(1); + parsedFile.DataRecords[0].Fields["Holding Clinic"] = ""; + parsedFile.DataRecords[0].Fields["Attended Not Scr"] = ""; + parsedFile.DataRecords[0].Fields["Screen Appt num"] = ""; + parsedFile.DataRecords[0].Fields["Cancelled By"] = ""; + var meshFile = new MeshFile() + { + FileId = "1", + FileType = MeshFileType.NbssAppointmentEvents, + MailboxId = "ABC", + Status = MeshFileStatus.Transforming + }; + + // Act + await _stagingPersister.WriteStagedData(parsedFile, meshFile); + + // Assert + Assert.Equal(1, await _dbContext.NbssAppointmentEvents.CountAsync()); + + var nbssAppointmentEvent = await _dbContext.NbssAppointmentEvents.FirstAsync(); + Assert.Null(nbssAppointmentEvent.HoldingClinic); + Assert.Null(nbssAppointmentEvent.AttendedNotScreened); + Assert.Null(nbssAppointmentEvent.ScreeningAppointmentNumber); + Assert.Null(nbssAppointmentEvent.CancelledBy); } [Theory] @@ -106,9 +138,9 @@ public async Task WriteStagedData_WhenFileValid_SavesToDb() public async Task WriteStagedData_WhenFieldMissing_DoesNotSaveToDb(string fieldName) { // Arrange - var parsedFile = TestDataBuilder.BuildValidParsedFile(); - var recordWithoutBSO = TestDataBuilder.BuildFileDataRecordWithField(fieldName, null, 1); - parsedFile.DataRecords.Add(recordWithoutBSO); + var parsedFile = TestDataBuilder.BuildValidParsedFile(0); + var record = TestDataBuilder.BuildFileDataRecordWithField(fieldName, null, 1); + parsedFile.DataRecords.Add(record); var meshFile = new MeshFile() { FileId = "1", @@ -127,12 +159,13 @@ public async Task WriteStagedData_WhenFieldMissing_DoesNotSaveToDb(string fieldN [InlineData("Screen Appt num")] [InlineData("Appt Date")] [InlineData("Appt Time")] + [InlineData("Action Timestamp")] public async Task WriteStagedData_WhenFieldHoldsInvalidValue_DoesNotSaveToDb(string fieldName) { // Arrange - var parsedFile = TestDataBuilder.BuildValidParsedFile(); - var recordWithoutBSO = TestDataBuilder.BuildFileDataRecordWithField(fieldName, "Invalid value", 1); - parsedFile.DataRecords.Add(recordWithoutBSO); + var parsedFile = TestDataBuilder.BuildValidParsedFile(0); + var record = TestDataBuilder.BuildFileDataRecordWithField(fieldName, "Invalid value", 1); + parsedFile.DataRecords.Add(record); var meshFile = new MeshFile() { FileId = "1",