phronCare/Models/Repositories/Stock/PhLSMExpeditionRepository.cs
Leandro Hernan Rojas 6e61b7b598
All checks were successful
CI/CD Pipeline / Build and Deploy with Docker Compose (push) Successful in 10m12s
Update Expeditions Print
2025-09-04 18:15:15 -03:00

183 lines
7.6 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using Domain.Constants;
using Domain.Dtos.Stock;
using Domain.Entities;
using Microsoft.EntityFrameworkCore;
using Models.Helpers;
using Models.Interfaces;
using Models.Models;
namespace Models.Repositories.Stock
{
public class PhLSMExpeditionRepository(
PhronCareOperationsHubContext context,
IPhSFormSeriesRepository formSeriesRepository) : IExpeditionRepository
{
private readonly PhronCareOperationsHubContext _context = context;
private readonly IPhSFormSeriesRepository _formSeriesRepository = formSeriesRepository;
/// <summary>
/// Crea la expedición completa (header + details) con numeración de serie y estado emitido.
/// </summary>
public async Task<(int Id, string Expeditionnumber)> CreateFullExpeditionAsync(
ELSExpeditionHeader expedition, int formSeriesId)
{
using var tx = await _context.Database.BeginTransactionAsync();
try
{
// 1) Numeración (EX-00000000) mismo patrón que Quotes
var next = await _formSeriesRepository.GetNextInternalNumberAsync(formSeriesId);
var series = await _formSeriesRepository.GetByIdAsync(formSeriesId)
?? throw new InvalidOperationException("Serie no encontrada");
var number = $"{series.Letter}-{next:D8}";
var issuedAt = DateTime.Now;
// 2) Completar datos de emisión en el agregado de dominio
expedition.Expeditionnumber = number;
expedition.Issuedate = issuedAt;
expedition.Status = (int)ExpeditionStatus.Emitida;
// 3) Mapear grafo completo Domain -> EF (Header + Details)
// Igual que haces en CreateFullQuoteAsync con EntityMapper.MapEntity(...)
var headerEntity = EntityMapper.MapEntity<ELSExpeditionHeader, PhLsmExpeditionHeader>(expedition);
// 4) Persistir de una (header + colecciones) y confirmar
_context.PhLsmExpeditionHeaders.Add(headerEntity);
await _context.SaveChangesAsync();
await tx.CommitAsync();
return (headerEntity.Id, headerEntity.Expeditionnumber);
}
catch
{
await tx.RollbackAsync();
throw;
}
}
/// <summary>
/// Devuelve el DTO completo de Expedición (cabecera + ítems) listo para UI/impresión.
/// </summary>
public async Task<ExpeditionDto?> GetDtoByIdAsync(int id)
{
// 1) Header + detalles
var header = await _context.PhLsmExpeditionHeaders
.AsNoTracking()
.Include(h => h.PhLsmExpeditionDetails)
.FirstOrDefaultAsync(h => h.Id == id);
if (header is null)
return null;
// 2) Resolver productos (un solo round-trip)
var productIds = header.PhLsmExpeditionDetails
.Select(d => d.ProductId)
.Distinct()
.ToList();
var productMap = productIds.Count == 0
? new Dictionary<int, (string? Name, string? Descripcion, string? FactoryCode, string? ExternalCode, string? RegulatoryCode)>()
: await _context.PhLsmProducts
.Where(p => productIds.Contains(p.Id))
.Select(p => new
{
p.Id,
p.Name,
p.Descripcion,
p.FactoryCode, // código fábrica (preferido en impresión)
p.ExternalCode, // GTIN
p.RegulatoryCode // PM
})
.ToDictionaryAsync(
p => p.Id,
p => (p.Name, p.Descripcion, p.FactoryCode, p.ExternalCode, p.RegulatoryCode)
);
//// 3) Resolver nombres de ubicaciones (si corresponde)
//var locationIds = header.PhLsmExpeditionDetails
// .Select(d => d.LocationId)
// .Where(l => l.HasValue)
// .Select(l => l!.Value)
// .Distinct()
// .ToList();
//var locationMap = locationIds.Count == 0
// ? new Dictionary<int, (string Name, string? Address)>()
// : await _context.PhLsmStockLocations
// .Where(l => locationIds.Contains(l.Id))
// .Select(l => new { l.Id, l.Name, l.Address }) // Address opcional
// .ToDictionaryAsync(l => l.Id, l => (l.Name, l.Address));
// 4) Proyección a DTO (ítems)
var items = header.PhLsmExpeditionDetails.Select(d =>
{
productMap.TryGetValue(d.ProductId, out var p);
var productName = !string.IsNullOrWhiteSpace(p.Name) ? p.Name
: (!string.IsNullOrWhiteSpace(p.Descripcion) ? p.Descripcion : string.Empty);
//var locationName = (d.LocationId.HasValue && locationMap.TryGetValue(d.LocationId.Value, out var ln))
// ? ln.Name
// : null;
return new ExpeditionItemDto
{
Id = d.Id,
ProductId = d.ProductId,
FactoryCode = p.FactoryCode ?? string.Empty, // preferido para mostrar
ProductName = productName,
Quantity = d.Quantity,
Batch = d.Batch,
Serial = d.Serial,
Expiration = d.Expiration,
LocationId = d.LocationId,
LocationName = string.Empty, // locationName, // si lo querés mostrar
};
}).ToList();
// 5) Completar cabecera del DTO
var dto = new ExpeditionDto
{
Id = header.Id,
Expeditionnumber = header.Expeditionnumber,
Issuedate = header.Issuedate,
Status = header.Status,
StatusLabel = MapStatus(header.Status),
ExtrainfoJson = header.ExtrainfoJson, // se arma en el momento de imprimir, como definiste
Observations = header.Observations,
// Opcional: si el header tiene BusinessUnitId / SeriesId, podés resolver aquí sus códigos/nombres.
Items = items
};
//// 6) Si todos los detalles comparten la misma ubicación, reflejarla en cabecera (útil para impresión)
//var distinctLocs = items.Select(i => i.LocationId).Where(x => x.HasValue).Distinct().ToList();
//if (distinctLocs.Count == 1)
//{
// dto.LocationId = distinctLocs[0];
// if (dto.LocationId.HasValue && locationMap.TryGetValue(dto.LocationId.Value, out var ln))
// {
// dto.LocationName = ln.Name;
// // dto.LocationAddress = ln.Address; // si tu DTO lo contempla
// }
//}
return dto;
}
// ----- helpers -----
/// <summary>
/// Mapea el estado entero a etiqueta amigable (enum: Emitida=1, EnTransito=2, EnDestino=3, Retorno=4, Cerrada=5, Anulada=6).
/// </summary>
private static string MapStatus(int status) => status switch
{
1 => "Emitida",
2 => "En tránsito",
3 => "En destino",
4 => "Retorno",
5 => "Cerrada",
6 => "Anulada",
_ => $"Desconocido ({status})"
};
}
}