using FMDAL.CustomCode.ScanEvent; using FMDAL.DTO.ScanEvent; using GB5Shared.DaprCache; using GB5Shared.DTO.Framework.Login; using GB5Shared.GB5Exception; using GB5Shared.GenerateAutoNumber; using GB5Shared.QueryExecutor; using GB5Shared.Resource.Response; using GB5Shared.Telemetry; using Microsoft.Extensions.Logging; using System; using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; using static GB5Shared.GB5Constant.Constant; namespace FMBLL.ScanEvent { // TSCANEVENT is a raw per-scan capture log, child of TSCANSESSION — insert-only, matching // AssetLedgerBLL's append-only pattern. Unlike AssetLedger there is no void method either: // TSCANEVENT carries no STATUS column at all (confirmed absent in the DDL) — a scan event, // once captured, is never edited or voided, only ever superseded by a later scan event row. public class ScanEventBLL : IScanEventBLL { private readonly IScanEventDAL _dal; private readonly IQueryExecutor _queryExecutor; private readonly AutoNumber _autoNumber; private readonly KeyInvalidate _keyInvalidate; private readonly ILogger _logger; public ScanEventBLL( IScanEventDAL dal, IQueryExecutor queryExecutor, AutoNumber autoNumber, KeyInvalidate keyInvalidate, ILogger logger) { _dal = dal; _queryExecutor = queryExecutor; _autoNumber = autoNumber; _keyInvalidate = keyInvalidate; _logger = logger; } public async Task GetScanEvent(int scanEventId, LoginDTO login, CancellationToken ct) => await _dal.GetScanEvent(scanEventId, login, ct).ConfigureAwait(false); public async Task> GetScanEventListBySession(int scanSessionId, LoginDTO login, CancellationToken ct) => await _dal.GetScanEventListBySession(scanSessionId, login, ct).ConfigureAwait(false); public async Task SaveScanEvent(ScanEventDTO dto, LoginDTO login, CancellationToken ct) { try { GB5Trace.Step("validate-scanevent", new { dto.ScanSessionId, dto.RawScannedValue }); if (dto.ScanEventId != 0) throw new MethodNotAllowedException("TSCANEVENT is append-only; existing scan events cannot be updated."); if (dto.ScanSessionId <= 0) throw new MethodNotAllowedException("ScanSessionId is required for a scan event."); if (string.IsNullOrWhiteSpace(dto.RawScannedValue)) throw new MethodNotAllowedException("RawScannedValue is required for a scan event."); GB5Trace.Step("save-scanevent", new { dto.ScanSessionId }); await using var tx = await _queryExecutor.BeginTransactionAsync(login).ConfigureAwait(false); try { var auto = await _autoNumber.GetNumberAsync(1, AUTONUMBERCONSTANT.SCANEVENT, login).ConfigureAwait(false); dto.ScanEventId = auto.StartNumber; await _dal.SaveScanEvent(dto, login, tx, ct).ConfigureAwait(false); await tx.CommitAsync(ct).ConfigureAwait(false); } catch { await tx.RollbackAsync(ct).ConfigureAwait(false); throw; } var cacheKey = new CacheKeyGeneration().KeyGeneration(dto.ScanSessionId, EntityConstant.OBJECTSCANEVENT, CacheKeyLevel.CLIENT_LEVEL, login); await _keyInvalidate.AllInvalidateCache(cacheKey).ConfigureAwait(false); return $"{SuccessResponse.SaveSuccessMessage} {dto.ScanEventId}"; } catch (Exception ex) { GB5Trace.MarkFailed("save-scanevent-failed", ex); _logger.LogError(ex, "SaveScanEvent failed for ScanSessionId {ScanSessionId}", dto.ScanSessionId); throw; } } } }