using FMDAL.CustomCode.ScanSession; using FMDAL.DTO.ScanSession; 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.Threading; using System.Threading.Tasks; using static GB5Shared.GB5Constant.Constant; namespace FMBLL.ScanSession { // TSCANSESSION is a full-CRUD master with a status lifecycle: OPEN(0) -> CONFIRMED(1) / // CANCELLED(2). SaveScanSession is create-only — sessions are never resaved as a whole, only // ever progressed via ConfirmScanSession/CancelScanSession, matching the module's design note // that there is no generic "update" event type reserved for this entity. public class ScanSessionBLL : IScanSessionBLL { private readonly IScanSessionDAL _dal; private readonly IQueryExecutor _queryExecutor; private readonly AutoNumber _autoNumber; private readonly KeyInvalidate _keyInvalidate; private readonly ILogger _logger; public ScanSessionBLL( IScanSessionDAL dal, IQueryExecutor queryExecutor, AutoNumber autoNumber, KeyInvalidate keyInvalidate, ILogger logger) { _dal = dal; _queryExecutor = queryExecutor; _autoNumber = autoNumber; _keyInvalidate = keyInvalidate; _logger = logger; } public async Task GetScanSession(int scanSessionId, LoginDTO login, CancellationToken ct) => await _dal.GetScanSession(scanSessionId, login, ct).ConfigureAwait(false); public async Task SaveScanSession(ScanSessionDTO dto, LoginDTO login, CancellationToken ct) { try { GB5Trace.Step("validate-scansession", new { dto.ScanSessionId, dto.SessionCode }); if (dto.ScanSessionId != 0) throw new MethodNotAllowedException("TSCANSESSION is create-only; existing sessions cannot be resaved — use ConfirmScanSession or CancelScanSession to progress status."); if (string.IsNullOrWhiteSpace(dto.SessionCode)) throw new MethodNotAllowedException("SessionCode is required for a scan session."); // New sessions always start OPEN with zeroed rollup counters — the real counts are // only known once ConfirmScanSession aggregates the session's TSCANEVENT rows. dto.SessionStatus = 0; dto.TotalScanned = 0; dto.TotalAccepted = 0; dto.TotalRejected = 0; dto.ConfirmedOn = null; GB5Trace.Step("save-scansession", new { dto.SessionCode }); await using var tx = await _queryExecutor.BeginTransactionAsync(login).ConfigureAwait(false); try { var auto = await _autoNumber.GetNumberAsync(1, AUTONUMBERCONSTANT.SCANSESSION, login).ConfigureAwait(false); dto.ScanSessionId = auto.StartNumber; await _dal.SaveScanSession(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.OBJECTSCANSESSION, CacheKeyLevel.CLIENT_LEVEL, login); await _keyInvalidate.AllInvalidateCache(cacheKey).ConfigureAwait(false); return $"{SuccessResponse.SaveSuccessMessage} {dto.ScanSessionId}"; } catch (Exception ex) { GB5Trace.MarkFailed("save-scansession-failed", ex); _logger.LogError(ex, "SaveScanSession failed for SessionCode {SessionCode}", dto.SessionCode); throw; } } public async Task ConfirmScanSession(int scanSessionId, LoginDTO login, CancellationToken ct) { try { GB5Trace.Step("confirm-scansession", new { scanSessionId }); var counts = await _dal.GetScanEventCountsBySession(scanSessionId, login, ct).ConfigureAwait(false); GB5Trace.Step("confirm-scansession-counts", new { scanSessionId, counts.TotalScanned, counts.TotalAccepted, counts.TotalRejected }); await using var tx = await _queryExecutor.BeginTransactionAsync(login).ConfigureAwait(false); try { var rows = await _dal.ConfirmScanSession(scanSessionId, counts, login.UserId, login, tx, ct).ConfigureAwait(false); if (rows == 0) throw new MethodNotAllowedException($"ScanSession {scanSessionId} cannot be confirmed — it was not found or is not currently OPEN."); await tx.CommitAsync(ct).ConfigureAwait(false); } catch { await tx.RollbackAsync(ct).ConfigureAwait(false); throw; } var cacheKey = new CacheKeyGeneration().KeyGeneration(scanSessionId, EntityConstant.OBJECTSCANSESSION, CacheKeyLevel.CLIENT_LEVEL, login); await _keyInvalidate.AllInvalidateCache(cacheKey).ConfigureAwait(false); return SuccessResponse.UpdateSuccess; } catch (Exception ex) { GB5Trace.MarkFailed("confirm-scansession-failed", ex); _logger.LogError(ex, "ConfirmScanSession failed for ScanSessionId {Id}", scanSessionId); throw; } } public async Task CancelScanSession(int scanSessionId, LoginDTO login, CancellationToken ct) { try { GB5Trace.Step("cancel-scansession", new { scanSessionId }); await using var tx = await _queryExecutor.BeginTransactionAsync(login).ConfigureAwait(false); try { var rows = await _dal.CancelScanSession(scanSessionId, login.UserId, login, tx, ct).ConfigureAwait(false); if (rows == 0) throw new MethodNotAllowedException($"ScanSession {scanSessionId} cannot be cancelled — it was not found or is not currently OPEN."); await tx.CommitAsync(ct).ConfigureAwait(false); } catch { await tx.RollbackAsync(ct).ConfigureAwait(false); throw; } var cacheKey = new CacheKeyGeneration().KeyGeneration(scanSessionId, EntityConstant.OBJECTSCANSESSION, CacheKeyLevel.CLIENT_LEVEL, login); await _keyInvalidate.AllInvalidateCache(cacheKey).ConfigureAwait(false); return SuccessResponse.UpdateSuccess; } catch (Exception ex) { GB5Trace.MarkFailed("cancel-scansession-failed", ex); _logger.LogError(ex, "CancelScanSession failed for ScanSessionId {Id}", scanSessionId); throw; } } } }