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 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 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 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 { $"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 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(); 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 { "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 GetDtoByExpeditionNumberAsync(string expeditionNumber) { throw new NotImplementedException(); } public Task> 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 GetDtoByIdAsync(int id) => _repo.GetDtoByIdAsync(id); public async Task 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); } } } }