phronCare/Core/Services/Stock/ExpeditionService.cs
leandro 6419ac8843
All checks were successful
CI/CD Pipeline / Build and Deploy with Docker Compose (pull_request) Successful in 34m41s
feat(expeditions): permitir transición Emitida → EnTransito desde la consulta
closes #7
2026-03-14 21:23:06 -03:00

232 lines
8.7 KiB
C#

using Core.Interfaces.Stock;
using Domain.Constants;
using Domain.Dtos.Stock;
using Domain.Entities;
using Domain.Generics;
using Models.Interfaces;
using System.Reflection;
using Transversal.Services;
namespace Core.Services.Stock
{
public class ExpeditionService : IExpeditionDom
{
#region Declaraciones
private readonly IExpeditionRepository _repo;
private readonly IPhLSMStockItemRepository _stockItemRepository;
public ExpeditionService(
IExpeditionRepository repo,
IPhLSMStockItemRepository stockItemRepository)
{
_repo = repo;
_stockItemRepository = stockItemRepository;
}
#endregion
#region Guardado completo de expedicion (encabezado + detalles)
public async Task<(int Id, string ExpeditionNumber)> CreateAndIssueAsync(
ELSExpeditionHeader header,
IEnumerable<ELSExpeditionDetail> details,
int formSeriesId)
{
if (header is null)
throw new ArgumentNullException(nameof(header));
if (details is null || !details.Any())
throw new InvalidOperationException("Debe incluir al menos un ítem.");
if (formSeriesId <= 0)
throw new ArgumentOutOfRangeException(nameof(formSeriesId), "Serie inválida.");
var detailList = details.ToList();
ValidateNoDuplicateStockItems(detailList);
await ValidateSerializedConflictsAsync(detailList);
await ValidateStockAvailabilityAsync(detailList);
header.PhLsmExpeditionDetails = detailList;
return await _repo.CreateFullExpeditionAsync(header, formSeriesId);
}
private static void ValidateNoDuplicateStockItems(List<ELSExpeditionDetail> detailList)
{
var duplicateIds = detailList
.Where(d => d.StockitemId > 0)
.GroupBy(d => d.StockitemId)
.Where(g => g.Count() > 1)
.Select(g => g.Key)
.OrderBy(x => x)
.ToList();
if (duplicateIds.Count == 0)
return;
var msg = "No se puede emitir la expedición. " +
"El mismo StockItem fue seleccionado más de una vez: " +
string.Join(", ", duplicateIds);
throw new InvalidOperationException(msg);
}
private async Task ValidateSerializedConflictsAsync(List<ELSExpeditionDetail> detailList)
{
var requestedStockItemIds = detailList
.Where(d => d.StockitemId > 0)
.Select(d => d.StockitemId)
.Distinct()
.ToList();
if (requestedStockItemIds.Count == 0)
return;
var conflicts = await _repo.CheckStockItemConflictsAsync(
requestedStockItemIds,
ignoreExpeditionId: null);
if (conflicts.Count == 0)
return;
var lines = new List<string>
{
$"No se puede emitir la expedición: se detectaron {conflicts.Count} stock items serializados ya asignados a expediciones activas."
};
foreach (var conflict in conflicts
.OrderBy(x => x.StockitemId)
.ThenBy(x => x.Expeditionnumber))
{
var statusLabel = ((ExpeditionStatus)conflict.Status).ToLabel();
lines.Add($"• StockItem {conflict.StockitemId} → {conflict.Expeditionnumber} ({statusLabel})");
}
throw new InvalidOperationException(string.Join(Environment.NewLine, lines));
}
private async Task ValidateStockAvailabilityAsync(List<ELSExpeditionDetail> detailList)
{
var requestedByStockItem = detailList
.Where(d => d.StockitemId > 0 && d.Quantity > 0)
.GroupBy(d => d.StockitemId)
.Select(g => new
{
StockitemId = g.Key,
RequestedQuantity = g.Sum(x => x.Quantity)
})
.ToList();
if (requestedByStockItem.Count == 0)
return;
var availability = await _stockItemRepository.GetAvailabilityByStockItemIdsAsync(
requestedByStockItem.Select(x => x.StockitemId));
var availabilityMap = availability.ToDictionary(x => x.StockitemId);
var errors = new List<string>();
foreach (var request in requestedByStockItem.OrderBy(x => x.StockitemId))
{
if (!availabilityMap.TryGetValue(request.StockitemId, out var stock))
{
errors.Add($"• StockItem {request.StockitemId} → no fue encontrado en stock.");
continue;
}
var hasSerial = !string.IsNullOrWhiteSpace(stock.Serial);
// Los serializados ya se validan por exclusividad en ValidateSerializedConflictsAsync
if (hasSerial)
continue;
if (request.RequestedQuantity > stock.AvailableQuantity)
{
errors.Add(
$"• StockItem {request.StockitemId} → solicitado: {request.RequestedQuantity}, disponible: {stock.AvailableQuantity}.");
}
}
if (errors.Count == 0)
return;
var lines = new List<string>
{
"No se puede emitir la expedición: algunos stock items no serializados no tienen cantidad disponible suficiente."
};
lines.AddRange(errors);
throw new InvalidOperationException(string.Join(Environment.NewLine, lines));
}
#endregion
// Otros métodos de la clase...
public Task<ExpeditionDto?> GetDtoByExpeditionNumberAsync(string expeditionNumber)
{
throw new NotImplementedException();
}
public Task<PagedResult<ExpeditionDto>> SearchAsync(
string? expeditionNumber,
string? status,
DateTime? issueDateFrom,
DateTime? issueDateTo,
int? locationId,
int page,
int pageSize)
=> _repo.SearchAsync(expeditionNumber, status, issueDateFrom, issueDateTo, locationId, page, pageSize);
public Task<ExpeditionDto?> GetDtoByIdAsync(int id)
=> _repo.GetDtoByIdAsync(id);
public async Task<byte[]> ExportFilteredToExcelAsync(ExpeditionSearchParams searchParams)
{
try
{
// Realiza la búsqueda de clientes con los parámetros proporcionados
var searchResult = await _repo.SearchAsync(
searchParams.Number,
searchParams.Status,
searchParams.From,
searchParams.To,
searchParams.LocationId,
searchParams.Page,
searchParams.PageSize
);
// Verifica que se hayan encontrado resultados
if (searchResult?.Items is null || !searchResult.Items.Any())
{
throw new Exception("No se encontraron clientes para exportar.");
}
// Llamamos a un método que exporta los datos a Excel
var stream = new XLSXExportBase();
// Convertimos los resultados de la búsqueda a un formato adecuado para el exportador
var items = searchResult.Items.Select(c => new
{
c.Expeditionnumber,
Issuedate = c.Issuedate.ToString("yyyy-MM-dd"), // ← string
Createdat = c.Createdat.ToString("yyyy-MM-dd HH:mm"), // ← string
c.Status,
c.LocationId,
c.ExternalReference,
c.TicketId,
c.ExtrainfoJson,
c.Observations,
c.TotalItems
}).ToList();
// Genera el archivo Excel
var excelFile = stream.ExportExcel(items);
// Devuelve el archivo Excel como un array de bytes
return excelFile;
}
catch (Exception ex)
{
var methodName = MethodBase.GetCurrentMethod()?.Name ?? "UnknownMethod";
throw new Exception($"{ex.Message}", ex);
}
}
public async Task MarkInTransitAsync(int expeditionId)
{
if (expeditionId <= 0)
throw new ArgumentException("El identificador de la expedición no es válido.");
await _repo.MarkInTransitAsync(expeditionId);
}
}
}