feature: Corrections and Supersession
This commit is contained in:
@@ -30,14 +30,47 @@ public class BatchService : IBatchService
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task<DigitizationBatch> CreateAsync(
|
||||
public async Task<CreateBatchResult> CreateAsync(
|
||||
Stream fileStream, string contentType, BatchType batchType,
|
||||
BatchTrack track, Guid? patientId, Guid actorUserId)
|
||||
BatchTrack track, Guid? patientId, Guid? supersedesBatchId, Guid actorUserId)
|
||||
{
|
||||
// Supersession validation: target batch must exist and be in Promoted status
|
||||
DigitizationBatch? supersededBatch = null;
|
||||
if (supersedesBatchId.HasValue)
|
||||
{
|
||||
supersededBatch = await _db.DigitizationBatches
|
||||
.FirstOrDefaultAsync(b => b.Id == supersedesBatchId.Value);
|
||||
|
||||
if (supersededBatch is null)
|
||||
throw new NotFoundException(
|
||||
$"Batch {supersedesBatchId.Value} not found.",
|
||||
"SUPERSEDED_BATCH_NOT_FOUND");
|
||||
|
||||
if (supersededBatch.Status != BatchStatus.Promoted)
|
||||
throw new ValidationException(
|
||||
"Only promoted batches can be superseded. " +
|
||||
$"Batch {supersedesBatchId.Value} is in '{supersededBatch.Status.ToDbString()}' status.",
|
||||
"SUPERSEDED_BATCH_NOT_PROMOTED");
|
||||
|
||||
// Prevent supersession chains: the target batch must not itself be a correction
|
||||
// that has already been superseded by another promoted correction
|
||||
var existingCorrection = await _db.DigitizationBatches
|
||||
.AnyAsync(b => b.SupersedesBatchId == supersedesBatchId.Value
|
||||
&& b.Status == BatchStatus.Promoted);
|
||||
|
||||
if (existingCorrection)
|
||||
throw new ConflictException(
|
||||
$"Batch {supersedesBatchId.Value} has already been superseded by a promoted correction. " +
|
||||
"Create a new correction against the latest promoted batch instead.",
|
||||
"BATCH_ALREADY_SUPERSEDED");
|
||||
|
||||
// Inherit patient context from the superseded batch
|
||||
patientId ??= supersededBatch.PatientId;
|
||||
}
|
||||
|
||||
var (objectKey, sha256, fileSize) = await _storage.UploadAsync(fileStream, contentType, Guid.NewGuid());
|
||||
|
||||
// Duplicate detection: same SHA-256 for same patient within 24 hours.
|
||||
// Cross-patient duplicates (same form scanned for two patients) are allowed.
|
||||
// Duplicate detection: same SHA-256 for same patient within 24 hours
|
||||
if (patientId.HasValue)
|
||||
{
|
||||
var cutoff = DateTimeOffset.UtcNow.AddHours(-24);
|
||||
@@ -63,6 +96,7 @@ public class BatchService : IBatchService
|
||||
DocumentRef = objectKey,
|
||||
DocumentSha256 = sha256,
|
||||
EnableRetroactiveAlerts = false,
|
||||
SupersedesBatchId = supersedesBatchId,
|
||||
CreatedAt = DateTimeOffset.UtcNow,
|
||||
UpdatedAt = DateTimeOffset.UtcNow
|
||||
};
|
||||
@@ -78,13 +112,21 @@ public class BatchService : IBatchService
|
||||
UploadedAt = DateTimeOffset.UtcNow
|
||||
};
|
||||
|
||||
var eventType = supersedesBatchId.HasValue
|
||||
? DigitizationEventType.CorrectionUploaded
|
||||
: DigitizationEventType.Uploaded;
|
||||
var eventMetadata = supersedesBatchId.HasValue
|
||||
? JsonSerializer.Serialize(new { supersedesBatchId = supersedesBatchId.Value })
|
||||
: null;
|
||||
|
||||
var evt = new DigitizationEvent
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
BatchId = batchId,
|
||||
EventType = DigitizationEventType.Uploaded,
|
||||
EventType = eventType,
|
||||
ActorUserId = actorUserId,
|
||||
OccurredAt = DateTimeOffset.UtcNow
|
||||
OccurredAt = DateTimeOffset.UtcNow,
|
||||
MetadataJson = eventMetadata
|
||||
};
|
||||
|
||||
_db.DigitizationBatches.Add(batch);
|
||||
@@ -92,8 +134,19 @@ public class BatchService : IBatchService
|
||||
_db.DigitizationEvents.Add(evt);
|
||||
await _db.SaveChangesAsync();
|
||||
|
||||
_logger.LogInformation("Batch {BatchId} created with document {ObjectKey}", batchId, objectKey);
|
||||
return batch;
|
||||
_logger.LogInformation(
|
||||
"Batch {BatchId} created (correction={IsCorrection}, supersedes={SupersedesBatchId})",
|
||||
batchId, supersedesBatchId.HasValue, supersedesBatchId);
|
||||
|
||||
SupersessionInfo? supersession = null;
|
||||
if (supersededBatch is not null)
|
||||
{
|
||||
var obsCount = await _db.Observations
|
||||
.CountAsync(o => o.SourceBatchId == supersededBatch.Id);
|
||||
supersession = BatchDetailResponse.ToSupersessionInfo(supersededBatch, obsCount);
|
||||
}
|
||||
|
||||
return new CreateBatchResult(batch, supersession);
|
||||
}
|
||||
|
||||
public async Task<DigitizationBatch> GetByIdAsync(Guid id)
|
||||
|
||||
Reference in New Issue
Block a user