using System.Text.Json; using Microsoft.EntityFrameworkCore; using VigilCare.ClinicalContracts.Sync; public class ClinicalSyncService : IClinicalSyncService { private readonly AppDbContext _db; private readonly ILogger _logger; public ClinicalSyncService(AppDbContext db, ILogger logger) { _db = db; _logger = logger; } public async Task UploadBatchAsync( ClinicalSyncBatchRequest request, CancellationToken ct) { var existing = await _db.ClinicalSyncBatches .AsNoTracking() .FirstOrDefaultAsync(b => b.BatchReference == request.BatchReference, ct); if (existing is not null) return new ClinicalBatchUploadResponse(existing.Id, existing.Status.ToDbString()); var gateway = await _db.WardGateways .FirstOrDefaultAsync(g => g.Id == request.GatewayId && g.SiteId == request.SiteId, ct) ?? throw new NotFoundException("Gateway not found for site.", "GATEWAY_NOT_FOUND"); var payload = JsonSerializer.Serialize(request); var batch = new ClinicalSyncBatch(gateway.Id, request.SiteId, request.BatchReference, payload); var batchId = Guid.NewGuid(); _db.Entry(batch).Property(nameof(ClinicalSyncBatch.Id)).CurrentValue = batchId; await using var tx = await _db.Database.BeginTransactionAsync(ct); _db.ClinicalSyncBatches.Add(batch); _db.OutboxEvents.Add(new OutboxEvent { Id = Guid.NewGuid(), Topic = ClinicalSyncOptions.BatchReceivedOutboxTopic, Payload = JsonSerializer.Serialize(new { batchId, gatewayId = gateway.Id, siteId = request.SiteId }), PartitionKey = gateway.Id.ToString(), CreatedAt = DateTimeOffset.UtcNow }); await _db.SaveChangesAsync(ct); await tx.CommitAsync(ct); _logger.LogInformation( "Sync batch {BatchId} received from gateway {GatewayId} — {ObsCount} observations", batch.Id, gateway.Id, request.Observations.Count); return new ClinicalBatchUploadResponse(batch.Id, "RECEIVED"); } public async Task GetBatchStatusAsync(Guid batchId, CancellationToken ct) { var batch = await _db.ClinicalSyncBatches .AsNoTracking() .Include(b => b.Conflicts) .FirstOrDefaultAsync(b => b.Id == batchId, ct) ?? throw new NotFoundException("Sync batch not found.", "BATCH_NOT_FOUND"); var conflicts = batch.Conflicts.Select(c => new ClinicalConflictDetail(c.ClientRef, c.ItemType, c.ConflictReason)).ToList(); return new ClinicalBatchStatusResponse(batch.Id, batch.Status.ToDbString(), conflicts); } public async Task> GetSyncHistoryAsync( Guid siteId, Guid gatewayId, int limit, CancellationToken ct) { var gatewayExists = await _db.WardGateways .AnyAsync(g => g.Id == gatewayId && g.SiteId == siteId, ct); if (!gatewayExists) throw new NotFoundException("Gateway not found for site.", "GATEWAY_NOT_FOUND"); return await _db.ClinicalSyncBatches .AsNoTracking() .Where(b => b.GatewayId == gatewayId && b.SiteId == siteId) .OrderByDescending(b => b.SubmittedAt) .Take(limit) .Select(b => new ClinicalSyncHistoryItem( b.Id, b.BatchReference, b.Status.ToDbString(), b.Conflicts.Count, b.SubmittedAt)) .ToListAsync(ct); } }