diff --git a/VigilCareRecordsAPI.Tests/CoverSheetBatchTests.cs b/VigilCareRecordsAPI.Tests/CoverSheetBatchTests.cs
new file mode 100644
index 0000000..8fe5deb
--- /dev/null
+++ b/VigilCareRecordsAPI.Tests/CoverSheetBatchTests.cs
@@ -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;
+
+///
+/// Tests for barcode-assisted batch creation via cover sheet codes.
+///
+[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();
+ 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();
+ 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();
+ 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();
+ 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();
+ 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();
+ 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();
+ 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 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();
+ return body.GetProperty("data")[0].GetProperty("code").GetString()!;
+ }
+
+ private async Task 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);
+ }
+}
diff --git a/VigilCareRecordsAPI.Tests/CoverSheetPdfTests.cs b/VigilCareRecordsAPI.Tests/CoverSheetPdfTests.cs
new file mode 100644
index 0000000..529c058
--- /dev/null
+++ b/VigilCareRecordsAPI.Tests/CoverSheetPdfTests.cs
@@ -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;
+
+///
+/// Tests for cover sheet PDF generation endpoints and the PDF builder.
+///
+[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();
+ 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
+ {
+ 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 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();
+ return body.GetProperty("data")[0].GetProperty("id").GetGuid();
+ }
+}
diff --git a/VigilCareRecordsAPI/Controllers/CoverSheetController.cs b/VigilCareRecordsAPI/Controllers/CoverSheetController.cs
new file mode 100644
index 0000000..f533990
--- /dev/null
+++ b/VigilCareRecordsAPI/Controllers/CoverSheetController.cs
@@ -0,0 +1,117 @@
+using System.Security.Claims;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Mvc;
+
+[ApiController]
+[Route("api/v1/cover-sheets")]
+[Produces("application/json")]
+[Authorize]
+public class CoverSheetController : ControllerBase
+{
+ private readonly ICoverSheetService _coverSheets;
+
+ public CoverSheetController(ICoverSheetService coverSheets)
+ {
+ _coverSheets = coverSheets;
+ }
+
+ ///
+ /// Generates one or more cover sheets with unique barcode codes.
+ /// Each cover sheet encodes batch type, track, optional patient, and
+ /// optional entry clerk assignment.
+ ///
+ [HttpPost("generate")]
+ [Authorize(Roles = "INTAKE_CLERK,ADMINISTRATOR")]
+ [ProducesResponseType(typeof(ApiResponse>), StatusCodes.Status201Created)]
+ [ProducesResponseType(typeof(ApiResponse