239 lines
8.5 KiB
C#
239 lines
8.5 KiB
C#
using AVSCartonShipmentVerifier.DTOs;
|
|
using AVSCartonShipmentVerifier.Configuration;
|
|
using AVSCartonShipmentVerifier.ScanHistory;
|
|
using Microsoft.EntityFrameworkCore;
|
|
using Microsoft.Extensions.Options;
|
|
|
|
namespace AVSCartonShipmentVerifier.Services;
|
|
|
|
public sealed class SqliteScanHistoryStore(
|
|
ScanHistoryDbContext dbContext,
|
|
IOptions<ScanHistoryOptions> options,
|
|
ILogger<SqliteScanHistoryStore> logger) : IScanHistoryStore
|
|
{
|
|
private readonly ScanHistoryDbContext _dbContext = dbContext;
|
|
private readonly ScanHistoryOptions _options = options.Value;
|
|
private readonly ILogger<SqliteScanHistoryStore> _logger = logger;
|
|
|
|
public async Task AddSuccessfulScanAsync(ScannedRecordDto record, CancellationToken cancellationToken = default)
|
|
{
|
|
try
|
|
{
|
|
_dbContext.ScanHistoryEntries.Add(new ScanHistoryEntry
|
|
{
|
|
ModelNumber = record.ModelNumber,
|
|
UniqueNumber = record.UniqueNumber,
|
|
ExistsInSystem = record.ExistsInSystem,
|
|
ShipmentMarked = record.ShipmentMarked,
|
|
MovedToWarehouseAtUtc = record.MovedToWarehouseAtUtc,
|
|
ProcessedAtUtc = record.ProcessedAtUtc
|
|
});
|
|
|
|
await _dbContext.SaveChangesAsync(cancellationToken);
|
|
await TrimSuccessfulAsync(cancellationToken);
|
|
}
|
|
catch (Exception exception)
|
|
{
|
|
_logger.LogError(exception, "Failed to persist successful scan history entry.");
|
|
}
|
|
}
|
|
|
|
public async Task AddRejectedScanAsync(RejectedScanRecordDto record, CancellationToken cancellationToken = default)
|
|
{
|
|
try
|
|
{
|
|
_dbContext.RejectedScanEntries.Add(new RejectedScanEntry
|
|
{
|
|
RawInputValue = record.RawInputValue,
|
|
ModelNumber = record.ModelNumber,
|
|
UniqueNumber = record.UniqueNumber,
|
|
RejectionReason = record.RejectionReason,
|
|
ProcessedAtUtc = record.ProcessedAtUtc
|
|
});
|
|
|
|
await _dbContext.SaveChangesAsync(cancellationToken);
|
|
await TrimRejectedAsync(cancellationToken);
|
|
}
|
|
catch (Exception exception)
|
|
{
|
|
_logger.LogError(exception, "Failed to persist rejected scan history entry.");
|
|
}
|
|
}
|
|
|
|
public async Task<IReadOnlyCollection<ScannedRecordDto>> GetLastFiveSuccessfulScansAsync(CancellationToken cancellationToken = default)
|
|
{
|
|
try
|
|
{
|
|
var entries = await _dbContext.ScanHistoryEntries
|
|
.AsNoTracking()
|
|
.Where(x => x.ShipmentMarked)
|
|
.OrderByDescending(x => x.ProcessedAtUtc)
|
|
.Take(5)
|
|
.ToArrayAsync(cancellationToken);
|
|
|
|
return MapSuccessful(entries);
|
|
}
|
|
catch (Exception exception) when (TryHandleReadCancellation(exception, cancellationToken, "Successful scan history (last five)"))
|
|
{
|
|
return Array.Empty<ScannedRecordDto>();
|
|
}
|
|
catch (Exception exception)
|
|
{
|
|
_logger.LogError(exception, "Failed to read successful scan history.");
|
|
return Array.Empty<ScannedRecordDto>();
|
|
}
|
|
}
|
|
|
|
public async Task<IReadOnlyCollection<RejectedScanRecordDto>> GetLastFiveRejectedScansAsync(CancellationToken cancellationToken = default)
|
|
{
|
|
try
|
|
{
|
|
var entries = await _dbContext.RejectedScanEntries
|
|
.AsNoTracking()
|
|
.OrderByDescending(x => x.ProcessedAtUtc)
|
|
.Take(5)
|
|
.ToArrayAsync(cancellationToken);
|
|
|
|
return MapRejected(entries);
|
|
}
|
|
catch (Exception exception) when (TryHandleReadCancellation(exception, cancellationToken, "Rejected scan history (last five)"))
|
|
{
|
|
return Array.Empty<RejectedScanRecordDto>();
|
|
}
|
|
catch (Exception exception)
|
|
{
|
|
_logger.LogError(exception, "Failed to read rejected scan history.");
|
|
return Array.Empty<RejectedScanRecordDto>();
|
|
}
|
|
}
|
|
|
|
public async Task<IReadOnlyCollection<ScannedRecordDto>> GetAllSuccessfulScansAsync(CancellationToken cancellationToken = default)
|
|
{
|
|
try
|
|
{
|
|
var entries = await _dbContext.ScanHistoryEntries
|
|
.AsNoTracking()
|
|
.Where(x => x.ShipmentMarked)
|
|
.OrderByDescending(x => x.ProcessedAtUtc)
|
|
.ToArrayAsync(cancellationToken);
|
|
|
|
return MapSuccessful(entries);
|
|
}
|
|
catch (Exception exception) when (TryHandleReadCancellation(exception, cancellationToken, "Successful scan history (all)"))
|
|
{
|
|
return Array.Empty<ScannedRecordDto>();
|
|
}
|
|
catch (Exception exception)
|
|
{
|
|
_logger.LogError(exception, "Failed to read full successful scan history.");
|
|
return Array.Empty<ScannedRecordDto>();
|
|
}
|
|
}
|
|
|
|
public async Task<IReadOnlyCollection<RejectedScanRecordDto>> GetAllRejectedScansAsync(CancellationToken cancellationToken = default)
|
|
{
|
|
try
|
|
{
|
|
var entries = await _dbContext.RejectedScanEntries
|
|
.AsNoTracking()
|
|
.OrderByDescending(x => x.ProcessedAtUtc)
|
|
.ToArrayAsync(cancellationToken);
|
|
|
|
return MapRejected(entries);
|
|
}
|
|
catch (Exception exception) when (TryHandleReadCancellation(exception, cancellationToken, "Rejected scan history (all)"))
|
|
{
|
|
return Array.Empty<RejectedScanRecordDto>();
|
|
}
|
|
catch (Exception exception)
|
|
{
|
|
_logger.LogError(exception, "Failed to read full rejected scan history.");
|
|
return Array.Empty<RejectedScanRecordDto>();
|
|
}
|
|
}
|
|
|
|
private bool TryHandleReadCancellation(Exception exception, CancellationToken cancellationToken, string operation)
|
|
{
|
|
if (exception is not OperationCanceledException || !cancellationToken.IsCancellationRequested)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
_logger.LogDebug(exception, "{Operation} read was cancelled.", operation);
|
|
return true;
|
|
}
|
|
|
|
private static ScannedRecordDto[] MapSuccessful(IEnumerable<ScanHistoryEntry> entries) =>
|
|
entries
|
|
.Select(x => new ScannedRecordDto
|
|
{
|
|
ModelNumber = x.ModelNumber,
|
|
UniqueNumber = x.UniqueNumber,
|
|
ExistsInSystem = x.ExistsInSystem,
|
|
ShipmentMarked = x.ShipmentMarked,
|
|
MovedToWarehouseAtUtc = x.MovedToWarehouseAtUtc.HasValue
|
|
? DateTime.SpecifyKind(x.MovedToWarehouseAtUtc.Value, DateTimeKind.Utc)
|
|
: null,
|
|
ProcessedAtUtc = DateTime.SpecifyKind(x.ProcessedAtUtc, DateTimeKind.Utc)
|
|
})
|
|
.ToArray();
|
|
|
|
private static RejectedScanRecordDto[] MapRejected(IEnumerable<RejectedScanEntry> entries) =>
|
|
entries
|
|
.Select(x => new RejectedScanRecordDto
|
|
{
|
|
RawInputValue = x.RawInputValue,
|
|
ModelNumber = x.ModelNumber,
|
|
UniqueNumber = x.UniqueNumber,
|
|
RejectionReason = x.RejectionReason,
|
|
ProcessedAtUtc = DateTime.SpecifyKind(x.ProcessedAtUtc, DateTimeKind.Utc)
|
|
})
|
|
.ToArray();
|
|
|
|
private async Task TrimSuccessfulAsync(CancellationToken cancellationToken)
|
|
{
|
|
var max = _options.MaxRecordsToKeep;
|
|
if (max <= 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
var toDelete = await _dbContext.ScanHistoryEntries
|
|
.OrderByDescending(x => x.ProcessedAtUtc)
|
|
.Skip(max)
|
|
.Select(x => x.Id)
|
|
.ToArrayAsync(cancellationToken);
|
|
|
|
if (toDelete.Length == 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
_dbContext.ScanHistoryEntries.RemoveRange(toDelete.Select(id => new ScanHistoryEntry { Id = id }));
|
|
await _dbContext.SaveChangesAsync(cancellationToken);
|
|
}
|
|
|
|
private async Task TrimRejectedAsync(CancellationToken cancellationToken)
|
|
{
|
|
var max = _options.MaxRecordsToKeep;
|
|
if (max <= 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
var toDelete = await _dbContext.RejectedScanEntries
|
|
.OrderByDescending(x => x.ProcessedAtUtc)
|
|
.Skip(max)
|
|
.Select(x => x.Id)
|
|
.ToArrayAsync(cancellationToken);
|
|
|
|
if (toDelete.Length == 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
_dbContext.RejectedScanEntries.RemoveRange(toDelete.Select(id => new RejectedScanEntry { Id = id }));
|
|
await _dbContext.SaveChangesAsync(cancellationToken);
|
|
}
|
|
}
|