feature: Approval and Promotion to VigilCareClinical
This commit is contained in:
@@ -0,0 +1,144 @@
|
||||
using System.Net.Http.Json;
|
||||
using System.Text.Json;
|
||||
|
||||
public static class BatchPipelineHelper
|
||||
{
|
||||
/// <summary>
|
||||
/// Drives a batch from upload through to verified status, returning the batch ID.
|
||||
/// Uses intake1 for upload, entry1 for data entry, verifier1 for verification.
|
||||
/// </summary>
|
||||
public static async Task<Guid> CreateAndVerifyBatchAsync(
|
||||
ApiFixture fixture,
|
||||
string batchType = "VITALS_SHEET",
|
||||
string track = "BACKFILL",
|
||||
string patientFullName = "Test Patient",
|
||||
string patientDateOfBirth = "1990-05-15")
|
||||
{
|
||||
// === Upload as intake1 ===
|
||||
var client = await AuthHelper.LoginAsync(fixture, "intake1");
|
||||
|
||||
var fileContent = new ByteArrayContent(GenerateTestPdf());
|
||||
fileContent.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue("application/pdf");
|
||||
|
||||
var formData = new MultipartFormDataContent
|
||||
{
|
||||
{ fileContent, "file", $"test-scan-{Guid.NewGuid()}.pdf" },
|
||||
{ new StringContent(batchType), "batchType" },
|
||||
{ new StringContent(track), "track" }
|
||||
};
|
||||
|
||||
var uploadResponse = await client.PostAsync("/api/v1/digitization-batches", formData);
|
||||
uploadResponse.EnsureSuccessStatusCode();
|
||||
|
||||
var uploadResult = await uploadResponse.Content.ReadFromJsonAsync<JsonElement>();
|
||||
var batchId = uploadResult.GetProperty("data").GetProperty("id").GetGuid();
|
||||
|
||||
// === Assign to entry1 ===
|
||||
var assignResponse = await client.PatchAsJsonAsync(
|
||||
$"/api/v1/digitization-batches/{batchId}/assign",
|
||||
new { entryClerkUserId = await GetUserIdAsync(fixture, "entry1") });
|
||||
assignResponse.EnsureSuccessStatusCode();
|
||||
|
||||
// === Data entry as entry1 ===
|
||||
client = await AuthHelper.LoginAsync(fixture, "entry1");
|
||||
|
||||
// Add draft patient
|
||||
(await client.PutAsJsonAsync(
|
||||
$"/api/v1/digitization-batches/{batchId}/draft/patient",
|
||||
new
|
||||
{
|
||||
fullName = patientFullName,
|
||||
dateOfBirth = patientDateOfBirth,
|
||||
sex = "Male",
|
||||
bloodType = "A+",
|
||||
noKnownAllergies = true
|
||||
})).EnsureSuccessStatusCode();
|
||||
|
||||
// Add draft encounter
|
||||
(await client.PutAsJsonAsync(
|
||||
$"/api/v1/digitization-batches/{batchId}/draft/encounter",
|
||||
new
|
||||
{
|
||||
admissionDate = "2024-01-15T08:00:00Z",
|
||||
department = Department.GeneralMedicine.ToDbString(),
|
||||
roomBed = "301-A",
|
||||
admissionReason = "Routine checkup",
|
||||
status = "active"
|
||||
})).EnsureSuccessStatusCode();
|
||||
|
||||
// Add draft observations
|
||||
(await client.PostAsJsonAsync(
|
||||
$"/api/v1/digitization-batches/{batchId}/draft/observations",
|
||||
new
|
||||
{
|
||||
observationCode = "HR",
|
||||
value = 88.0,
|
||||
unit = "bpm",
|
||||
recordedAt = "2024-01-15T09:30:00Z",
|
||||
note = "Resting heart rate"
|
||||
})).EnsureSuccessStatusCode();
|
||||
|
||||
(await client.PostAsJsonAsync(
|
||||
$"/api/v1/digitization-batches/{batchId}/draft/observations",
|
||||
new
|
||||
{
|
||||
observationCode = "TEMP",
|
||||
value = 37.2,
|
||||
unit = "C",
|
||||
recordedAt = "2024-01-15T09:30:00Z",
|
||||
note = "Oral temperature"
|
||||
})).EnsureSuccessStatusCode();
|
||||
|
||||
(await client.PostAsJsonAsync(
|
||||
$"/api/v1/digitization-batches/{batchId}/draft/observations",
|
||||
new
|
||||
{
|
||||
observationCode = "BP_SYS",
|
||||
value = 120.0,
|
||||
unit = "mmHg",
|
||||
recordedAt = "2024-01-15T09:31:00Z"
|
||||
})).EnsureSuccessStatusCode();
|
||||
|
||||
// Submit for verification
|
||||
(await client.PostAsync(
|
||||
$"/api/v1/digitization-batches/{batchId}/submit-for-verification",
|
||||
null)).EnsureSuccessStatusCode();
|
||||
|
||||
// === Verify as verifier1 ===
|
||||
client = await AuthHelper.LoginAsync(fixture, "verifier1");
|
||||
|
||||
var verifyRequest = new VerifyBatchRequest(
|
||||
new List<FieldCheck>
|
||||
{
|
||||
new("patient.fullName", "ok", null),
|
||||
new("observation.heartRate", "ok", null),
|
||||
new("observation.temperature", "ok", null),
|
||||
new("observation.bloodPressureSystolic", "ok", null)
|
||||
},
|
||||
Passed: true
|
||||
);
|
||||
|
||||
var verifyResponse = await client.PostAsJsonAsync(
|
||||
$"/api/v1/digitization-batches/{batchId}/verify", verifyRequest);
|
||||
verifyResponse.EnsureSuccessStatusCode();
|
||||
|
||||
return batchId;
|
||||
}
|
||||
|
||||
private static async Task<Guid> GetUserIdAsync(ApiFixture fixture, string username)
|
||||
{
|
||||
var client = await AuthHelper.LoginAsync(fixture, username);
|
||||
var meResponse = await client.GetFromJsonAsync<JsonElement>("/api/v1/auth/me");
|
||||
return meResponse.GetProperty("data").GetProperty("id").GetGuid();
|
||||
}
|
||||
|
||||
private static byte[] GenerateTestPdf()
|
||||
{
|
||||
// Minimal valid PDF for testing
|
||||
var pdf = "%PDF-1.4\n1 0 obj\n<< /Type /Catalog /Pages 2 0 R >>\nendobj\n" +
|
||||
"2 0 obj\n<< /Type /Pages /Kids [3 0 R] /Count 1 >>\nendobj\n" +
|
||||
"3 0 obj\n<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] >>\nendobj\n" +
|
||||
$"%%EOF\n%% test-{Guid.NewGuid()}";
|
||||
return System.Text.Encoding.ASCII.GetBytes(pdf);
|
||||
}
|
||||
}
|
||||
@@ -7,9 +7,12 @@ public static class DbResetHelper
|
||||
public static async Task ResetAsync(AppDbContext db)
|
||||
{
|
||||
await db.Database.ExecuteSqlRawAsync(@"
|
||||
TRUNCATE TABLE digitization_events, draft_observations,
|
||||
draft_encounters, draft_patients,
|
||||
scanned_documents, digitization_batches, users
|
||||
TRUNCATE TABLE clinical.outbox_events, clinical.observations,
|
||||
clinical.encounters, clinical.patients,
|
||||
idempotency_records,
|
||||
digitization_events, draft_observations,
|
||||
draft_encounters, draft_patients,
|
||||
scanned_documents, digitization_batches, users
|
||||
RESTART IDENTITY CASCADE;
|
||||
");
|
||||
}
|
||||
|
||||
@@ -0,0 +1,448 @@
|
||||
using System.Net;
|
||||
using System.Net.Http.Json;
|
||||
using System.Text.Json;
|
||||
using FluentAssertions;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
[Collection("Database")]
|
||||
public class PromotionTests : IAsyncLifetime
|
||||
{
|
||||
private readonly ApiFixture _fixture;
|
||||
private HttpClient _client = null!;
|
||||
|
||||
public PromotionTests(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);
|
||||
_client = _fixture.CreateClient();
|
||||
}
|
||||
|
||||
public Task DisposeAsync() => Task.CompletedTask;
|
||||
|
||||
/// <summary>
|
||||
/// Full pipeline: upload → entry → verify → approve → live observations exist, zero alerts for backfill.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task Approve_BackfillBatch_CreatesLiveRecords_ZeroOutboxEvents()
|
||||
{
|
||||
// Arrange: drive batch through full lifecycle to verified status
|
||||
var batchId = await BatchPipelineHelper.CreateAndVerifyBatchAsync(
|
||||
_fixture, track: "BACKFILL");
|
||||
|
||||
// Act: approve as clinical approver (approver1)
|
||||
_client = await AuthHelper.LoginAsync(_fixture, "approver1");
|
||||
|
||||
var idempotencyKey = Guid.NewGuid().ToString();
|
||||
_client.DefaultRequestHeaders.Add("Idempotency-Key", idempotencyKey);
|
||||
|
||||
var approveResponse = await _client.PostAsJsonAsync(
|
||||
$"/api/v1/digitization-batches/{batchId}/approve",
|
||||
new ApproveRequest(EnableRetroactiveAlerts: false));
|
||||
|
||||
// Assert: HTTP 200
|
||||
approveResponse.StatusCode.Should().Be(HttpStatusCode.OK);
|
||||
|
||||
var result = await approveResponse.Content
|
||||
.ReadFromJsonAsync<ApiResponse<PromotionResultResponse>>();
|
||||
|
||||
result.Should().NotBeNull();
|
||||
result!.Success.Should().BeTrue();
|
||||
result.Data.Should().NotBeNull();
|
||||
|
||||
var promotion = result.Data!;
|
||||
promotion.BatchId.Should().Be(batchId);
|
||||
promotion.Status.Should().Be("PROMOTED");
|
||||
promotion.PatientId.Should().NotBeEmpty();
|
||||
promotion.Mrn.Should().StartWith("VCR-");
|
||||
promotion.EncounterId.Should().NotBeEmpty();
|
||||
promotion.ObservationIds.Should().HaveCount(3);
|
||||
promotion.OutboxEventsWritten.Should().Be(0,
|
||||
"backfill with enableRetroactiveAlerts=false should write zero outbox events");
|
||||
|
||||
// Verify live records in database
|
||||
using var scope = _fixture.Services.CreateScope();
|
||||
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
|
||||
|
||||
// Patient exists with correct MRN
|
||||
var patient = await db.Patients.FirstOrDefaultAsync(p => p.Id == promotion.PatientId);
|
||||
patient.Should().NotBeNull();
|
||||
patient!.Mrn.Should().Be(promotion.Mrn);
|
||||
patient.FullName.Should().Be("Test Patient");
|
||||
patient.DateOfBirth.Should().Be(new DateOnly(1990, 5, 15));
|
||||
|
||||
// Encounter exists
|
||||
var encounter = await db.Encounters.FirstOrDefaultAsync(e => e.Id == promotion.EncounterId);
|
||||
encounter.Should().NotBeNull();
|
||||
encounter!.PatientId.Should().Be(promotion.PatientId);
|
||||
encounter.Department.Should().Be(Department.GeneralMedicine);
|
||||
|
||||
// All 3 observations exist with correct source
|
||||
var observations = await db.Observations
|
||||
.Where(o => o.SourceBatchId == batchId)
|
||||
.ToListAsync();
|
||||
observations.Should().HaveCount(3);
|
||||
observations.Should().AllSatisfy(o =>
|
||||
{
|
||||
o.Source.Should().Be("digitization_backfill");
|
||||
o.PatientId.Should().Be(promotion.PatientId);
|
||||
o.EncounterId.Should().Be(promotion.EncounterId);
|
||||
});
|
||||
|
||||
// Zero outbox events for this batch
|
||||
var observationIds = observations.Select(o => o.Id).ToList();
|
||||
var outboxEvents = await db.OutboxEvents
|
||||
.Where(e => observationIds.Contains(e.AggregateId))
|
||||
.ToListAsync();
|
||||
outboxEvents.Should().BeEmpty(
|
||||
"backfill with enableRetroactiveAlerts=false should not create outbox events");
|
||||
|
||||
// Batch status is Promoted
|
||||
var batch = await db.DigitizationBatches.FirstAsync(b => b.Id == batchId);
|
||||
batch.Status.Should().Be(BatchStatus.Promoted);
|
||||
batch.PromotedAt.Should().NotBeNull();
|
||||
batch.PromotionEncounterId.Should().Be(promotion.EncounterId);
|
||||
|
||||
// DigitizationEvent with type "promoted" exists
|
||||
var promotedEvent = await db.DigitizationEvents
|
||||
.FirstOrDefaultAsync(e => e.BatchId == batchId && e.EventType == DigitizationEventType.Promoted);
|
||||
promotedEvent.Should().NotBeNull();
|
||||
|
||||
// Clean up header for other tests
|
||||
_client.DefaultRequestHeaders.Remove("Idempotency-Key");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Backfill batch with enableRetroactiveAlerts=true should write outbox events for all observations.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task Approve_BackfillWithRetroactiveAlerts_CreatesOutboxEvents()
|
||||
{
|
||||
// Arrange
|
||||
var batchId = await BatchPipelineHelper.CreateAndVerifyBatchAsync(
|
||||
_fixture, track: "BACKFILL");
|
||||
|
||||
// Act
|
||||
_client = await AuthHelper.LoginAsync(_fixture, "approver1");
|
||||
var idempotencyKey = Guid.NewGuid().ToString();
|
||||
_client.DefaultRequestHeaders.Add("Idempotency-Key", idempotencyKey);
|
||||
|
||||
var approveResponse = await _client.PostAsJsonAsync(
|
||||
$"/api/v1/digitization-batches/{batchId}/approve",
|
||||
new ApproveRequest(EnableRetroactiveAlerts: true));
|
||||
|
||||
// Assert
|
||||
approveResponse.StatusCode.Should().Be(HttpStatusCode.OK);
|
||||
|
||||
var result = await approveResponse.Content
|
||||
.ReadFromJsonAsync<ApiResponse<PromotionResultResponse>>();
|
||||
|
||||
result!.Data!.OutboxEventsWritten.Should().Be(3,
|
||||
"backfill with enableRetroactiveAlerts=true should write one outbox event per observation");
|
||||
|
||||
using var scope = _fixture.Services.CreateScope();
|
||||
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
|
||||
|
||||
var observationIds = await db.Observations
|
||||
.Where(o => o.SourceBatchId == batchId)
|
||||
.Select(o => o.Id)
|
||||
.ToListAsync();
|
||||
|
||||
var outboxEvents = await db.OutboxEvents
|
||||
.Where(e => observationIds.Contains(e.AggregateId))
|
||||
.ToListAsync();
|
||||
outboxEvents.Should().HaveCount(3);
|
||||
outboxEvents.Should().AllSatisfy(e =>
|
||||
{
|
||||
e.EventType.Should().Be("observation.created");
|
||||
e.AggregateType.Should().Be("Observation");
|
||||
e.ProcessedAt.Should().BeNull();
|
||||
});
|
||||
|
||||
_client.DefaultRequestHeaders.Remove("Idempotency-Key");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Live-capture track always writes outbox events regardless of enableRetroactiveAlerts flag.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task Approve_LiveCaptureBatch_AlwaysCreatesOutboxEvents()
|
||||
{
|
||||
// Arrange
|
||||
var batchId = await BatchPipelineHelper.CreateAndVerifyBatchAsync(
|
||||
_fixture, track: "LIVE_CAPTURE");
|
||||
|
||||
// Act
|
||||
_client = await AuthHelper.LoginAsync(_fixture, "approver1");
|
||||
var idempotencyKey = Guid.NewGuid().ToString();
|
||||
_client.DefaultRequestHeaders.Add("Idempotency-Key", idempotencyKey);
|
||||
|
||||
var approveResponse = await _client.PostAsJsonAsync(
|
||||
$"/api/v1/digitization-batches/{batchId}/approve",
|
||||
new ApproveRequest(EnableRetroactiveAlerts: false));
|
||||
|
||||
// Assert
|
||||
approveResponse.StatusCode.Should().Be(HttpStatusCode.OK);
|
||||
|
||||
var result = await approveResponse.Content
|
||||
.ReadFromJsonAsync<ApiResponse<PromotionResultResponse>>();
|
||||
|
||||
result!.Data!.OutboxEventsWritten.Should().Be(3,
|
||||
"live_capture track should always write outbox events");
|
||||
|
||||
using var scope = _fixture.Services.CreateScope();
|
||||
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
|
||||
|
||||
var observations = await db.Observations
|
||||
.Where(o => o.SourceBatchId == batchId)
|
||||
.ToListAsync();
|
||||
observations.Should().AllSatisfy(o =>
|
||||
o.Source.Should().Be("live_capture"));
|
||||
|
||||
_client.DefaultRequestHeaders.Remove("Idempotency-Key");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Idempotent promotion: approve twice with same Idempotency-Key produces one set of live rows.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task Approve_SameIdempotencyKey_ReturnsIdenticalResult_NoDuplicateRows()
|
||||
{
|
||||
// Arrange
|
||||
var batchId = await BatchPipelineHelper.CreateAndVerifyBatchAsync(_fixture);
|
||||
|
||||
_client = await AuthHelper.LoginAsync(_fixture, "approver1");
|
||||
var idempotencyKey = Guid.NewGuid().ToString();
|
||||
|
||||
// Act: first approve
|
||||
_client.DefaultRequestHeaders.Add("Idempotency-Key", idempotencyKey);
|
||||
|
||||
var firstResponse = await _client.PostAsJsonAsync(
|
||||
$"/api/v1/digitization-batches/{batchId}/approve",
|
||||
new ApproveRequest(EnableRetroactiveAlerts: false));
|
||||
|
||||
firstResponse.StatusCode.Should().Be(HttpStatusCode.OK);
|
||||
var firstResult = await firstResponse.Content
|
||||
.ReadFromJsonAsync<ApiResponse<PromotionResultResponse>>();
|
||||
|
||||
// Act: second approve with same key
|
||||
_client.DefaultRequestHeaders.Remove("Idempotency-Key");
|
||||
_client.DefaultRequestHeaders.Add("Idempotency-Key", idempotencyKey);
|
||||
|
||||
var secondResponse = await _client.PostAsJsonAsync(
|
||||
$"/api/v1/digitization-batches/{batchId}/approve",
|
||||
new ApproveRequest(EnableRetroactiveAlerts: false));
|
||||
|
||||
secondResponse.StatusCode.Should().Be(HttpStatusCode.OK);
|
||||
var secondResult = await secondResponse.Content
|
||||
.ReadFromJsonAsync<ApiResponse<PromotionResultResponse>>();
|
||||
|
||||
// Assert: identical results
|
||||
secondResult!.Data!.PatientId.Should().Be(firstResult!.Data!.PatientId);
|
||||
secondResult.Data.EncounterId.Should().Be(firstResult.Data.EncounterId);
|
||||
secondResult.Data.ObservationIds.Should().BeEquivalentTo(firstResult.Data.ObservationIds);
|
||||
secondResult.Data.Mrn.Should().Be(firstResult.Data.Mrn);
|
||||
|
||||
// Assert: only one set of rows in database
|
||||
using var scope = _fixture.Services.CreateScope();
|
||||
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
|
||||
|
||||
var observations = await db.Observations
|
||||
.Where(o => o.SourceBatchId == batchId)
|
||||
.ToListAsync();
|
||||
observations.Should().HaveCount(3,
|
||||
"idempotent replay should not create duplicate observations");
|
||||
|
||||
_client.DefaultRequestHeaders.Remove("Idempotency-Key");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Separation of duties: entry clerk cannot approve their own batch.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task Approve_ByEntryClerk_Returns409()
|
||||
{
|
||||
// Arrange
|
||||
var batchId = await BatchPipelineHelper.CreateAndVerifyBatchAsync(_fixture);
|
||||
|
||||
// Act: try to approve as entry1 (who entered the data)
|
||||
_client = await AuthHelper.LoginAsync(_fixture, "entry1");
|
||||
_client.DefaultRequestHeaders.Add("Idempotency-Key", Guid.NewGuid().ToString());
|
||||
|
||||
var approveResponse = await _client.PostAsJsonAsync(
|
||||
$"/api/v1/digitization-batches/{batchId}/approve",
|
||||
new ApproveRequest());
|
||||
|
||||
// Assert: either 403 (wrong role) or 409 (separation of duties)
|
||||
// entry1 has DataEntryClerk role, so the [Authorize(Roles = ...)] will reject with 403
|
||||
approveResponse.StatusCode.Should().Be(HttpStatusCode.Forbidden);
|
||||
|
||||
_client.DefaultRequestHeaders.Remove("Idempotency-Key");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Missing Idempotency-Key header returns 400.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task Approve_MissingIdempotencyKey_Returns400()
|
||||
{
|
||||
// Arrange
|
||||
var batchId = await BatchPipelineHelper.CreateAndVerifyBatchAsync(_fixture);
|
||||
_client = await AuthHelper.LoginAsync(_fixture, "approver1");
|
||||
|
||||
// Act: approve without Idempotency-Key header
|
||||
var approveResponse = await _client.PostAsJsonAsync(
|
||||
$"/api/v1/digitization-batches/{batchId}/approve",
|
||||
new ApproveRequest());
|
||||
|
||||
// Assert
|
||||
approveResponse.StatusCode.Should().Be(HttpStatusCode.BadRequest);
|
||||
|
||||
var result = await approveResponse.Content.ReadFromJsonAsync<ApiResponse<object>>();
|
||||
result!.Error!.Code.Should().Be("MISSING_IDEMPOTENCY_KEY");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Batch in wrong status (e.g., uploaded) returns 409.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task Approve_WrongStatus_Returns409()
|
||||
{
|
||||
// Arrange: create a batch but don't verify it
|
||||
_client = await AuthHelper.LoginAsync(_fixture, "intake1");
|
||||
|
||||
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("VITALS_SHEET"), "batchType" }
|
||||
};
|
||||
|
||||
var uploadResponse = await _client.PostAsync(
|
||||
"/api/v1/digitization-batches", formData);
|
||||
uploadResponse.EnsureSuccessStatusCode();
|
||||
|
||||
var uploadResult = await uploadResponse.Content.ReadFromJsonAsync<JsonElement>();
|
||||
var batchId = uploadResult.GetProperty("data").GetProperty("id").GetGuid();
|
||||
|
||||
// Act: try to approve an uploaded batch
|
||||
_client = await AuthHelper.LoginAsync(_fixture, "approver1");
|
||||
_client.DefaultRequestHeaders.Add("Idempotency-Key", Guid.NewGuid().ToString());
|
||||
|
||||
var approveResponse = await _client.PostAsJsonAsync(
|
||||
$"/api/v1/digitization-batches/{batchId}/approve",
|
||||
new ApproveRequest());
|
||||
|
||||
// Assert
|
||||
approveResponse.StatusCode.Should().Be(HttpStatusCode.Conflict);
|
||||
|
||||
_client.DefaultRequestHeaders.Remove("Idempotency-Key");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// GET /promotion-result returns correct data after promotion.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task GetPromotionResult_AfterPromotion_ReturnsLiveIds()
|
||||
{
|
||||
// Arrange: create and promote a batch
|
||||
var batchId = await BatchPipelineHelper.CreateAndVerifyBatchAsync(_fixture);
|
||||
|
||||
_client = await AuthHelper.LoginAsync(_fixture, "approver1");
|
||||
var idempotencyKey = Guid.NewGuid().ToString();
|
||||
_client.DefaultRequestHeaders.Add("Idempotency-Key", idempotencyKey);
|
||||
|
||||
var approveResponse = await _client.PostAsJsonAsync(
|
||||
$"/api/v1/digitization-batches/{batchId}/approve",
|
||||
new ApproveRequest());
|
||||
approveResponse.EnsureSuccessStatusCode();
|
||||
|
||||
var approveResult = await approveResponse.Content
|
||||
.ReadFromJsonAsync<ApiResponse<PromotionResultResponse>>();
|
||||
|
||||
_client.DefaultRequestHeaders.Remove("Idempotency-Key");
|
||||
|
||||
// Act: get promotion result
|
||||
var resultResponse = await _client.GetAsync(
|
||||
$"/api/v1/digitization-batches/{batchId}/promotion-result");
|
||||
|
||||
// Assert
|
||||
resultResponse.StatusCode.Should().Be(HttpStatusCode.OK);
|
||||
|
||||
var result = await resultResponse.Content
|
||||
.ReadFromJsonAsync<ApiResponse<PromotionResultResponse>>();
|
||||
|
||||
result!.Data!.BatchId.Should().Be(batchId);
|
||||
result.Data.PatientId.Should().Be(approveResult!.Data!.PatientId);
|
||||
result.Data.EncounterId.Should().Be(approveResult.Data.EncounterId);
|
||||
result.Data.ObservationIds.Should().HaveCount(3);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// GET /promotion-result on non-promoted batch returns 409.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task GetPromotionResult_NotPromoted_Returns409()
|
||||
{
|
||||
// Arrange
|
||||
var batchId = await BatchPipelineHelper.CreateAndVerifyBatchAsync(_fixture);
|
||||
_client = await AuthHelper.LoginAsync(_fixture, "approver1");
|
||||
|
||||
// Act: query promotion result before approving
|
||||
var resultResponse = await _client.GetAsync(
|
||||
$"/api/v1/digitization-batches/{batchId}/promotion-result");
|
||||
|
||||
// Assert
|
||||
resultResponse.StatusCode.Should().Be(HttpStatusCode.Conflict);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// MRN generation produces unique, sequential values.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task Approve_MultipleBatches_ProducesSequentialMrns()
|
||||
{
|
||||
// Arrange & Act: promote two batches with distinct patients so each gets a new MRN
|
||||
var batchId1 = await BatchPipelineHelper.CreateAndVerifyBatchAsync(_fixture);
|
||||
var batchId2 = await BatchPipelineHelper.CreateAndVerifyBatchAsync(
|
||||
_fixture, patientFullName: "Test Patient Two", patientDateOfBirth: "1991-06-20");
|
||||
|
||||
_client = await AuthHelper.LoginAsync(_fixture, "approver1");
|
||||
|
||||
_client.DefaultRequestHeaders.Add("Idempotency-Key", Guid.NewGuid().ToString());
|
||||
var result1 = await _client.PostAsJsonAsync(
|
||||
$"/api/v1/digitization-batches/{batchId1}/approve",
|
||||
new ApproveRequest());
|
||||
_client.DefaultRequestHeaders.Remove("Idempotency-Key");
|
||||
|
||||
_client.DefaultRequestHeaders.Add("Idempotency-Key", Guid.NewGuid().ToString());
|
||||
var result2 = await _client.PostAsJsonAsync(
|
||||
$"/api/v1/digitization-batches/{batchId2}/approve",
|
||||
new ApproveRequest());
|
||||
_client.DefaultRequestHeaders.Remove("Idempotency-Key");
|
||||
|
||||
var promotion1 = (await result1.Content
|
||||
.ReadFromJsonAsync<ApiResponse<PromotionResultResponse>>())!.Data!;
|
||||
var promotion2 = (await result2.Content
|
||||
.ReadFromJsonAsync<ApiResponse<PromotionResultResponse>>())!.Data!;
|
||||
|
||||
// Assert: MRNs are unique
|
||||
promotion1.Mrn.Should().NotBe(promotion2.Mrn);
|
||||
promotion1.Mrn.Should().StartWith("VCR-");
|
||||
promotion2.Mrn.Should().StartWith("VCR-");
|
||||
|
||||
// Assert: MRNs follow sequential pattern
|
||||
var mrn1Num = int.Parse(promotion1.Mrn.Split('-')[1]);
|
||||
var mrn2Num = int.Parse(promotion2.Mrn.Split('-')[1]);
|
||||
(mrn2Num - mrn1Num).Should().BeGreaterThanOrEqualTo(1);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user