using FMDAL.CustomCode.AssetLedger; using FMDAL.DTO.AssetLedger; 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.Data.Common; using System.Threading; using System.Threading.Tasks; using static GB5Shared.GB5Constant.Constant; namespace FMBLL.AssetLedger { // TASSETLEDGER is insert-only + soft-void (RAM design §4.9) — every save is a new row, // never an update. BizTransactionTypeId is resolved by the caller (scan-event handler, // manual-move endpoint, scheduler) — this BLL never hardcodes or infers it. public class AssetLedgerBLL : IAssetLedgerBLL { private readonly IAssetLedgerDAL _dal; private readonly IQueryExecutor _queryExecutor; private readonly AutoNumber _autoNumber; private readonly KeyInvalidate _keyInvalidate; private readonly ILogger _logger; public AssetLedgerBLL( IAssetLedgerDAL dal, IQueryExecutor queryExecutor, AutoNumber autoNumber, KeyInvalidate keyInvalidate, ILogger logger) { _dal = dal; _queryExecutor = queryExecutor; _autoNumber = autoNumber; _keyInvalidate = keyInvalidate; _logger = logger; } public async Task GetAssetLedger(int assetLedgerId, LoginDTO login, CancellationToken ct) => await _dal.GetAssetLedger(assetLedgerId, login, ct).ConfigureAwait(false); public async Task> GetAssetLedgerListByAsset(int assetId, LoginDTO login, CancellationToken ct) => await _dal.GetAssetLedgerListByAsset(assetId, login, ct).ConfigureAwait(false); public async Task> GetAssetLedgerListByReference(string referenceType, int referenceId, LoginDTO login, CancellationToken ct) => await _dal.GetAssetLedgerListByReference(referenceType, referenceId, login, ct).ConfigureAwait(false); public async Task SaveAssetLedger(AssetLedgerDTO dto, LoginDTO login, CancellationToken ct) { try { await using var tx = await _queryExecutor.BeginTransactionAsync(login).ConfigureAwait(false); try { await SaveAssetLedgerCoreAsync(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.AssetId, EntityConstant.OBJECTASSETLEDGER, CacheKeyLevel.CLIENT_LEVEL, login); await _keyInvalidate.AllInvalidateCache(cacheKey).ConfigureAwait(false); return $"{SuccessResponse.SaveSuccessMessage} {dto.AssetLedgerId}"; } catch (Exception ex) { GB5Trace.MarkFailed("save-assetledger-failed", ex); _logger.LogError(ex, "SaveAssetLedger failed for AssetId {AssetId}", dto.AssetId); throw; } } // Transaction-scoped variant — caller (e.g. AssetScanEventHandler) owns the transaction // so the ledger insert and the MASSET snapshot update commit/rollback atomically together, // per RAM design §4.2/§4.12: snapshot columns must be written synchronously with the // triggering ledger row, never as a separate follow-up write. No cache invalidation here — // the caller invalidates once after its own commit succeeds. public async Task SaveAssetLedgerInTransaction(AssetLedgerDTO dto, LoginDTO login, DbTransaction tx, CancellationToken ct) { await SaveAssetLedgerCoreAsync(dto, login, tx, ct).ConfigureAwait(false); return dto.AssetLedgerId; } private async Task SaveAssetLedgerCoreAsync(AssetLedgerDTO dto, LoginDTO login, DbTransaction tx, CancellationToken ct) { GB5Trace.Step("validate-assetledger", new { dto.AssetId, dto.BizTransactionTypeId }); if (dto.AssetLedgerId != 0) throw new MethodNotAllowedException("TASSETLEDGER is append-only; existing entries cannot be updated."); if (dto.AssetId <= 0) throw new MethodNotAllowedException("AssetId is required for an asset ledger entry."); // -1 is this codebase's platform-wide "None/Not Applicable" sentinel (see e.g. // BizTransactionTypeListCriteriaDTO.PeriodId = -1 convention) — used here for AUDIT // attestation rows, which are not a business transaction and so have no type to // resolve. Only the C# default/uninitialized 0 is rejected. if (dto.BizTransactionTypeId == 0) throw new MethodNotAllowedException("BizTransactionTypeId must be resolved by the caller before saving an asset ledger entry."); GB5Trace.Step("save-assetledger", new { dto.AssetId }); var auto = await _autoNumber.GetNumberAsync(1, AUTONUMBERCONSTANT.ASSETLEDGER, login).ConfigureAwait(false); dto.AssetLedgerId = auto.StartNumber; dto.AssetLedgerNumber = $"AL{auto.StartNumber}"; await _dal.SaveAssetLedger(dto, login, tx, ct).ConfigureAwait(false); } public async Task VoidAssetLedger(int assetLedgerId, LoginDTO login, CancellationToken ct) { try { GB5Trace.Step("void-assetledger", new { assetLedgerId }); await using var tx = await _queryExecutor.BeginTransactionAsync(login).ConfigureAwait(false); try { await _dal.VoidAssetLedger(assetLedgerId, login, tx, ct).ConfigureAwait(false); await tx.CommitAsync(ct).ConfigureAwait(false); } catch { await tx.RollbackAsync(ct).ConfigureAwait(false); throw; } var cacheKey = new CacheKeyGeneration().KeyGeneration(assetLedgerId, EntityConstant.OBJECTASSETLEDGER, CacheKeyLevel.CLIENT_LEVEL, login); await _keyInvalidate.AllInvalidateCache(cacheKey).ConfigureAwait(false); return SuccessResponse.DeleteSuccessMessage; } catch (Exception ex) { GB5Trace.MarkFailed("void-assetledger-failed", ex); _logger.LogError(ex, "VoidAssetLedger failed for AssetLedgerId {Id}", assetLedgerId); throw; } } } }