All checks were successful
CI/CD Pipeline / Build and Deploy with Docker Compose (push) Successful in 9m18s
329 lines
14 KiB
C#
329 lines
14 KiB
C#
using Domain.Dtos.Stock; // StockItemScanResultDto
|
|
using Domain.Generics; // PagedResult<T>
|
|
using Microsoft.EntityFrameworkCore;
|
|
using Models.Helpers; // ToPagedResultAsync
|
|
using Models.Interfaces; // IPhLSMStockItemRepository
|
|
using Models.Models; // PhronCareOperationsHubContext
|
|
|
|
namespace Models.Repositories.Stock
|
|
{
|
|
public class PhLSMStockItemRepository(PhronCareOperationsHubContext context) : IPhLSMStockItemRepository
|
|
{
|
|
private readonly PhronCareOperationsHubContext _context = context;
|
|
|
|
public async Task<PagedResult<StockItemScanResultDto>> SearchStockItemsAsync(
|
|
string? codeOrText,
|
|
string? batch,
|
|
int? locationId,
|
|
int? productType,
|
|
int? traceabilityType,
|
|
bool? plusProcess,
|
|
int page,
|
|
int take)
|
|
{
|
|
// Base: stock disponible (>0) + joins necesarios
|
|
var baseQuery =
|
|
from si in _context.PhLsmStockItems.AsNoTracking()
|
|
join p in _context.PhLsmProducts.AsNoTracking() on si.ProductId equals p.Id
|
|
join l in _context.PhLsmStockLocations.AsNoTracking() on si.LocationId equals l.Id into _loc
|
|
from loc in _loc.DefaultIfEmpty()
|
|
where si.Quantity > 0
|
|
select new { si, p, loc };
|
|
|
|
// ---- Filtros (case-insensitive donde aplica) ----
|
|
if (!string.IsNullOrWhiteSpace(codeOrText))
|
|
{
|
|
var t = codeOrText.Trim().ToLower();
|
|
baseQuery = baseQuery.Where(x =>
|
|
(!string.IsNullOrEmpty(x.p.FactoryCode) && x.p.FactoryCode.ToLower().Contains(t)) ||
|
|
(!string.IsNullOrEmpty(x.p.ExternalCode) && x.p.ExternalCode.ToLower().Contains(t)) ||
|
|
(!string.IsNullOrEmpty(x.p.Name) && x.p.Name.ToLower().Contains(t)) ||
|
|
(!string.IsNullOrEmpty(x.p.Descripcion) && x.p.Descripcion.ToLower().Contains(t)) ||
|
|
(!string.IsNullOrEmpty(x.si.Batch) && x.si.Batch.ToLower().Contains(t))
|
|
);
|
|
}
|
|
|
|
if (!string.IsNullOrWhiteSpace(batch))
|
|
{
|
|
var b = batch.Trim().ToLower();
|
|
baseQuery = baseQuery.Where(x => x.si.Batch != null && x.si.Batch.ToLower().Contains(b));
|
|
}
|
|
|
|
if (locationId.HasValue)
|
|
baseQuery = baseQuery.Where(x => x.si.LocationId == locationId.Value);
|
|
|
|
if (productType.HasValue)
|
|
baseQuery = baseQuery.Where(x => x.p.ProductType == productType.Value);
|
|
|
|
if (traceabilityType.HasValue)
|
|
baseQuery = baseQuery.Where(x => x.p.TraceabilityType == traceabilityType.Value);
|
|
|
|
if (plusProcess.HasValue)
|
|
baseQuery = baseQuery.Where(x => x.p.PlusProcess == plusProcess.Value);
|
|
|
|
// Orden lógico
|
|
baseQuery = baseQuery
|
|
.OrderBy(x => x.si.Expiration)
|
|
.ThenBy(x => x.p.Name);
|
|
|
|
// Proyección final a DTO (IQueryable<StockItemScanResultDto>)
|
|
var dtoQuery = baseQuery.Select(x => new StockItemScanResultDto
|
|
{
|
|
StockItemId = x.si.Id,
|
|
ProductId = x.p.Id,
|
|
FactoryCode = x.p.FactoryCode ?? string.Empty,
|
|
ExternalCode = x.p.ExternalCode,
|
|
ProductName = x.p.Name ?? string.Empty,
|
|
Description = x.p.Descripcion,
|
|
LocationId = x.si.LocationId,
|
|
LocationName = x.loc != null ? x.loc.Descripcion : null,
|
|
Batch = x.si.Batch,
|
|
Expiration = x.si.Expiration,
|
|
TraceabilityType = x.p.TraceabilityType,
|
|
AvailableQty = x.si.Quantity,
|
|
PlusProcess = x.p.PlusProcess
|
|
});
|
|
|
|
page = page <= 0 ? 1 : page;
|
|
take = take <= 0 ? 20 : take;
|
|
|
|
// Paginado con tu helper (manteniendo el mismo patrón que ProductRepository)
|
|
var paged = await dtoQuery.ToPagedResultAsync(page, take);
|
|
|
|
return new PagedResult<StockItemScanResultDto>
|
|
{
|
|
Items = paged.Items,
|
|
TotalItems = paged.TotalItems,
|
|
Page = paged.Page,
|
|
PageSize = paged.PageSize
|
|
};
|
|
}
|
|
|
|
// -------- Búsqueda PARSEADA (GS1/DataMatrix) ----------
|
|
public async Task<PagedResult<StockItemScanResultDto>> SearchStockItemsParsedAsync(
|
|
string? gtin,
|
|
string? batch,
|
|
DateOnly? expiration,
|
|
string? serial,
|
|
int? locationId,
|
|
int page,
|
|
int take)
|
|
{
|
|
// 0) Sin ningún dato parseado -> vacío (evitar traer todo)
|
|
if (string.IsNullOrWhiteSpace(gtin) &&
|
|
string.IsNullOrWhiteSpace(batch) &&
|
|
!expiration.HasValue &&
|
|
string.IsNullOrWhiteSpace(serial))
|
|
{
|
|
return new PagedResult<StockItemScanResultDto>
|
|
{ Items = [], TotalItems = 0, Page = page <= 0 ? 1 : page, PageSize = take <= 0 ? 20 : take };
|
|
}
|
|
|
|
// 1) Resolver productos por GTIN/Factory/Regulatory si vino GTIN
|
|
var productIds = new List<int>();
|
|
if (!string.IsNullOrWhiteSpace(gtin))
|
|
{
|
|
var g = gtin.Trim();
|
|
productIds = await _context.PhLsmProducts.AsNoTracking()
|
|
.Where(p => p.ExternalCode == g || p.FactoryCode == g || p.RegulatoryCode == g)
|
|
.Select(p => p.Id).ToListAsync();
|
|
|
|
if (productIds.Count == 0)
|
|
{
|
|
return new PagedResult<StockItemScanResultDto>
|
|
{ Items = [], TotalItems = 0, Page = page <= 0 ? 1 : page, PageSize = take <= 0 ? 20 : take };
|
|
}
|
|
}
|
|
|
|
// 2) Base query (stock disponible)
|
|
var baseQuery =
|
|
from si in _context.PhLsmStockItems.AsNoTracking()
|
|
join p in _context.PhLsmProducts.AsNoTracking() on si.ProductId equals p.Id
|
|
join l in _context.PhLsmStockLocations.AsNoTracking() on si.LocationId equals l.Id into _loc
|
|
from loc in _loc.DefaultIfEmpty()
|
|
where si.Quantity > 0
|
|
select new { si, p, loc };
|
|
|
|
if (productIds.Count > 0)
|
|
baseQuery = baseQuery.Where(x => productIds.Contains(x.p.Id));
|
|
|
|
if (locationId.HasValue)
|
|
baseQuery = baseQuery.Where(x => x.si.LocationId == locationId.Value);
|
|
|
|
// 3) Reglas por tipo de trazabilidad — PRIORIDAD: tipo REAL del producto si hay productIds
|
|
if (productIds.Count > 0)
|
|
{
|
|
var traces = await _context.PhLsmProducts.AsNoTracking()
|
|
.Where(p => productIds.Contains(p.Id))
|
|
.Select(p => p.TraceabilityType)
|
|
.Distinct()
|
|
.ToListAsync();
|
|
|
|
// Nota: normalmente habrá un solo tipo; si hubiera mezcla, estas ramas las contemplan.
|
|
|
|
if (traces.Contains(4) || traces.Contains(5))
|
|
{
|
|
// T4/T5: requieren serial. T5 además requiere expiration exacta.
|
|
if (string.IsNullOrWhiteSpace(serial))
|
|
return new PagedResult<StockItemScanResultDto>
|
|
{ Items = [], TotalItems = 0, Page = page <= 0 ? 1 : page, PageSize = take <= 0 ? 20 : take };
|
|
|
|
var s = serial.Trim();
|
|
|
|
if (traces.Contains(5) && expiration.HasValue)
|
|
{
|
|
var e = expiration.Value;
|
|
baseQuery = baseQuery.Where(x =>
|
|
(x.p.TraceabilityType == 5 &&
|
|
x.si.Serial != null && x.si.Serial == s &&
|
|
x.si.Expiration.HasValue && x.si.Expiration.Value == e)
|
|
||
|
|
// si el conjunto tuviera también productos T4, permitimos T4 por serial
|
|
(x.p.TraceabilityType == 4 &&
|
|
x.si.Serial != null && x.si.Serial == s)
|
|
);
|
|
}
|
|
else
|
|
{
|
|
baseQuery = baseQuery.Where(x =>
|
|
x.p.TraceabilityType == 4 &&
|
|
x.si.Serial != null && x.si.Serial == s
|
|
);
|
|
}
|
|
}
|
|
else if (traces.Contains(3))
|
|
{
|
|
// T3: exige batch + expiration exactos
|
|
if (string.IsNullOrWhiteSpace(batch) || !expiration.HasValue)
|
|
return new PagedResult<StockItemScanResultDto>
|
|
{ Items = [], TotalItems = 0, Page = page <= 0 ? 1 : page, PageSize = take <= 0 ? 20 : take };
|
|
|
|
var b = batch.Trim();
|
|
var e = expiration.Value;
|
|
|
|
baseQuery = baseQuery.Where(x =>
|
|
x.p.TraceabilityType == 3 &&
|
|
x.si.Batch == b &&
|
|
x.si.Expiration.HasValue && x.si.Expiration.Value == e
|
|
);
|
|
}
|
|
else if (traces.Contains(2))
|
|
{
|
|
// T2: exige batch exacto (ignorar serial/expiration si vinieran)
|
|
if (string.IsNullOrWhiteSpace(batch))
|
|
return new PagedResult<StockItemScanResultDto>
|
|
{ Items = [], TotalItems = 0, Page = page <= 0 ? 1 : page, PageSize = take <= 0 ? 20 : take };
|
|
|
|
var b = batch.Trim();
|
|
|
|
baseQuery = baseQuery.Where(x =>
|
|
x.p.TraceabilityType == 2 &&
|
|
x.si.Batch == b
|
|
);
|
|
}
|
|
else if (traces.Contains(1))
|
|
{
|
|
// T1: sin traza (solo producto + ubicación)
|
|
baseQuery = baseQuery.Where(x => x.p.TraceabilityType == 1);
|
|
}
|
|
else
|
|
{
|
|
return new PagedResult<StockItemScanResultDto>
|
|
{ Items = [], TotalItems = 0, Page = page <= 0 ? 1 : page, PageSize = take <= 0 ? 20 : take };
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// Fallback: sin productIds -> decidir por presencia de datos (tu heurística original)
|
|
if (!string.IsNullOrWhiteSpace(serial))
|
|
{
|
|
var s = serial.Trim();
|
|
|
|
if (expiration.HasValue)
|
|
{
|
|
var e = expiration.Value;
|
|
baseQuery = baseQuery.Where(x =>
|
|
(x.p.TraceabilityType == 5 &&
|
|
x.si.Serial != null && x.si.Serial == s &&
|
|
x.si.Expiration.HasValue && x.si.Expiration.Value == e)
|
|
||
|
|
(x.p.TraceabilityType == 4 &&
|
|
x.si.Serial != null && x.si.Serial == s)
|
|
);
|
|
}
|
|
else
|
|
{
|
|
baseQuery = baseQuery.Where(x =>
|
|
x.p.TraceabilityType == 4 &&
|
|
x.si.Serial != null && x.si.Serial == s
|
|
);
|
|
}
|
|
}
|
|
else if (!string.IsNullOrWhiteSpace(batch))
|
|
{
|
|
var b = batch.Trim();
|
|
|
|
if (expiration.HasValue)
|
|
{
|
|
var e = expiration.Value;
|
|
baseQuery = baseQuery.Where(x =>
|
|
(x.p.TraceabilityType == 3 &&
|
|
x.si.Batch == b &&
|
|
x.si.Expiration.HasValue && x.si.Expiration.Value == e)
|
|
||
|
|
(x.p.TraceabilityType == 2 &&
|
|
x.si.Batch == b)
|
|
);
|
|
}
|
|
else
|
|
{
|
|
baseQuery = baseQuery.Where(x =>
|
|
x.p.TraceabilityType == 2 &&
|
|
x.si.Batch == b
|
|
);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// Solo aceptar T1 cuando no hay otra evidencia
|
|
baseQuery = baseQuery.Where(x => x.p.TraceabilityType == 1);
|
|
}
|
|
}
|
|
|
|
// 4) Orden y proyección
|
|
baseQuery = baseQuery.OrderBy(x => x.si.Expiration).ThenBy(x => x.p.Name);
|
|
|
|
var dtoQuery = baseQuery.Select(x => new StockItemScanResultDto
|
|
{
|
|
StockItemId = x.si.Id,
|
|
ProductId = x.p.Id,
|
|
FactoryCode = x.p.FactoryCode ?? string.Empty,
|
|
ExternalCode = x.p.ExternalCode,
|
|
ProductName = x.p.Name ?? string.Empty,
|
|
Description = x.p.Descripcion,
|
|
LocationId = x.si.LocationId,
|
|
LocationName = x.loc != null ? x.loc.Descripcion : null,
|
|
Batch = x.si.Batch,
|
|
Expiration = x.si.Expiration,
|
|
Serial = x.si.Serial,
|
|
TraceabilityType = x.p.TraceabilityType,
|
|
AvailableQty = x.si.Quantity,
|
|
PlusProcess = x.p.PlusProcess
|
|
});
|
|
|
|
page = page <= 0 ? 1 : page;
|
|
take = take <= 0 ? 20 : take;
|
|
|
|
var paged = await dtoQuery.ToPagedResultAsync(page, take);
|
|
|
|
return new PagedResult<StockItemScanResultDto>
|
|
{
|
|
Items = paged.Items,
|
|
TotalItems = paged.TotalItems,
|
|
Page = paged.Page,
|
|
PageSize = paged.PageSize
|
|
};
|
|
}
|
|
}
|
|
}
|