diff --git a/src/ServiceLayer.Common/Data/Migrations/20250521094738_AddNbssAppointmentEventTable.Designer.cs b/src/ServiceLayer.Common/Data/Migrations/20250521094738_AddNbssAppointmentEventTable.Designer.cs
new file mode 100644
index 0000000..b3f4862
--- /dev/null
+++ b/src/ServiceLayer.Common/Data/Migrations/20250521094738_AddNbssAppointmentEventTable.Designer.cs
@@ -0,0 +1,222 @@
+//
+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("20250521094738_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(0)");
+
+ b.Property("AppointmenId")
+ .IsRequired()
+ .HasMaxLength(27)
+ .HasColumnType("varchar(27)");
+
+ b.Property("AppointmentDateTime")
+ .HasColumnType("datetime2(0)");
+
+ 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("varchar(9)");
+
+ b.Property("BookedBy")
+ .IsRequired()
+ .HasMaxLength(1)
+ .HasColumnType("char(1)");
+
+ b.Property("CancelledBy")
+ .HasMaxLength(1)
+ .HasColumnType("char(1)");
+
+ b.Property("ClinicAddressLine1")
+ .IsRequired()
+ .HasMaxLength(30)
+ .HasColumnType("varchar(30)");
+
+ b.Property("ClinicAddressLine2")
+ .IsRequired()
+ .HasMaxLength(30)
+ .HasColumnType("varchar(30)");
+
+ b.Property("ClinicAddressLine3")
+ .IsRequired()
+ .HasMaxLength(30)
+ .HasColumnType("varchar(30)");
+
+ b.Property("ClinicAddressLine4")
+ .IsRequired()
+ .HasMaxLength(30)
+ .HasColumnType("varchar(30)");
+
+ b.Property("ClinicAddressLine5")
+ .IsRequired()
+ .HasMaxLength(30)
+ .HasColumnType("varchar(30)");
+
+ b.Property("ClinicCode")
+ .IsRequired()
+ .HasMaxLength(5)
+ .HasColumnType("varchar(5)");
+
+ b.Property("ClinicName")
+ .IsRequired()
+ .HasMaxLength(40)
+ .HasColumnType("varchar(40)");
+
+ b.Property("ClinicNameOnLetters")
+ .IsRequired()
+ .HasMaxLength(50)
+ .HasColumnType("varchar(50)");
+
+ b.Property("ClinicPostcode")
+ .IsRequired()
+ .HasMaxLength(8)
+ .HasColumnType("varchar(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("varchar(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/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 f000fcd..4a05938 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,162 @@ 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(0)");
+
+ b.Property("AppointmenId")
+ .IsRequired()
+ .HasMaxLength(27)
+ .HasColumnType("varchar(27)");
+
+ b.Property("AppointmentDateTime")
+ .HasColumnType("datetime2(0)");
+
+ 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("varchar(9)");
+
+ b.Property("BookedBy")
+ .IsRequired()
+ .HasMaxLength(1)
+ .HasColumnType("char(1)");
+
+ b.Property("CancelledBy")
+ .HasMaxLength(1)
+ .HasColumnType("char(1)");
+
+ b.Property("ClinicAddressLine1")
+ .IsRequired()
+ .HasMaxLength(30)
+ .HasColumnType("varchar(30)");
+
+ b.Property("ClinicAddressLine2")
+ .IsRequired()
+ .HasMaxLength(30)
+ .HasColumnType("varchar(30)");
+
+ b.Property("ClinicAddressLine3")
+ .IsRequired()
+ .HasMaxLength(30)
+ .HasColumnType("varchar(30)");
+
+ b.Property("ClinicAddressLine4")
+ .IsRequired()
+ .HasMaxLength(30)
+ .HasColumnType("varchar(30)");
+
+ b.Property("ClinicAddressLine5")
+ .IsRequired()
+ .HasMaxLength(30)
+ .HasColumnType("varchar(30)");
+
+ b.Property("ClinicCode")
+ .IsRequired()
+ .HasMaxLength(5)
+ .HasColumnType("varchar(5)");
+
+ b.Property("ClinicName")
+ .IsRequired()
+ .HasMaxLength(40)
+ .HasColumnType("varchar(40)");
+
+ b.Property("ClinicNameOnLetters")
+ .IsRequired()
+ .HasMaxLength(50)
+ .HasColumnType("varchar(50)");
+
+ b.Property("ClinicPostcode")
+ .IsRequired()
+ .HasMaxLength(8)
+ .HasColumnType("varchar(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("varchar(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..7a68760
--- /dev/null
+++ b/src/ServiceLayer.Common/Data/Models/NbssAppointmentEvent.cs
@@ -0,0 +1,89 @@
+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; }
+ [StringLength(5)]
+ [Column(TypeName = "varchar(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)]
+ [Column(TypeName = "varchar(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)]
+ [Column(TypeName = "varchar(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 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; }
+}
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);
}
}
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 ab2efde..650c9aa 100644
--- a/src/ServiceLayer.Mesh/FileTypes/NbssAppointmentEvents/StagingPersister.cs
+++ b/src/ServiceLayer.Mesh/FileTypes/NbssAppointmentEvents/StagingPersister.cs
@@ -1,13 +1,57 @@
+using System.Globalization;
+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, MeshFile meshFile)
{
- // TODO - implement this
- throw new NotImplementedException();
+ var nbssAppointmentEvents = MapFileDataRecordsToNbssAppointmentEvents(parsedFile, meshFile.FileId);
+
+ await dbContext.NbssAppointmentEvents.AddRangeAsync(nbssAppointmentEvents);
+ await dbContext.SaveChangesAsync();
+ }
+
+ private static List MapFileDataRecordsToNbssAppointmentEvents(ParsedFile parsedFile, string fileId)
+ {
+ return [.. parsedFile.DataRecords.Select(record => 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 = NullIfWhiteSpace(record.Fields["Holding Clinic"]),
+ Status = record.Fields["Status"],
+ 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 = NullByteIfWhiteSpace(record.Fields["Screen Appt num"]),
+ BookedBy = record.Fields["Booked 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"],
+ 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)
+ })];
}
+
+ 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
new file mode 100644
index 0000000..2b775ec
--- /dev/null
+++ b/tests/ServiceLayer.Mesh.Tests/FileTypes/NbssAppointmentEvents/StagingPersisterTests.cs
@@ -0,0 +1,181 @@
+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(Guid.NewGuid().ToString())
+ .Options;
+
+ _dbContext = new ServiceLayerDbContext(options);
+
+ _stagingPersister = new StagingPersister(_dbContext);
+ }
+
+ [Fact]
+ public async Task WriteStagedData_WhenFileValid_MapsFieldsAndSavesToDb()
+ {
+ // Arrange
+ 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",
+ 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();
+ Assert.NotEqual(Guid.Empty, nbssAppointmentEvent.Id);
+ Assert.Equal(meshFile.FileId, nbssAppointmentEvent.MeshFileId);
+ Assert.Equal(firstRecord["BSO"], nbssAppointmentEvent.BSO);
+ Assert.Equal(parsedFile.FileTrailer!.ExtractId, nbssAppointmentEvent.ExtractId);
+ 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]
+ [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(0);
+ var record = TestDataBuilder.BuildFileDataRecordWithField(fieldName, null, 1);
+ parsedFile.DataRecords.Add(record);
+ 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")]
+ [InlineData("Action Timestamp")]
+ public async Task WriteStagedData_WhenFieldHoldsInvalidValue_DoesNotSaveToDb(string fieldName)
+ {
+ // Arrange
+ var parsedFile = TestDataBuilder.BuildValidParsedFile(0);
+ var record = TestDataBuilder.BuildFileDataRecordWithField(fieldName, "Invalid value", 1);
+ parsedFile.DataRecords.Add(record);
+ 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());
+ }
+}