All checks were successful
CI/CD Pipeline / Build and Deploy with Docker Compose (push) Successful in 10m12s
183 lines
7.6 KiB
C#
183 lines
7.6 KiB
C#
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})"
|
||
};
|
||
}
|
||
|
||
}
|