All checks were successful
CI/CD Pipeline / Build and Deploy with Docker Compose (pull_request) Successful in 20m38s
Closes #5
225 lines
8.5 KiB
C#
225 lines
8.5 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);
|
|
}
|
|
}
|
|
}
|
|
}
|