using Distribution.Features.Stock; namespace Distribution.Features.StockReport; public class StockReportService(IStockReportRepository repo) { public const int MaxRangeDays = 366; public const int MaxExpiryDays = 3650; public static string? Validate(StockReportFilter f, int page, int pageSize) { if (page < 1 || pageSize < 1 || pageSize > 200) return "page must be >= 1 and pageSize between 1 and 200."; if (f.To < f.From) return "'to' must not be before 'from'."; if ((f.To - f.From).TotalDays > MaxRangeDays) return $"Date range cannot exceed {MaxRangeDays} days."; if (f.Status is not (null or "low" or "out")) return "status must be 'low' or 'out'."; return null; } public static string? ValidateExpiry(int days, int page, int pageSize) { if (page < 1 || pageSize < 1 || pageSize > 200) return "page must be >= 1 and pageSize between 1 and 200."; if (days < 0 || days > MaxExpiryDays) return $"days must be between 0 and {MaxExpiryDays}."; return null; } // Low-stock is judged on warehouse quantity, so it only applies when the scope includes warehouses. private async Task IncludesWarehouses(Guid? locationId) => !locationId.HasValue || await repo.IsWarehouseAsync(locationId.Value); public async Task GetOverviewAsync(Guid? locationId, int expiryDays) { var agg = await repo.GetOverviewAsync(locationId, await IncludesWarehouses(locationId), DateTime.Now.Date, expiryDays); return new StockOverview(agg.InStockItems, agg.TotalQty, agg.TotalValue, agg.LowStockItems, agg.ActiveItems - agg.InStockItems, agg.ExpiringSoonBatches, agg.ExpiredBatches); } public async Task> GetLedgerAsync(StockReportFilter f, int page, int pageSize) { var includeLow = await IncludesWarehouses(f.LocationId); var (rows, total) = await repo.GetLedgerAsync(f, includeLow, page, pageSize); var mapped = rows.Select(r => { var status = r.Current <= 0 ? StockStatus.Out : includeLow && r.ReorderLevel != null && r.WarehouseQty <= r.ReorderLevel ? StockStatus.Low : StockStatus.Ok; return new StockLedgerRow(r.ItemId, r.Sku, r.Name, r.Brand, r.Category, r.Uom, r.Opening, r.In, r.Out, r.Opening + r.In - r.Out, r.Current, r.CostPrice, r.Current * (r.CostPrice ?? 0m), r.ReorderLevel, status); }).ToList(); return new PagedResponse(page, pageSize, total, mapped); } public async Task> GetExpiryAsync( Guid? locationId, string? search, int days, int page, int pageSize) { var today = DateTime.Now.Date; var (rows, total) = await repo.GetExpiryAsync(locationId, search, today.AddDays(days + 1), page, pageSize); var mapped = rows.Select(r => new StockExpiryRow( r.ItemId, r.Sku, r.Name, r.BatchId, r.BatchNumber, r.ExpiryDate, (int)(r.ExpiryDate.Date - today).TotalDays, r.Quantity, r.CostPrice, r.Quantity * (r.CostPrice ?? 0m), r.LocationId, r.LocationType, r.LocationName)).ToList(); return new PagedResponse(page, pageSize, total, mapped); } }