using Distribution.Data; using Distribution.Models; using Microsoft.EntityFrameworkCore; namespace Distribution.Features.Stock; public class StockRepository(AppDBContext db) : IStockRepository { public Task GetBalanceAsync(Guid itemId, Guid locationId) => db.StockBalances .Include(b => b.Item) .FirstOrDefaultAsync(b => b.FK_ItemId == itemId && b.LocationId == locationId && b.FK_ItemBatchId == null && b.FK_ItemSerialNumberId == null); public Task GetBalanceAsync(Guid itemId, Guid locationId, Guid? itemBatchId, Guid? itemSerialNumberId) => db.StockBalances .Include(b => b.Item) .FirstOrDefaultAsync(b => b.FK_ItemId == itemId && b.LocationId == locationId && b.FK_ItemBatchId == itemBatchId && b.FK_ItemSerialNumberId == itemSerialNumberId); public Task> GetBalancesByLocationAsync(Guid locationId, LocationType locationType) => db.StockBalances .Include(b => b.Item) .Where(b => b.LocationId == locationId && b.LocationType == locationType) .ToListAsync(); public Task> GetBalancesAsync(Guid? locationId, LocationType? locationType, Guid? itemId, int page, int pageSize) { var query = db.StockBalances.Include(b => b.Item).AsQueryable(); if (locationId.HasValue) query = query.Where(b => b.LocationId == locationId.Value); if (locationType.HasValue) query = query.Where(b => b.LocationType == locationType.Value); if (itemId.HasValue) query = query.Where(b => b.FK_ItemId == itemId.Value); return query.Skip((page - 1) * pageSize).Take(pageSize).ToListAsync(); } public Task GetBalancesCountAsync(Guid? locationId, LocationType? locationType, Guid? itemId) { var query = db.StockBalances.AsQueryable(); if (locationId.HasValue) query = query.Where(b => b.LocationId == locationId.Value); if (locationType.HasValue) query = query.Where(b => b.LocationType == locationType.Value); if (itemId.HasValue) query = query.Where(b => b.FK_ItemId == itemId.Value); return query.CountAsync(); } public Task> GetBalancesByItemAsync(Guid itemId) => db.StockBalances .Include(b => b.Item) .Where(b => b.FK_ItemId == itemId) .ToListAsync(); public Task> GetLowStockAsync(Guid? locationId) { var query = db.StockBalances .Include(b => b.Item) .Where(b => b.LocationType == LocationType.Warehouse && b.Item.ReorderLevel != null && b.Quantity < b.Item.ReorderLevel); if (locationId.HasValue) query = query.Where(b => b.LocationId == locationId.Value); return query.OrderBy(b => b.Quantity).ToListAsync(); } public async Task AddBalanceAsync(StockBalance balance) => await db.StockBalances.AddAsync(balance); public void UpdateBalance(StockBalance balance) => db.StockBalances.Update(balance); public async Task AddMovementAsync(StockMovement movement) => await db.StockMovements.AddAsync(movement); public Task GetMovementByIdAsync(Guid id) => db.StockMovements.Include(m => m.Item).FirstOrDefaultAsync(m => m.Id == id); public void UpdateMovement(StockMovement movement) => db.StockMovements.Update(movement); public Task> GetMovementsAsync(Guid? locationId, Guid? itemId, MovementType? type, DateTime? from, DateTime? to, int page, int pageSize) { var query = db.StockMovements.Include(m => m.Item).AsQueryable(); if (locationId.HasValue) query = query.Where(m => m.FromLocationId == locationId.Value || m.ToLocationId == locationId.Value); if (itemId.HasValue) query = query.Where(m => m.FK_ItemId == itemId.Value); if (type.HasValue) query = query.Where(m => m.Type == type.Value); if (from.HasValue) query = query.Where(m => m.CreatedAt >= from.Value); if (to.HasValue) query = query.Where(m => m.CreatedAt <= to.Value); return query.OrderByDescending(m => m.CreatedAt).Skip((page - 1) * pageSize).Take(pageSize).ToListAsync(); } public Task GetMovementsCountAsync(Guid? locationId, Guid? itemId, MovementType? type, DateTime? from, DateTime? to) { var query = db.StockMovements.AsQueryable(); if (locationId.HasValue) query = query.Where(m => m.FromLocationId == locationId.Value || m.ToLocationId == locationId.Value); if (itemId.HasValue) query = query.Where(m => m.FK_ItemId == itemId.Value); if (type.HasValue) query = query.Where(m => m.Type == type.Value); if (from.HasValue) query = query.Where(m => m.CreatedAt >= from.Value); if (to.HasValue) query = query.Where(m => m.CreatedAt <= to.Value); return query.CountAsync(); } public Task FindBatchAsync(Guid itemId, string batchNumber) => db.ItemBatches.FirstOrDefaultAsync(b => b.FK_ItemId == itemId && b.BatchNumber == batchNumber); public async Task AddItemBatchAsync(ItemBatch batch) => await db.ItemBatches.AddAsync(batch); public Task SerialNumberExistsAsync(Guid itemId, string serialNumber) => db.ItemSerialNumbers.AnyAsync(s => s.FK_ItemId == itemId && s.SerialNumber == serialNumber); public async Task GetNextSerialSequenceAsync(Guid itemId, string prefix) { // Only serials of the form count; anything else (a supplier's own serial that // happens to start with the prefix) is ignored for numbering. var serials = await db.ItemSerialNumbers .AsNoTracking() .Where(s => s.FK_ItemId == itemId && s.SerialNumber.StartsWith(prefix)) .Select(s => s.SerialNumber) .ToListAsync(); var max = 0L; foreach (var serial in serials) { var suffix = serial[prefix.Length..]; if (suffix.Length is > 0 and <= 9 && suffix.All(char.IsDigit) && long.TryParse(suffix, out var n)) max = Math.Max(max, n); } return (int)max + 1; } public async Task AddItemSerialNumberAsync(ItemSerialNumber serial) => await db.ItemSerialNumbers.AddAsync(serial); }