feature: Barcode/QR Cover Sheet System
This commit is contained in:
@@ -0,0 +1,143 @@
|
||||
using System.Net;
|
||||
using System.Net.Http.Json;
|
||||
using System.Text.Json;
|
||||
using FluentAssertions;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
/// <summary>
|
||||
/// Tests for barcode-assisted batch creation via cover sheet codes.
|
||||
/// </summary>
|
||||
[Collection("Database")]
|
||||
public class CoverSheetBatchTests : IAsyncLifetime
|
||||
{
|
||||
private readonly ApiFixture _fixture;
|
||||
private HttpClient _intakeClient = null!;
|
||||
|
||||
public CoverSheetBatchTests(ApiFixture fixture) => _fixture = fixture;
|
||||
|
||||
public async Task InitializeAsync()
|
||||
{
|
||||
using var scope = _fixture.Services.CreateScope();
|
||||
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
|
||||
await DbResetHelper.ResetAsync(db);
|
||||
await DataSeeder.SeedAsync(db);
|
||||
_intakeClient = await AuthHelper.LoginAsync(_fixture, "intake1");
|
||||
}
|
||||
|
||||
public Task DisposeAsync() => Task.CompletedTask;
|
||||
|
||||
[Fact]
|
||||
public async Task Create_WithCoverSheetCode_CreatesBatchAndRedeemsCoverSheet()
|
||||
{
|
||||
var code = await GenerateCoverSheetCodeAsync("VITALS_SHEET", "BACKFILL");
|
||||
|
||||
var response = await UploadWithCoverSheetAsync(code);
|
||||
|
||||
response.StatusCode.Should().Be(HttpStatusCode.Created);
|
||||
|
||||
var body = await response.Content.ReadFromJsonAsync<JsonElement>();
|
||||
var batchId = body.GetProperty("data").GetProperty("id").GetGuid();
|
||||
body.GetProperty("data").GetProperty("batchType").GetString()
|
||||
.Should().Be("VITALS_SHEET");
|
||||
body.GetProperty("data").GetProperty("track").GetString()
|
||||
.Should().Be("BACKFILL");
|
||||
|
||||
using var scope = _fixture.Services.CreateScope();
|
||||
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
|
||||
var sheet = await db.CoverSheets.FirstAsync(c => c.Code == code);
|
||||
sheet.IsUsed.Should().BeTrue();
|
||||
sheet.BatchId.Should().Be(batchId);
|
||||
sheet.UsedAt.Should().NotBeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Create_WithUsedCoverSheetCode_Returns409()
|
||||
{
|
||||
var code = await GenerateCoverSheetCodeAsync("VITALS_SHEET", "BACKFILL");
|
||||
|
||||
var first = await UploadWithCoverSheetAsync(code);
|
||||
first.EnsureSuccessStatusCode();
|
||||
|
||||
var second = await UploadWithCoverSheetAsync(code);
|
||||
second.StatusCode.Should().Be(HttpStatusCode.Conflict);
|
||||
|
||||
var body = await second.Content.ReadFromJsonAsync<JsonElement>();
|
||||
body.GetProperty("error").GetProperty("code").GetString()
|
||||
.Should().Be("COVER_SHEET_ALREADY_USED");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Create_WithUnknownCoverSheetCode_Returns404()
|
||||
{
|
||||
var response = await UploadWithCoverSheetAsync("VCR-CS-DEADBEEF");
|
||||
|
||||
response.StatusCode.Should().Be(HttpStatusCode.NotFound);
|
||||
|
||||
var body = await response.Content.ReadFromJsonAsync<JsonElement>();
|
||||
body.GetProperty("error").GetProperty("code").GetString()
|
||||
.Should().Be("COVER_SHEET_NOT_FOUND");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Create_WithCoverSheetPreAssigned_AutoAssignsBatch()
|
||||
{
|
||||
Guid entryUserId;
|
||||
using (var scope = _fixture.Services.CreateScope())
|
||||
{
|
||||
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
|
||||
entryUserId = await BatchSeedHelper.UserIdAsync(db, "entry1");
|
||||
}
|
||||
|
||||
var code = await GenerateCoverSheetCodeAsync(
|
||||
"LAB_RESULTS", "BACKFILL", assignToUserId: entryUserId);
|
||||
|
||||
var response = await UploadWithCoverSheetAsync(code);
|
||||
response.EnsureSuccessStatusCode();
|
||||
|
||||
var body = await response.Content.ReadFromJsonAsync<JsonElement>();
|
||||
body.GetProperty("data").GetProperty("batchType").GetString()
|
||||
.Should().Be("LAB_RESULTS");
|
||||
body.GetProperty("data").GetProperty("enteredByUserId").GetGuid()
|
||||
.Should().Be(entryUserId);
|
||||
body.GetProperty("data").GetProperty("status").GetString()
|
||||
.Should().Be("IN_ENTRY");
|
||||
}
|
||||
|
||||
private async Task<string> GenerateCoverSheetCodeAsync(
|
||||
string batchType,
|
||||
string track,
|
||||
Guid? assignToUserId = null)
|
||||
{
|
||||
var payload = new
|
||||
{
|
||||
count = 1,
|
||||
batchType,
|
||||
track,
|
||||
assignToUserId
|
||||
};
|
||||
|
||||
var response = await _intakeClient.PostAsJsonAsync("/api/v1/cover-sheets/generate", payload);
|
||||
response.EnsureSuccessStatusCode();
|
||||
|
||||
var body = await response.Content.ReadFromJsonAsync<JsonElement>();
|
||||
return body.GetProperty("data")[0].GetProperty("code").GetString()!;
|
||||
}
|
||||
|
||||
private async Task<HttpResponseMessage> UploadWithCoverSheetAsync(string coverSheetCode)
|
||||
{
|
||||
var fileContent = new ByteArrayContent(
|
||||
System.Text.Encoding.ASCII.GetBytes(
|
||||
$"%PDF-1.4\n%%EOF\n%test-{Guid.NewGuid()}"));
|
||||
fileContent.Headers.ContentType =
|
||||
new System.Net.Http.Headers.MediaTypeHeaderValue("application/pdf");
|
||||
|
||||
var formData = new MultipartFormDataContent
|
||||
{
|
||||
{ fileContent, "file", "test.pdf" },
|
||||
{ new StringContent(coverSheetCode), "coverSheetCode" }
|
||||
};
|
||||
|
||||
return await _intakeClient.PostAsync("/api/v1/digitization-batches", formData);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
using System.Net;
|
||||
using System.Net.Http.Json;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using FluentAssertions;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
/// <summary>
|
||||
/// Tests for cover sheet PDF generation endpoints and the PDF builder.
|
||||
/// </summary>
|
||||
[Collection("Database")]
|
||||
public class CoverSheetPdfTests : IAsyncLifetime
|
||||
{
|
||||
private readonly ApiFixture _fixture;
|
||||
private HttpClient _intakeClient = null!;
|
||||
|
||||
public CoverSheetPdfTests(ApiFixture fixture) => _fixture = fixture;
|
||||
|
||||
public async Task InitializeAsync()
|
||||
{
|
||||
using var scope = _fixture.Services.CreateScope();
|
||||
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
|
||||
await DbResetHelper.ResetAsync(db);
|
||||
await DataSeeder.SeedAsync(db);
|
||||
_intakeClient = await AuthHelper.LoginAsync(_fixture, "intake1");
|
||||
}
|
||||
|
||||
public Task DisposeAsync() => Task.CompletedTask;
|
||||
|
||||
[Fact]
|
||||
public void Generate_ProducesValidPdfWithCoverSheetMetadata()
|
||||
{
|
||||
var sheet = new CoverSheet
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
Code = "VCR-CS-A3F7B2D1",
|
||||
BatchType = BatchType.VitalsSheet,
|
||||
Track = BatchTrack.Backfill,
|
||||
CreatedAt = new DateTimeOffset(2026, 6, 27, 12, 0, 0, TimeSpan.Zero),
|
||||
Patient = new Patient
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
FullName = "Maria Garcia",
|
||||
Mrn = "MRN-12345"
|
||||
},
|
||||
AssignToUser = new User
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
FullName = "Entry Clerk One"
|
||||
}
|
||||
};
|
||||
|
||||
var pdf = CoverSheetPdfGenerator.Generate(sheet);
|
||||
|
||||
var text = Encoding.ASCII.GetString(pdf);
|
||||
text.Should().StartWith("%PDF-1.4");
|
||||
text.Should().Contain("VCR-CS-A3F7B2D1");
|
||||
text.Should().Contain("VITALS_SHEET");
|
||||
text.Should().Contain("BACKFILL");
|
||||
text.Should().Contain("Maria Garcia");
|
||||
text.Should().Contain("MRN-12345");
|
||||
text.Should().Contain("Entry Clerk One");
|
||||
text.Should().Contain("2026-06-27");
|
||||
text.Should().Contain("Attach to front of chart section");
|
||||
text.Should().Contain("/Subtype /Image");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GenerateBatch_ProducesMultiPagePdf()
|
||||
{
|
||||
var sheets = new[]
|
||||
{
|
||||
new CoverSheet
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
Code = "VCR-CS-11111111",
|
||||
BatchType = BatchType.VitalsSheet,
|
||||
Track = BatchTrack.Backfill,
|
||||
CreatedAt = DateTimeOffset.UtcNow
|
||||
},
|
||||
new CoverSheet
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
Code = "VCR-CS-22222222",
|
||||
BatchType = BatchType.LabResults,
|
||||
Track = BatchTrack.Backfill,
|
||||
CreatedAt = DateTimeOffset.UtcNow
|
||||
}
|
||||
};
|
||||
|
||||
var pdf = CoverSheetPdfGenerator.GenerateBatch(sheets);
|
||||
var text = Encoding.ASCII.GetString(pdf);
|
||||
|
||||
text.Should().Contain("/Count 2");
|
||||
text.Should().Contain("VCR-CS-11111111");
|
||||
text.Should().Contain("VCR-CS-22222222");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GeneratePdf_Endpoint_ReturnsPdfForCoverSheetId()
|
||||
{
|
||||
var sheetId = await GenerateCoverSheetIdAsync();
|
||||
|
||||
var response = await _intakeClient.PostAsync($"/api/v1/cover-sheets/{sheetId}/pdf", null);
|
||||
|
||||
response.StatusCode.Should().Be(HttpStatusCode.OK);
|
||||
response.Content.Headers.ContentType!.MediaType.Should().Be("application/pdf");
|
||||
|
||||
var bytes = await response.Content.ReadAsByteArrayAsync();
|
||||
var text = Encoding.ASCII.GetString(bytes);
|
||||
text.Should().StartWith("%PDF-1.4");
|
||||
text.Should().Contain("VCR-CS-");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GenerateBatchPdf_Endpoint_ReturnsMultiPagePdf()
|
||||
{
|
||||
var ids = new List<Guid>
|
||||
{
|
||||
await GenerateCoverSheetIdAsync(),
|
||||
await GenerateCoverSheetIdAsync()
|
||||
};
|
||||
|
||||
var response = await _intakeClient.PostAsJsonAsync(
|
||||
"/api/v1/cover-sheets/batch-pdf",
|
||||
new { coverSheetIds = ids });
|
||||
|
||||
response.StatusCode.Should().Be(HttpStatusCode.OK);
|
||||
response.Content.Headers.ContentType!.MediaType.Should().Be("application/pdf");
|
||||
|
||||
var text = Encoding.ASCII.GetString(await response.Content.ReadAsByteArrayAsync());
|
||||
text.Should().Contain("/Count 2");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GeneratePdf_UnknownId_Returns404()
|
||||
{
|
||||
var response = await _intakeClient.PostAsync(
|
||||
$"/api/v1/cover-sheets/{Guid.NewGuid()}/pdf", null);
|
||||
|
||||
response.StatusCode.Should().Be(HttpStatusCode.NotFound);
|
||||
}
|
||||
|
||||
private async Task<Guid> GenerateCoverSheetIdAsync()
|
||||
{
|
||||
var response = await _intakeClient.PostAsJsonAsync(
|
||||
"/api/v1/cover-sheets/generate",
|
||||
new { count = 1, batchType = "VITALS_SHEET", track = "BACKFILL" });
|
||||
response.EnsureSuccessStatusCode();
|
||||
|
||||
var body = await response.Content.ReadFromJsonAsync<JsonElement>();
|
||||
return body.GetProperty("data")[0].GetProperty("id").GetGuid();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user