using AVSCartonShipmentVerifier.DTOs; using AVSCartonShipmentVerifier.Repositories; namespace AVSCartonShipmentVerifier.Services; public sealed class ShipmentVerificationService( ICartonVerificationRepository repository, IScanHistoryStore historyStore, ILogger logger) : IShipmentVerificationService { private readonly ICartonVerificationRepository _repository = repository; private readonly IScanHistoryStore _historyStore = historyStore; private readonly ILogger _logger = logger; public async Task 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: bool marked; try { marked = 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 (!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 success = new VerificationResultDto { ModelNumber = parseResult.ModelNumber, UniqueNumber = parseResult.UniqueNumber, RawQrValue = raw, ExistsInSystem = true, ShipmentMarked = true, 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, ProcessedAtUtc = result.ProcessedAtUtc }; }