feature: Reconciliation Jobs
This commit is contained in:
@@ -0,0 +1,93 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
public sealed class PendingOrdersCheck
|
||||
{
|
||||
public static readonly ReconciliationCheckType CheckType = ReconciliationCheckType.PendingOrderNoResult;
|
||||
|
||||
private readonly AppDbContext _db;
|
||||
private readonly ReconciliationPublisher _publisher;
|
||||
private readonly ReconciliationJobOptions _opts;
|
||||
private readonly ILogger<PendingOrdersCheck> _logger;
|
||||
|
||||
public PendingOrdersCheck(
|
||||
AppDbContext db,
|
||||
ReconciliationPublisher publisher,
|
||||
IOptions<ReconciliationJobOptions> opts,
|
||||
ILogger<PendingOrdersCheck> logger)
|
||||
{
|
||||
_db = db;
|
||||
_publisher = publisher;
|
||||
_opts = opts.Value;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task<int> RunAsync(CancellationToken ct)
|
||||
{
|
||||
var cutoff = DateTimeOffset.UtcNow
|
||||
.AddHours(-_opts.PendingOrderThresholdHours);
|
||||
|
||||
var staleOrders = await _db.Orders
|
||||
.Where(o => (o.Status == OrderStatus.Pending || o.Status == OrderStatus.InProgress)
|
||||
&& o.OrderedAt < cutoff
|
||||
&& o.ResultedAt == null)
|
||||
.Select(o => new
|
||||
{
|
||||
o.Id,
|
||||
o.EncounterId,
|
||||
o.OrderType,
|
||||
o.Description,
|
||||
o.OrderedAt,
|
||||
PatientId = o.Encounter!.PatientId,
|
||||
})
|
||||
.ToListAsync(ct);
|
||||
|
||||
if (!staleOrders.Any())
|
||||
{
|
||||
_logger.LogDebug("[RECONCILIATION] Check2 — no stale pending orders");
|
||||
return 0;
|
||||
}
|
||||
|
||||
var byEncounter = staleOrders
|
||||
.GroupBy(o => new { o.EncounterId, o.PatientId })
|
||||
.ToList();
|
||||
|
||||
var inserted = 0;
|
||||
foreach (var group in byEncounter)
|
||||
{
|
||||
var alreadyOpen = await _db.ReconciliationAlerts
|
||||
.AnyAsync(r => r.CheckType == CheckType
|
||||
&& r.EncounterId == group.Key.EncounterId
|
||||
&& r.ResolvedAt == null, ct);
|
||||
if (alreadyOpen) continue;
|
||||
|
||||
var lines = group.Select(o =>
|
||||
$" [{o.OrderType.ToDbString()}] {o.Description} (ordered {o.OrderedAt:u}, "
|
||||
+ $"pending {(DateTimeOffset.UtcNow - o.OrderedAt).TotalHours:F1}h) ID={o.Id}");
|
||||
var details = $"{group.Count()} order(s) pending without result for encounter "
|
||||
+ $"{group.Key.EncounterId}:\n{string.Join("\n", lines)}";
|
||||
|
||||
var rec = new ReconciliationAlert
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
CheckType = CheckType, // ReconciliationCheckType enum
|
||||
EncounterId = group.Key.EncounterId,
|
||||
PatientId = group.Key.PatientId,
|
||||
Details = details,
|
||||
CreatedAt = DateTimeOffset.UtcNow,
|
||||
};
|
||||
|
||||
_db.ReconciliationAlerts.Add(rec);
|
||||
await _db.SaveChangesAsync(ct);
|
||||
await _publisher.PublishAsync(rec, ct);
|
||||
|
||||
_logger.LogWarning(
|
||||
"[RECONCILIATION] {CheckType} — encounter {EncounterId}: {Count} stale order(s)",
|
||||
CheckType, group.Key.EncounterId, group.Count());
|
||||
|
||||
inserted++;
|
||||
}
|
||||
|
||||
return inserted;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user