185 lines
8.4 KiB
C#
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
|
|
};
|
|
}
|