66 lines
3.3 KiB
C#
66 lines
3.3 KiB
C#
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<bool> IncludesWarehouses(Guid? locationId) =>
|
|
!locationId.HasValue || await repo.IsWarehouseAsync(locationId.Value);
|
|
|
|
public async Task<StockOverview> 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<PagedResponse<StockLedgerRow>> 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<StockLedgerRow>(page, pageSize, total, mapped);
|
|
}
|
|
|
|
public async Task<PagedResponse<StockExpiryRow>> 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<StockExpiryRow>(page, pageSize, total, mapped);
|
|
}
|
|
}
|