using GB5Shared.DTO.Framework.Login; using GB5Shared.GenerateAutoNumber; using GB5Shared.QueryExecutor; using GB5Shared.Resource.Response; using MMDAL.CustomCode.PartLevel; using MMDAL.DTO.PartLevel; using MMDAL.Query.PartLevel; using static GB5Shared.GB5Constant.Constant; namespace MMBLL.PartLevel { public class PartAllocationBLL : IPartAllocationBLL { private readonly IPartAllocationDAL _PartAllocationDAL; private readonly IPartDetailDAL _PartDetailDAL; private readonly IPartPositionDAL _PartPositionDAL; private readonly AutoNumber _AutoNumber; private readonly IQueryExecutor _QueryExecutor; private const int MaxRetries = 3; public PartAllocationBLL( IPartAllocationDAL partAllocationDAL, IPartDetailDAL partDetailDAL, IPartPositionDAL partPositionDAL, AutoNumber autoNumber, IQueryExecutor queryExecutor) { _PartAllocationDAL = partAllocationDAL; _PartDetailDAL = partDetailDAL; _PartPositionDAL = partPositionDAL; _AutoNumber = autoNumber; _QueryExecutor = queryExecutor; } public async Task> GetPartAllocationList( int partDetailId, LoginDTO loginDTO, CancellationToken ct) => await _PartAllocationDAL.GetPartAllocationList(partDetailId, loginDTO, ct).ConfigureAwait(false); // Manual allocate: reserves qty from TPARTPOSITION; inserts TPARTALLOCATION row. public async Task AllocateParts(AllocatePartsDTO dto, LoginDTO loginDTO, CancellationToken ct) { if (dto.PartDetailId <= 0) throw new ArgumentException("PartDetailId is required."); if (dto.Quantity <= 0) throw new ArgumentException("Quantity must be > 0."); var position = await _PartPositionDAL .GetPartPositionByKey( await GetItemIdFromDetail(dto.PartDetailId, loginDTO, ct).ConfigureAwait(false), null, 0, dto.LotId, loginDTO, ct) .ConfigureAwait(false); if (position is null || position.Available < dto.Quantity) throw new InvalidOperationException("Insufficient available stock for this allocation."); var Trans = await _QueryExecutor.BeginTransactionAsync(loginDTO); try { var auto = await _AutoNumber.GetNumberAsync(1, AUTONUMBERCONSTANT.PARTALLOCATION, loginDTO); var alloc = new PartAllocationDTO { PartAllocationId = auto.StartNumber, PartDetailId = dto.PartDetailId, LotId = dto.LotId, LotDetailId = dto.LotDetailId, AllocatedQty = dto.Quantity, TenantId = loginDTO.ClientId }; await _PartAllocationDAL.InsertPartAllocation(alloc, loginDTO, Trans, ct).ConfigureAwait(false); int updated = await _PartPositionDAL .UpdateAllocate(position.PartPositionId, dto.Quantity, loginDTO, Trans, ct) .ConfigureAwait(false); if (updated == 0) throw new InvalidOperationException("Concurrent update detected: stock position changed. Please retry."); await _QueryExecutor.CommitAsync(Trans); return SuccessResponse.SaveSuccess; } catch (Exception) { await _QueryExecutor.RollbackAsync(Trans); throw; } } public async Task IssueParts(IssuePartsDTO dto, LoginDTO loginDTO, CancellationToken ct) { if (dto.PartAllocationId <= 0) throw new ArgumentException("PartAllocationId is required."); if (dto.Quantity <= 0) throw new ArgumentException("Quantity must be > 0."); var alloc = await _PartAllocationDAL.GetPartAllocation(dto.PartAllocationId, loginDTO, ct).ConfigureAwait(false) ?? throw new InvalidOperationException("Part allocation not found."); var position = await GetPositionForAllocation(alloc, loginDTO, ct).ConfigureAwait(false); var Trans = await _QueryExecutor.BeginTransactionAsync(loginDTO); try { await _PartAllocationDAL.UpdateIssued(dto.PartAllocationId, dto.Quantity, loginDTO, Trans, ct).ConfigureAwait(false); int updated = await _PartPositionDAL .UpdateIssue(position.PartPositionId, dto.Quantity, loginDTO, Trans, ct) .ConfigureAwait(false); if (updated == 0) throw new InvalidOperationException("Concurrent update detected. Please retry."); await _QueryExecutor.CommitAsync(Trans); return SuccessResponse.UpdateSuccess; } catch (Exception) { await _QueryExecutor.RollbackAsync(Trans); throw; } } public async Task ConsumeParts(ConsumePartsDTO dto, LoginDTO loginDTO, CancellationToken ct) { if (dto.PartAllocationId <= 0) throw new ArgumentException("PartAllocationId is required."); if (dto.Quantity <= 0) throw new ArgumentException("Quantity must be > 0."); var alloc = await _PartAllocationDAL.GetPartAllocation(dto.PartAllocationId, loginDTO, ct).ConfigureAwait(false) ?? throw new InvalidOperationException("Part allocation not found."); var position = await GetPositionForAllocation(alloc, loginDTO, ct).ConfigureAwait(false); var Trans = await _QueryExecutor.BeginTransactionAsync(loginDTO); try { await _PartAllocationDAL.UpdateConsumed(dto.PartAllocationId, dto.Quantity, loginDTO, Trans, ct).ConfigureAwait(false); int updated = await _PartPositionDAL .UpdateConsume(position.PartPositionId, dto.Quantity, loginDTO, Trans, ct) .ConfigureAwait(false); if (updated == 0) throw new InvalidOperationException("Concurrent update detected. Please retry."); await _QueryExecutor.CommitAsync(Trans); return SuccessResponse.UpdateSuccess; } catch (Exception) { await _QueryExecutor.RollbackAsync(Trans); throw; } } public async Task ReleaseParts(ReleasePartsDTO dto, LoginDTO loginDTO, CancellationToken ct) { if (dto.PartAllocationId <= 0) throw new ArgumentException("PartAllocationId is required."); var alloc = await _PartAllocationDAL.GetPartAllocation(dto.PartAllocationId, loginDTO, ct).ConfigureAwait(false) ?? throw new InvalidOperationException("Part allocation not found."); var position = await GetPositionForAllocation(alloc, loginDTO, ct).ConfigureAwait(false); decimal remainingReserved = alloc.AllocatedQty - alloc.IssuedQty; var Trans = await _QueryExecutor.BeginTransactionAsync(loginDTO); try { await _PartAllocationDAL.UpdateReleased(dto.PartAllocationId, loginDTO, Trans, ct).ConfigureAwait(false); if (remainingReserved > 0) { int updated = await _PartPositionDAL .UpdateRelease(position.PartPositionId, remainingReserved, loginDTO, Trans, ct) .ConfigureAwait(false); if (updated == 0) throw new InvalidOperationException("Concurrent update detected. Please retry."); } await _QueryExecutor.CommitAsync(Trans); return SuccessResponse.DeleteSuccessMessage; } catch (Exception) { await _QueryExecutor.RollbackAsync(Trans); throw; } } // Priority-aware batch allocation: // Loads all open TPARTDETAIL sorted by PRIORITY ASC, then PLANNEDDUEDATE ASC. // For each detail, finds available TPARTPOSITION stock and allocates MIN(remaining, available). // Priority=1 demand is served first; partial allocation is allowed; lower priority is never // served ahead of higher priority even if lower priority could be fully satisfied. public async Task AutoAllocateByPriority( AllocationScopeDTO scope, LoginDTO loginDTO, CancellationToken ct) { var (sql, p) = PartDetailQB.BuildOpenDetailsPrioritized(scope); var openDetails = await _QueryExecutor .QueryAsync(loginDTO, sql, p, cancellationToken: ct) .ConfigureAwait(false) ?? []; var result = new AllocationResultDTO(); foreach (var detail in openDetails) { ct.ThrowIfCancellationRequested(); var position = await _PartPositionDAL .GetPartPositionByKey(detail.ItemId, detail.PartDimId, 0, null, loginDTO, ct) .ConfigureAwait(false); if (position is null || position.Available <= 0) { result.TotalShortages++; result.TotalShortageQty += detail.RemainingQty; result.Shortages.Add(new PartShortageLineDTO { PartDetailId = detail.PartDetailId, ItemId = detail.ItemId, ItemCode = detail.ItemCode, RequiredQty = detail.RemainingQty, AvailableQty = 0, ShortageQty = detail.RemainingQty, Priority = detail.Priority }); continue; } decimal toAllocate = Math.Min(detail.RemainingQty, position.Available); // Retry loop to handle ROWVERSION concurrency conflicts. bool success = false; for (int attempt = 0; attempt < MaxRetries && !success; attempt++) { var Trans = await _QueryExecutor.BeginTransactionAsync(loginDTO); try { var auto = await _AutoNumber.GetNumberAsync(1, AUTONUMBERCONSTANT.PARTALLOCATION, loginDTO); var alloc = new PartAllocationDTO { PartAllocationId = auto.StartNumber, PartDetailId = detail.PartDetailId, AllocatedQty = toAllocate, TenantId = loginDTO.ClientId }; await _PartAllocationDAL.InsertPartAllocation(alloc, loginDTO, Trans, ct).ConfigureAwait(false); int updated = await _PartPositionDAL .UpdateAllocate(position.PartPositionId, toAllocate, loginDTO, Trans, ct) .ConfigureAwait(false); if (updated == 0) { await _QueryExecutor.RollbackAsync(Trans); // refresh position and recalculate position = await _PartPositionDAL .GetPartPositionByKey(detail.ItemId, detail.PartDimId, 0, null, loginDTO, ct) .ConfigureAwait(false); if (position is null || position.Available <= 0) break; toAllocate = Math.Min(detail.RemainingQty, position.Available); continue; } await _QueryExecutor.CommitAsync(Trans); success = true; } catch (Exception) { await _QueryExecutor.RollbackAsync(Trans); throw; } } if (success) { result.TotalAllocatedQty += toAllocate; if (toAllocate < detail.RemainingQty) { result.TotalPartiallyAllocated++; result.TotalShortages++; result.TotalShortageQty += detail.RemainingQty - toAllocate; result.Shortages.Add(new PartShortageLineDTO { PartDetailId = detail.PartDetailId, ItemId = detail.ItemId, ItemCode = detail.ItemCode, RequiredQty = detail.RemainingQty, AvailableQty = toAllocate, ShortageQty = detail.RemainingQty - toAllocate, Priority = detail.Priority }); } else { result.TotalAllocated++; } } } return result; } private async Task GetItemIdFromDetail(int partDetailId, LoginDTO loginDTO, CancellationToken ct) { var detail = await _PartDetailDAL.GetPartDetail(partDetailId, loginDTO, ct).ConfigureAwait(false) ?? throw new InvalidOperationException($"PartDetail {partDetailId} not found."); return detail.ItemId; } private async Task GetPositionForAllocation( PartAllocationDTO alloc, LoginDTO loginDTO, CancellationToken ct) { var detail = await _PartDetailDAL.GetPartDetail(alloc.PartDetailId, loginDTO, ct).ConfigureAwait(false) ?? throw new InvalidOperationException($"PartDetail {alloc.PartDetailId} not found."); return await _PartPositionDAL .GetPartPositionByKey(detail.ItemId, detail.PartDimId, 0, alloc.LotId, loginDTO, ct) .ConfigureAwait(false) ?? throw new InvalidOperationException("Part position record not found for this allocation."); } } }