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()); + } +}