using GB5Shared.DaprCache; using GB5Shared.DTO.Framework.Criteria; using GB5Shared.DTO.Framework.Login; using GB5Shared.GenerateAutoNumber; using GB5Shared.QueryExecutor; using GB5Shared.Resource.Response; using GB5Shared.Telemetry; using Microsoft.Extensions.Logging; using MMDAL.CustomCode.StockReservation; using MMDAL.DTO.StockReservation; using System.Text.Json; using static GB5Shared.GB5Constant.Constant; namespace MMBLL.StockReservation; public class StockReservationBLL : IStockReservationBLL { private readonly IStockReservationDAL _dal; private readonly AutoNumber _autoNumber; private readonly IQueryExecutor _queryExecutor; private readonly KeyInvalidate _keyInvalidate; private readonly ILogger _logger; private const int MaxRetries = 3; public StockReservationBLL( IStockReservationDAL dal, AutoNumber autoNumber, IQueryExecutor queryExecutor, KeyInvalidate keyInvalidate, ILogger logger) { _dal = dal; _autoNumber = autoNumber; _queryExecutor = queryExecutor; _keyInvalidate = keyInvalidate; _logger = logger; } // ── Reads ───────────────────────────────────────────────────────────── public async Task GetReservationAsync( int stockReservationId, LoginDTO loginDTO) { return await _dal.GetReservationAsync(stockReservationId, loginDTO); } public async Task GetReservationListAsync( int ouId, byte? statusFilter, int? itemId, int page, int size, LoginDTO loginDTO, CancellationToken ct) { var list = await _dal.GetReservationListAsync(ouId, statusFilter, itemId, page, size, loginDTO, ct).ConfigureAwait(false); return JsonSerializer.Serialize(list); } public async Task GetReservationsByMrpRunAsync( int mrpRunId, LoginDTO loginDTO, CancellationToken ct) { var list = await _dal.GetReservationsByMrpRunAsync(mrpRunId, loginDTO, ct).ConfigureAwait(false); return JsonSerializer.Serialize(list); } public async Task GetAvailabilityForReservationAsync( int ouId, int itemId, int skuId, LoginDTO loginDTO, CancellationToken ct) { var availability = await _dal.GetStockAvailabilityAsync(ouId, itemId, skuId, loginDTO, ct).ConfigureAwait(false); return JsonSerializer.Serialize(availability); } public async Task GetReassignmentLogAsync( int stockReservationId, LoginDTO loginDTO, CancellationToken ct) { var log = await _dal.GetReassignmentLogAsync(stockReservationId, loginDTO, ct).ConfigureAwait(false); return JsonSerializer.Serialize(log); } public async Task GetSubconTrackingReportAsync( int ouId, LoginDTO loginDTO, CancellationToken ct) { var report = await _dal.GetSubconTrackingAsync(ouId, loginDTO, ct).ConfigureAwait(false); return JsonSerializer.Serialize(report); } // ── SaveReservation ─────────────────────────────────────────────────── public async Task SaveReservationAsync( StockReservationDTO dto, LoginDTO loginDTO, CancellationToken ct) { GB5Trace.Step("validate-reservation", new { dto.OUId, dto.ItemId }); dto.TenantId = loginDTO.ClientId; // Updated validations to allow negative IDs if (dto.ForObjectHeaderId is null) throw new ArgumentException("ForObjectHeaderId is required."); if (dto.ForObjectId is null) throw new ArgumentException("ForObjectId is required."); if (dto.ItemId is null) // ✅ Changed: only check for null, not <= 0 throw new ArgumentException("ItemId is required."); if (dto.ReservedQuantity <= 0) throw new ArgumentException("ReservedQuantity must be greater than zero."); if ((ReservationMode)dto.ReservationMode == ReservationMode.Planning) throw new InvalidOperationException("Planning-mode reservations are created by AutoReserve only."); var isNew = dto.StockReservationId == 0; for (int attempt = 0; attempt < MaxRetries; attempt++) { var tx = await _queryExecutor.BeginTransactionAsync(loginDTO); try { dto.StockReservationId = await _dal.InsertReservationAsync(dto, loginDTO, tx, ct).ConfigureAwait(false); int updated = (ReservationSource)dto.ReservationSource == ReservationSource.Stock ? await _dal.IncrementStockReservedAsync( dto.OUId, dto.ItemId ?? 0, dto.SKUId ?? 0, dto.StoreId ?? 0, dto.ReservedQuantity, loginDTO, tx, ct).ConfigureAwait(false) : await _dal.DecrementPipelinePendingAsync( dto.FromAllotedAllocationId ?? 0, dto.ReservedQuantity, loginDTO, tx, ct).ConfigureAwait(false); if (updated == 0) { await _queryExecutor.RollbackAsync(tx); if (attempt < MaxRetries - 1) continue; throw new InvalidOperationException("Insufficient available quantity."); } await _queryExecutor.CommitAsync(tx); break; } catch (InvalidOperationException) { await _queryExecutor.RollbackAsync(tx); throw; } catch (Exception ex) { await _queryExecutor.RollbackAsync(tx); GB5Trace.MarkFailed("save-reservation-failed", ex); _logger.LogError(ex, "SaveReservation failed for ItemId {ItemId}", dto.ItemId); throw; } } //GB5Trace.Step("event-publish", new { EventTypeId = EventTypeConstant.SAVERESERVATIONEVENTTYPEID }); //await InvalidateReservationCacheAsync(loginDTO); return isNew ? $"{SuccessResponse.SaveSuccessMessage} {dto.StockReservationId}" : SuccessResponse.UpdateSuccess; } // ── UnreserveAsync ──────────────────────────────────────────────────── public async Task UnreserveAsync( UnreserveDTO dto, LoginDTO loginDTO, CancellationToken ct) { GB5Trace.Step("validate-unreserve", new { dto.StockReservationId }); if (dto.StockReservationId <= 0) throw new ArgumentException("StockReservationId is required."); if (string.IsNullOrWhiteSpace(dto.Reason)) throw new ArgumentException("Reason is required for unreserve."); var existing = await _dal.GetReservationDtoAsync(dto.StockReservationId, loginDTO, ct).ConfigureAwait(false) ?? throw new InvalidOperationException("Reservation not found."); bool isPartial = dto.ReleaseQuantity.HasValue; decimal releaseQty = dto.ReleaseQuantity ?? existing.BalanceQuantity; if (releaseQty <= 0) throw new ArgumentException("Release quantity must be greater than zero."); if (releaseQty > existing.BalanceQuantity) throw new InvalidOperationException("Release quantity exceeds balance (issued portion cannot be unreserved)."); GB5Trace.Step("unreserve", new { dto.StockReservationId, isPartial, releaseQty }); var tx = await _queryExecutor.BeginTransactionAsync(loginDTO); try { int updated; if (isPartial) { updated = await _dal.ReduceReservedQuantityAsync( dto.StockReservationId, dto.Version, releaseQty, loginDTO, tx, ct).ConfigureAwait(false); } else { updated = await _dal.ReleaseReservationAsync( dto.StockReservationId, dto.Version, dto.Reason, loginDTO, tx, ct).ConfigureAwait(false); } if (updated == 0) { await _queryExecutor.RollbackAsync(tx); throw new InvalidOperationException("Concurrent update detected. Please reload and try again."); } if ((ReservationSource)existing.ReservationSource == ReservationSource.Stock && existing.StoreId.HasValue) { await _dal.DecrementStockReservedAsync( existing.OUId, existing.ItemId ?? 0, // ✅ Use null-coalescing operator existing.SKUId ?? 0, existing.StoreId.Value, releaseQty, loginDTO, tx, ct).ConfigureAwait(false); } else if (existing.FromAllotedAllocationId.HasValue) // ✅ Fixed: "Alloted" { await _dal.IncrementPipelinePendingAsync( existing.FromAllotedAllocationId.Value, // ✅ Fixed: "Alloted" releaseQty, loginDTO, tx, ct).ConfigureAwait(false); } await _queryExecutor.CommitAsync(tx); } catch (InvalidOperationException) { await _queryExecutor.RollbackAsync(tx); throw; } catch (Exception ex) { await _queryExecutor.RollbackAsync(tx); GB5Trace.MarkFailed("unreserve-failed", ex); _logger.LogError(ex, "Unreserve failed for StockReservationId {Id}", dto.StockReservationId); throw; } GB5Trace.Step("event-publish", new { EventTypeId = EventTypeConstant.DELETERESERVATIONEVENTTYPEID }); await InvalidateReservationCacheAsync(loginDTO); return SuccessResponse.DeleteSuccessMessage; } // ── FreeTrackDemandAsync ────────────────────────────────────────────── public async Task FreeTrackDemandAsync( StockReservationFreeTrackDTO dto, LoginDTO loginDTO, CancellationToken ct) { GB5Trace.Step("validate-free-track", new { dto.ForObjectHeaderId, dto.ItemId }); if (dto.ForObjectHeaderId <= 0) throw new ArgumentException("ForObjectHeaderId is required."); if (dto.ItemId <= 0) throw new ArgumentException("ItemId is required."); if (string.IsNullOrWhiteSpace(dto.Reason)) throw new ArgumentException("Reason is required."); int duplicates = await _dal.CheckDuplicateActiveAsync( dto.ForObjectHeaderId, dto.ForObjectId ?? 0, dto.ItemId, dto.SKUId ?? 0, loginDTO, ct).ConfigureAwait(false); if (duplicates > 0) throw new InvalidOperationException("Unreserve the existing reservation before marking as Free-Tracked."); GB5Trace.Step("save-free-track", new { dto.ForObjectId }); var tx = await _queryExecutor.BeginTransactionAsync(loginDTO); try { var newId = (await _autoNumber.GetNumberAsync(1, AUTONUMBERCONSTANT.STOCKRESERVATION, loginDTO)).StartNumber; var reservationNumber = $"FT/{DateTime.UtcNow:yyyyMM}/{newId:D5}"; await _dal.SaveFreeTrackAsync(newId, reservationNumber, dto, loginDTO, tx, ct).ConfigureAwait(false); await _queryExecutor.CommitAsync(tx); } catch (Exception ex) { await _queryExecutor.RollbackAsync(tx); GB5Trace.MarkFailed("free-track-failed", ex); _logger.LogError(ex, "FreeTrack failed for ForObjectHeaderId {Id}", dto.ForObjectHeaderId); throw; } GB5Trace.Step("event-publish", new { EventTypeId = EventTypeConstant.SAVEFREETRACKEVNT }); await InvalidateReservationCacheAsync(loginDTO); return SuccessResponse.SaveSuccess; } // ── ReassignReservationAsync ────────────────────────────────────────── public async Task ReassignReservationAsync( StockReservationReassignDTO dto, LoginDTO loginDTO, CancellationToken ct) { GB5Trace.Step("validate-reassign", new { dto.OriginalReservationId }); if (dto.OriginalReservationId <= 0) throw new ArgumentException("OriginalReservationId is required."); if (dto.ReassignQuantity <= 0) throw new ArgumentException("ReassignQuantity must be greater than zero."); if (string.IsNullOrWhiteSpace(dto.Reason)) throw new ArgumentException("Reason is required."); if (dto.NewForObjectId is null or 0) throw new ArgumentException("New demand target is required."); var original = await _dal.GetReservationDtoAsync(dto.OriginalReservationId, loginDTO, ct).ConfigureAwait(false) ?? throw new InvalidOperationException("Original reservation not found."); if ((ReservationStatus)original.ReservationStatus != ReservationStatus.Confirmed) throw new InvalidOperationException("Only Confirmed reservations can be reassigned."); if (dto.ReassignQuantity > original.BalanceQuantity) throw new InvalidOperationException("Reassign quantity exceeds balance quantity."); if (original.ForObjectId == dto.NewForObjectId) throw new InvalidOperationException("New demand target must differ from original."); GB5Trace.Step("save-reassign", new { dto.OriginalReservationId, dto.NewForObjectId }); var tx = await _queryExecutor.BeginTransactionAsync(loginDTO); try { bool fullReassign = dto.ReassignQuantity == original.BalanceQuantity; if (fullReassign) { int updated = await _dal.MarkReassignedAsync( dto.OriginalReservationId, original.Version, loginDTO, tx, ct).ConfigureAwait(false); if (updated == 0) throw new InvalidOperationException("Concurrent update detected. Please reload and try again."); } else { int updated = await _dal.ReduceReservedQuantityAsync( dto.OriginalReservationId, original.Version, dto.ReassignQuantity, loginDTO, tx, ct).ConfigureAwait(false); if (updated == 0) throw new InvalidOperationException("Concurrent update detected. Please reload and try again."); } var newDto = CloneWithNewDemand(original, dto); newDto.StockReservationId = (await _autoNumber.GetNumberAsync(1, AUTONUMBERCONSTANT.STOCKRESERVATION, loginDTO)).StartNumber; newDto.ReservationNumber = $"SR/{DateTime.UtcNow:yyyyMM}/{newDto.StockReservationId:D5}"; newDto.ReservedQuantity = dto.ReassignQuantity; await _dal.InsertReservationAsync(newDto, loginDTO, tx, ct).ConfigureAwait(false); var logId = (await _autoNumber.GetNumberAsync(1, AUTONUMBERCONSTANT.REASSIGNMENTLOG, loginDTO)).StartNumber; var log = new ReassignmentLogDTO { ReassignmentLogId = logId, OriginalReservationId = dto.OriginalReservationId, NewReservationId = newDto.StockReservationId, ReassignedQuantity = dto.ReassignQuantity, Reason = dto.Reason }; await _dal.InsertReassignmentLogAsync(log, logId, loginDTO, tx, ct).ConfigureAwait(false); await _queryExecutor.CommitAsync(tx); } catch (InvalidOperationException) { await _queryExecutor.RollbackAsync(tx); throw; } catch (Exception ex) { await _queryExecutor.RollbackAsync(tx); GB5Trace.MarkFailed("reassign-failed", ex); _logger.LogError(ex, "Reassign failed for OriginalReservationId {Id}", dto.OriginalReservationId); throw; } GB5Trace.Step("event-publish", new { EventTypeId = EventTypeConstant.SAVEREASSIGNMENTEVENTTYPEID }); await InvalidateReservationCacheAsync(loginDTO); return SuccessResponse.SaveSuccess; } // ── AutoReserveForMrpRunAsync ───────────────────────────────────────── public async Task AutoReserveForMrpRunAsync( AutoReserveRequestDTO dto, LoginDTO loginDTO, CancellationToken ct) { GB5Trace.Step("validate-auto-reserve", new { dto.MrpRunId }); if (dto.MrpRunId <= 0) throw new ArgumentException("MrpRunId is required."); var demandLines = await _dal.GetReservationsByMrpRunAsync(dto.MrpRunId, loginDTO, ct).ConfigureAwait(false); var result = new AutoReserveResultDTO { MrpRunId = dto.MrpRunId }; var batch = new List(100); foreach (var demand in demandLines) { result.TotalDemands++; var availability = (await _dal.GetStockAvailabilityAsync( dto.OUId, demand.ItemId, 0, loginDTO, ct).ConfigureAwait(false)).ToList(); decimal remaining = demand.ReservedQuantity; foreach (var store in availability.OrderByDescending(s => s.AvailableQuantity)) { if (remaining <= 0) break; decimal reserveQty = Math.Min(remaining, store.AvailableQuantity); var newDto = new StockReservationDTO { TenantId = loginDTO.ClientId, OUId = dto.OUId, ItemId = demand.ItemId, StoreId = store.StoreId, ReservedQuantity = reserveQty, ReservationMode = (byte)dto.Mode, ReservationStatus = (byte)ReservationStatus.Tentative, ReservationSource = (byte)ReservationSource.Stock, AssignmentLevel = (byte)AssignmentLevel.Document, ForObjectId = demand.ForObjectId, MrpRunId = dto.MrpRunId }; batch.Add(newDto); remaining -= reserveQty; if (batch.Count >= 100) await FlushBatchAsync(batch, loginDTO, ct); } if (remaining <= 0) result.FullyReservedCount++; else if (remaining < demand.ReservedQuantity) { result.PartiallyReservedCount++; result.Shortages.Add(new AutoReserveShortageDTO { ItemId = demand.ItemId, ItemCode = demand.ItemCode, ItemName = demand.ItemName, ForObjectId = demand.ForObjectId, RequiredQuantity = demand.ReservedQuantity, ShortageQuantity = remaining }); } else { result.NotReservedCount++; result.Shortages.Add(new AutoReserveShortageDTO { ItemId = demand.ItemId, ItemCode = demand.ItemCode, ItemName = demand.ItemName, ForObjectId = demand.ForObjectId, RequiredQuantity = demand.ReservedQuantity, ShortageQuantity = remaining }); } } if (batch.Count > 0) await FlushBatchAsync(batch, loginDTO, ct); GB5Trace.Step("event-publish", new { EventTypeId = EventTypeConstant.SAVEAUTORESERVATIONEVENTTYPEID }); await InvalidateReservationCacheAsync(loginDTO); return JsonSerializer.Serialize(result); } // ── ConfirmTentativeReservationsAsync ───────────────────────────────── public async Task ConfirmTentativeReservationsAsync( int mrpRunId, LoginDTO loginDTO, CancellationToken ct) { GB5Trace.Step("validate-confirm-tentative", new { mrpRunId }); if (mrpRunId <= 0) throw new ArgumentException("MrpRunId is required."); GB5Trace.Step("confirm-tentative", new { mrpRunId }); var tx = await _queryExecutor.BeginTransactionAsync(loginDTO); try { await _dal.ConfirmTentativeByMrpRunAsync(mrpRunId, loginDTO, tx, ct).ConfigureAwait(false); await _queryExecutor.CommitAsync(tx); } catch (Exception ex) { await _queryExecutor.RollbackAsync(tx); GB5Trace.MarkFailed("confirm-tentative-failed", ex); _logger.LogError(ex, "ConfirmTentative failed for MrpRunId {Id}", mrpRunId); throw; } GB5Trace.Step("event-publish", new { EventTypeId = EventTypeConstant.CONFIRMRESERVATIONEVENTTYPEID }); await InvalidateReservationCacheAsync(loginDTO); return SuccessResponse.UpdateSuccess; } // ── ConfirmReservationAsync (single) ────────────────────────────────── public async Task ConfirmReservationAsync( int stockReservationId, LoginDTO loginDTO, CancellationToken ct) { GB5Trace.Step("validate-confirm-reservation", new { stockReservationId }); if (stockReservationId <= 0) throw new ArgumentException("StockReservationId is required."); var tx = await _queryExecutor.BeginTransactionAsync(loginDTO); try { int updated = await _dal.ConfirmOneTentativeAsync(stockReservationId, loginDTO, tx, ct).ConfigureAwait(false); if (updated == 0) { await _queryExecutor.RollbackAsync(tx); throw new InvalidOperationException("Reservation is not in Tentative status or does not exist."); } await _queryExecutor.CommitAsync(tx); } catch (InvalidOperationException) { await _queryExecutor.RollbackAsync(tx); throw; } catch (Exception ex) { await _queryExecutor.RollbackAsync(tx); GB5Trace.MarkFailed("confirm-reservation-failed", ex); _logger.LogError(ex, "ConfirmReservation failed for StockReservationId {Id}", stockReservationId); throw; } GB5Trace.Step("event-publish", new { EventTypeId = EventTypeConstant.CONFIRMRESERVATIONEVENTTYPEID }); await InvalidateReservationCacheAsync(loginDTO); return SuccessResponse.UpdateSuccess; } // ── MarkSubconOutAsync ──────────────────────────────────────────────── public async Task MarkSubconOutAsync( StockReservationSubconDTO dto, LoginDTO loginDTO, CancellationToken ct) { GB5Trace.Step("validate-subcon-out", new { dto.StockReservationId }); if (dto.StockReservationId <= 0) throw new ArgumentException("StockReservationId is required."); if (dto.SubconObjectHeaderId <= 0) throw new ArgumentException("SubconObjectHeaderId is required."); if (string.IsNullOrWhiteSpace(dto.SubconLocation)) throw new ArgumentException("SubconLocation is required."); GB5Trace.Step("subcon-out", new { dto.StockReservationId, dto.SubconObjectHeaderId }); var tx = await _queryExecutor.BeginTransactionAsync(loginDTO); try { int updated = await _dal.MarkSubconOutAsync(dto, loginDTO, tx, ct).ConfigureAwait(false); if (updated == 0) { await _queryExecutor.RollbackAsync(tx); throw new InvalidOperationException("Concurrent update detected or reservation not found."); } await _queryExecutor.CommitAsync(tx); } catch (InvalidOperationException) { await _queryExecutor.RollbackAsync(tx); throw; } catch (Exception ex) { await _queryExecutor.RollbackAsync(tx); GB5Trace.MarkFailed("subcon-out-failed", ex); _logger.LogError(ex, "MarkSubconOut failed for StockReservationId {Id}", dto.StockReservationId); throw; } GB5Trace.Step("event-publish", new { EventTypeId = EventTypeConstant.SUBCONOUTEVENTTYPEID }); await InvalidateReservationCacheAsync(loginDTO); return SuccessResponse.UpdateSuccess; } // ── MarkSubconReturnedAsync ─────────────────────────────────────────── public async Task MarkSubconReturnedAsync( SubconReturnDTO dto, LoginDTO loginDTO, CancellationToken ct) { GB5Trace.Step("validate-subcon-return", new { dto.StockReservationId }); if (dto.StockReservationId <= 0) throw new ArgumentException("StockReservationId is required."); if (dto.ReturnedQuantity <= 0) throw new ArgumentException("ReturnedQuantity must be greater than zero."); GB5Trace.Step("subcon-return", new { dto.StockReservationId, dto.ReturnedQuantity }); var tx = await _queryExecutor.BeginTransactionAsync(loginDTO); try { int updated = await _dal.MarkSubconReturnedAsync( dto.StockReservationId, dto.Version, dto.ReturnedQuantity, loginDTO, tx, ct).ConfigureAwait(false); if (updated == 0) { await _queryExecutor.RollbackAsync(tx); throw new InvalidOperationException("Reservation is not in SubcontractingOut status or concurrent update occurred."); } await _queryExecutor.CommitAsync(tx); } catch (InvalidOperationException) { await _queryExecutor.RollbackAsync(tx); throw; } catch (Exception ex) { await _queryExecutor.RollbackAsync(tx); GB5Trace.MarkFailed("subcon-return-failed", ex); _logger.LogError(ex, "MarkSubconReturned failed for StockReservationId {Id}", dto.StockReservationId); throw; } GB5Trace.Step("event-publish", new { EventTypeId = EventTypeConstant.SUBCONRETURNEVENTTYPEID }); await InvalidateReservationCacheAsync(loginDTO); return SuccessResponse.UpdateSuccess; } // ── Helpers ─────────────────────────────────────────────────────────── private async Task FlushBatchAsync( List batch, LoginDTO loginDTO, CancellationToken ct) { foreach (var dto in batch) dto.StockReservationId = (await _autoNumber.GetNumberAsync(1, AUTONUMBERCONSTANT.STOCKRESERVATION, loginDTO)).StartNumber; var tx = await _queryExecutor.BeginTransactionAsync(loginDTO); try { await _dal.BulkInsertReservationsAsync(batch, loginDTO, tx, ct).ConfigureAwait(false); await _queryExecutor.CommitAsync(tx); } catch (Exception ex) { await _queryExecutor.RollbackAsync(tx); GB5Trace.MarkFailed("auto-reserve-batch-failed", ex); _logger.LogError(ex, "AutoReserve batch flush failed"); throw; } batch.Clear(); } private static StockReservationDTO CloneWithNewDemand( StockReservationDTO original, StockReservationReassignDTO reassign) { return new StockReservationDTO { TenantId = original.TenantId, OUId = original.OUId, PeriodId = original.PeriodId, ReservationMode = (byte)((ReservationMode)original.ReservationMode == ReservationMode.Planning ? ReservationMode.PostPlanning : (ReservationMode)original.ReservationMode), ReservationStatus = (byte)ReservationStatus.Confirmed, ReservationSource = original.ReservationSource, AssignmentLevel = original.AssignmentLevel, ForObjectHeaderTypeId = reassign.NewForObjectHeaderTypeId, ForObjectHeaderId = reassign.NewForObjectHeaderId, ForObjectTypeId = reassign.NewForObjectTypeId, ForObjectId = reassign.NewForObjectId, ForAllotedAllocationId = reassign.NewForAllotedAllocationId, ForBizTransactionTypeId = reassign.NewForBizTransactionTypeId, ForDate = reassign.NewForDate, ForSlNo = reassign.NewForSlNo, ItemId = original.ItemId, SKUId = original.SKUId, LotId = original.LotId, PackId = original.PackId, StoreId = original.StoreId, FromObjectHeaderTypeId = original.FromObjectHeaderTypeId, FromObjectHeaderId = original.FromObjectHeaderId, FromObjectTypeId = original.FromObjectTypeId, FromObjectId = original.FromObjectId, FromAllotedAllocationId = original.FromAllotedAllocationId, FromBizTransactionTypeId = original.FromBizTransactionTypeId, IsLotLocked = original.IsLotLocked, IsAutoLinked = original.IsAutoLinked }; } private async Task InvalidateReservationCacheAsync(LoginDTO loginDTO) { var cacheKey = $"StockReservation:{loginDTO.ClientId}"; await _keyInvalidate.AllInvalidateCache(cacheKey); } public async Task GetSelectListStockReservation( int firstNumber, int maxResult, CriteriaDTO criteriaDTO, LoginDTO loginDTO) { try { return await _dal.GetSelectListStockReservation( firstNumber, maxResult, criteriaDTO, loginDTO); } catch (Exception ex) { _logger.LogError(ex, "GetSelectListStockReservation failed"); throw; } } }