AVS-Carton-Shipment-Verifier/Services/ShipmentVerificationService.cs

185 lines
8.4 KiB
C#

using AVSCartonShipmentVerifier.DTOs;
using AVSCartonShipmentVerifier.Repositories;
namespace AVSCartonShipmentVerifier.Services;
public sealed class ShipmentVerificationService(
ICartonVerificationRepository repository,
IScanHistoryStore historyStore,
ILogger<ShipmentVerificationService> logger) : IShipmentVerificationService
{
private readonly ICartonVerificationRepository _repository = repository;
private readonly IScanHistoryStore _historyStore = historyStore;
private readonly ILogger<ShipmentVerificationService> _logger = logger;
public async Task<VerificationResultDto> VerifyQrAsync(string rawQrValue, string? entryMethod, CancellationToken cancellationToken = default)
{
var nowUtc = DateTime.UtcNow;
var raw = (rawQrValue ?? string.Empty).Trim();
if (IsManualEntry(entryMethod))
{
await PersistRejectionAsync(raw, string.Empty, string.Empty, VerificationRejectionReasons.ManualEntryNotAllowed, nowUtc, cancellationToken);
return Reject(raw, string.Empty, string.Empty, VerificationRejectionReasons.ManualEntryNotAllowed, existsInSystem: false, nowUtc);
}
var parseResult = TryParseQr(raw);
if (!parseResult.IsValid)
{
await PersistRejectionAsync(raw, parseResult.ModelNumber, parseResult.UniqueNumber, VerificationRejectionReasons.InvalidFormat, nowUtc, cancellationToken);
return Reject(raw, parseResult.ModelNumber, parseResult.UniqueNumber, VerificationRejectionReasons.InvalidFormat, existsInSystem: false, nowUtc);
}
CartonVerificationStatus status;
try
{
status = await _repository.GetCartonStatusByUniqueNumberAsync(parseResult.UniqueNumber, cancellationToken);
}
catch (InvalidOperationException exception) when (exception.Message.Contains("Connection string 'VerificationDatabase' is not set", StringComparison.OrdinalIgnoreCase))
{
_logger.LogError(exception, "Verification database is not configured.");
await PersistRejectionAsync(raw, parseResult.ModelNumber, parseResult.UniqueNumber, VerificationRejectionReasons.DatabaseOrSystemError, nowUtc, cancellationToken);
return Reject(raw, parseResult.ModelNumber, parseResult.UniqueNumber, VerificationRejectionReasons.DatabaseOrSystemError, existsInSystem: false, nowUtc);
}
catch (Exception exception)
{
_logger.LogError(exception, "Verification lookup failed for unique number {UniqueNumber}.", parseResult.UniqueNumber);
await PersistRejectionAsync(raw, parseResult.ModelNumber, parseResult.UniqueNumber, VerificationRejectionReasons.DatabaseOrSystemError, nowUtc, cancellationToken);
return Reject(raw, parseResult.ModelNumber, parseResult.UniqueNumber, VerificationRejectionReasons.DatabaseOrSystemError, existsInSystem: false, nowUtc);
}
switch (status)
{
case CartonVerificationStatus.NotFound:
await PersistRejectionAsync(raw, parseResult.ModelNumber, parseResult.UniqueNumber, VerificationRejectionReasons.CartonDoesNotExist, nowUtc, cancellationToken);
return Reject(raw, parseResult.ModelNumber, parseResult.UniqueNumber, VerificationRejectionReasons.CartonDoesNotExist, existsInSystem: false, nowUtc);
case CartonVerificationStatus.AlreadyTransferred:
await PersistRejectionAsync(raw, parseResult.ModelNumber, parseResult.UniqueNumber, VerificationRejectionReasons.AlreadyTransferredDuplicateScan, nowUtc, cancellationToken);
return Reject(
raw,
parseResult.ModelNumber,
parseResult.UniqueNumber,
VerificationRejectionReasons.AlreadyTransferredDuplicateScan,
existsInSystem: true,
nowUtc);
case CartonVerificationStatus.PendingTransfer:
ShipmentMarkResult markResult;
try
{
markResult = await _repository.MarkShipmentVerifiedAsync(parseResult.UniqueNumber, cancellationToken);
}
catch (Exception exception)
{
_logger.LogError(exception, "Failed to mark shipment verified for unique number {UniqueNumber}.", parseResult.UniqueNumber);
await PersistRejectionAsync(raw, parseResult.ModelNumber, parseResult.UniqueNumber, VerificationRejectionReasons.DatabaseOrSystemError, nowUtc, cancellationToken);
return Reject(raw, parseResult.ModelNumber, parseResult.UniqueNumber, VerificationRejectionReasons.DatabaseOrSystemError, existsInSystem: true, nowUtc);
}
if (!markResult.Marked)
{
await PersistRejectionAsync(raw, parseResult.ModelNumber, parseResult.UniqueNumber, VerificationRejectionReasons.DatabaseOrSystemError, nowUtc, cancellationToken);
return Reject(raw, parseResult.ModelNumber, parseResult.UniqueNumber, VerificationRejectionReasons.DatabaseOrSystemError, existsInSystem: true, nowUtc);
}
var movedToWarehouseAtUtc = markResult.MovedToWarehouseAtUtc ?? nowUtc;
var success = new VerificationResultDto
{
ModelNumber = parseResult.ModelNumber,
UniqueNumber = parseResult.UniqueNumber,
RawQrValue = raw,
ExistsInSystem = true,
ShipmentMarked = true,
MovedToWarehouseAtUtc = movedToWarehouseAtUtc,
Message = "Record found and shipment marked successfully.",
RejectionReason = null,
ProcessedAtUtc = nowUtc
};
await _historyStore.AddSuccessfulScanAsync(ToScannedRecord(success), cancellationToken);
return success;
default:
throw new InvalidOperationException($"Unexpected carton status: {status}.");
}
}
private static bool IsManualEntry(string? entryMethod) =>
string.Equals(entryMethod?.Trim(), "Manual", StringComparison.OrdinalIgnoreCase);
private async Task PersistRejectionAsync(
string rawInput,
string modelNumber,
string uniqueNumber,
string reason,
DateTime processedAtUtc,
CancellationToken cancellationToken)
{
await _historyStore.AddRejectedScanAsync(new RejectedScanRecordDto
{
RawInputValue = rawInput,
ModelNumber = modelNumber,
UniqueNumber = uniqueNumber,
RejectionReason = reason,
ProcessedAtUtc = processedAtUtc
}, cancellationToken);
}
private static VerificationResultDto Reject(
string raw,
string modelNumber,
string uniqueNumber,
string reason,
bool existsInSystem,
DateTime processedAtUtc) => new()
{
RawQrValue = raw,
ModelNumber = modelNumber,
UniqueNumber = uniqueNumber,
ExistsInSystem = existsInSystem,
ShipmentMarked = false,
Message = reason,
RejectionReason = reason,
ProcessedAtUtc = processedAtUtc
};
private static (bool IsValid, string ModelNumber, string UniqueNumber) TryParseQr(string rawQrValue)
{
if (string.IsNullOrWhiteSpace(rawQrValue))
{
return (false, string.Empty, string.Empty);
}
if (!rawQrValue.Contains(';'))
{
return (false, string.Empty, string.Empty);
}
var parts = rawQrValue.Split(';', StringSplitOptions.TrimEntries);
if (parts.Length != 2)
{
return (false, string.Empty, string.Empty);
}
var modelNumber = parts[0];
var uniqueNumber = parts[1];
if (string.IsNullOrWhiteSpace(modelNumber) || string.IsNullOrWhiteSpace(uniqueNumber))
{
return (false, modelNumber, uniqueNumber);
}
return (true, modelNumber, uniqueNumber);
}
private static ScannedRecordDto ToScannedRecord(VerificationResultDto result) => new()
{
ModelNumber = result.ModelNumber,
UniqueNumber = result.UniqueNumber,
ExistsInSystem = result.ExistsInSystem,
ShipmentMarked = result.ShipmentMarked,
MovedToWarehouseAtUtc = result.MovedToWarehouseAtUtc,
ProcessedAtUtc = result.ProcessedAtUtc
};
}