using FMDAL.CustomCode.AssetPool; using FMDAL.DTO.AssetPool; using GB5Shared.DaprCache; using GB5Shared.DTO.Framework.Login; using GB5Shared.EventLogPublish; 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.Linq; using System.Threading; using System.Threading.Tasks; using static GB5Shared.GB5Constant.Constant; namespace FMBLL.AssetPool { public class AssetPoolBLL : IAssetPoolBLL { private readonly IAssetPoolDAL _dal; private readonly IQueryExecutor _queryExecutor; private readonly AutoNumber _autoNumber; private readonly KeyInvalidate _keyInvalidate; private readonly EventLogPublish _eventLog; private readonly ILogger _logger; public AssetPoolBLL( IAssetPoolDAL dal, IQueryExecutor queryExecutor, AutoNumber autoNumber, KeyInvalidate keyInvalidate, EventLogPublish eventLog, ILogger logger) { _dal = dal; _queryExecutor = queryExecutor; _autoNumber = autoNumber; _keyInvalidate = keyInvalidate; _eventLog = eventLog; _logger = logger; } public async Task GetAssetPool(int assetPoolId, LoginDTO login, CancellationToken ct) => await _dal.GetAssetPool(assetPoolId, login, ct).ConfigureAwait(false); public async Task> GetAssetPoolList(LoginDTO login, CancellationToken ct) => await _dal.GetAssetPoolList(login, ct).ConfigureAwait(false); public async Task SaveAssetPool(AssetPoolDTO dto, LoginDTO login, CancellationToken ct) { try { GB5Trace.Step("validate-assetpool", new { dto.AssetPoolId }); if (string.IsNullOrWhiteSpace(dto.PoolCode)) throw new ArgumentException("PoolCode is required"); if (string.IsNullOrWhiteSpace(dto.PoolName)) throw new ArgumentException("PoolName is required"); bool isNew = dto.AssetPoolId == 0; var now = DateTime.UtcNow; GB5Trace.Step("save-assetpool", new { dto.AssetPoolId, isNew }); await using var tx = await _queryExecutor.BeginTransactionAsync(login).ConfigureAwait(false); try { if (isNew) { var auto = await _autoNumber.GetNumberAsync(1, AUTONUMBERCONSTANT.ASSETPOOL, login).ConfigureAwait(false); dto.AssetPoolId = auto.StartNumber; dto.OUId = login.WorkOUId; // Resolved server-side, never trusted from the client — same tenant-filtering // rule as every other GB5 table. dto.TenantId = login.ClientId; dto.CreatedById = login.UserId; dto.CreatedOn = now; dto.ModifiedById = login.UserId; dto.ModifiedOn = now; await _dal.SaveAssetPool(dto, login, tx, ct).ConfigureAwait(false); // Every member submitted with a brand-new pool is a new row — allocate // exactly one AutoNumber per row (never a fixed count regardless of actual // row count, matching the fix already applied to Claim/ClaimDetail this session). var newMembers = dto.AssetPoolDetailArray ?? new List(); if (newMembers.Count > 0) { var detailAuto = await _autoNumber.GetNumberAsync(newMembers.Count, AUTONUMBERCONSTANT.ASSETPOOLDETAIL, login).ConfigureAwait(false); int nextDetailId = detailAuto.StartNumber; short slNo = 1; foreach (var member in newMembers) { member.AssetPoolDetailId = nextDetailId++; member.AssetPoolId = dto.AssetPoolId; member.AssetPoolDetailSlNo = slNo++; member.ValidFrom = now; member.ValidTo = null; member.Status = 1; member.CreatedById = login.UserId; member.CreatedOn = now; member.ModifiedById = login.UserId; member.ModifiedOn = now; await _dal.SaveAssetPoolDetail(member, login, tx, ct).ConfigureAwait(false); } } } else { dto.ModifiedById = login.UserId; dto.ModifiedOn = now; dto.TenantId = login.ClientId; await _dal.UpdateAssetPool(dto, login, tx, ct).ConfigureAwait(false); // Diff the submitted membership array against what's currently active in the // DB: rows the client no longer includes are closed out (VALIDTO set, never // hard-deleted); rows with AssetPoolDetailId == 0 are brand-new members. var activeDetails = (await _dal.GetActiveAssetPoolDetails(dto.AssetPoolId, login, ct).ConfigureAwait(false)).ToList(); var submitted = dto.AssetPoolDetailArray ?? new List(); var submittedActiveIds = submitted .Where(d => d.AssetPoolDetailId != 0) .Select(d => d.AssetPoolDetailId) .ToHashSet(); var toRemove = activeDetails.Where(a => !submittedActiveIds.Contains(a.AssetPoolDetailId)).ToList(); foreach (var removed in toRemove) { await _dal.CloseAssetPoolDetail(removed.AssetPoolDetailId, now, login.UserId, login, tx, ct).ConfigureAwait(false); } var toAdd = submitted.Where(d => d.AssetPoolDetailId == 0).ToList(); if (toAdd.Count > 0) { var detailAuto = await _autoNumber.GetNumberAsync(toAdd.Count, AUTONUMBERCONSTANT.ASSETPOOLDETAIL, login).ConfigureAwait(false); int nextDetailId = detailAuto.StartNumber; short slNo = (short)(activeDetails.Count > 0 ? activeDetails.Max(d => d.AssetPoolDetailSlNo) + 1 : 1); foreach (var member in toAdd) { member.AssetPoolDetailId = nextDetailId++; member.AssetPoolId = dto.AssetPoolId; member.AssetPoolDetailSlNo = slNo++; member.ValidFrom = now; member.ValidTo = null; member.Status = 1; member.CreatedById = login.UserId; member.CreatedOn = now; member.ModifiedById = login.UserId; member.ModifiedOn = now; await _dal.SaveAssetPoolDetail(member, login, tx, ct).ConfigureAwait(false); } } } await tx.CommitAsync(ct).ConfigureAwait(false); } catch { await tx.RollbackAsync(ct).ConfigureAwait(false); throw; } GB5Trace.Step("event-publish", new { EventTypeId = isNew ? EventTypeConstant.SAVEASSETPOOLEVENTTYPEID : EventTypeConstant.UPDATEASSETPOOLEVENTTYPEID }); await _eventLog.PublishEventLogAsync( isNew ? "Asset Pool Saved" : "Asset Pool Updated", dto, isNew ? EventTypeConstant.SAVEASSETPOOLEVENTTYPEID : EventTypeConstant.UPDATEASSETPOOLEVENTTYPEID, dto.AssetPoolId, login, ct: ct).ConfigureAwait(false); var cacheKey = new CacheKeyGeneration().KeyGeneration(dto.AssetPoolId, EntityConstant.OBJECTASSETPOOL, CacheKeyLevel.CLIENT_LEVEL, login); await _keyInvalidate.AllInvalidateCache(cacheKey).ConfigureAwait(false); return isNew ? $"{SuccessResponse.SaveSuccessMessage} {dto.AssetPoolId}" : SuccessResponse.UpdateSuccess; } catch (Exception ex) { GB5Trace.MarkFailed("save-assetpool-failed", ex); _logger.LogError(ex, "SaveAssetPool failed for AssetPoolId {AssetPoolId}", dto.AssetPoolId); throw; } } public async Task DeleteAssetPool(int assetPoolId, LoginDTO login, CancellationToken ct) { try { GB5Trace.Step("delete-assetpool", new { assetPoolId }); await using var tx = await _queryExecutor.BeginTransactionAsync(login).ConfigureAwait(false); try { await _dal.DeleteAssetPool(assetPoolId, login.UserId, login, tx, ct).ConfigureAwait(false); await tx.CommitAsync(ct).ConfigureAwait(false); } catch { await tx.RollbackAsync(ct).ConfigureAwait(false); throw; } GB5Trace.Step("event-publish", new { EventTypeId = EventTypeConstant.DELETEASSETPOOLEVENTTYPEID }); await _eventLog.PublishEventLogAsync( "Asset Pool Deleted", new { AssetPoolId = assetPoolId }, EventTypeConstant.DELETEASSETPOOLEVENTTYPEID, assetPoolId, login, ct: ct).ConfigureAwait(false); var cacheKey = new CacheKeyGeneration().KeyGeneration(assetPoolId, EntityConstant.OBJECTASSETPOOL, CacheKeyLevel.CLIENT_LEVEL, login); await _keyInvalidate.AllInvalidateCache(cacheKey).ConfigureAwait(false); return SuccessResponse.DeleteSuccessMessage; } catch (Exception ex) { GB5Trace.MarkFailed("delete-assetpool-failed", ex); _logger.LogError(ex, "DeleteAssetPool failed for AssetPoolId {AssetPoolId}", assetPoolId); throw; } } } }