feat(stock): reserve stock when expedition moves to EnTransito #10
4
.gitignore
vendored
4
.gitignore
vendored
@ -397,6 +397,10 @@ FodyWeavers.xsd
|
||||
*.msm
|
||||
*.msp
|
||||
|
||||
# Patch files (temporary)
|
||||
*.patch
|
||||
*.diff
|
||||
|
||||
# JetBrains Rider
|
||||
*.sln.iml
|
||||
/Core/obj/Debug/net8.0/Core.csproj.FileListAbsolute.txt
|
||||
|
||||
@ -23,6 +23,10 @@ namespace Models.Interfaces
|
||||
Task<(int Id, string Expeditionnumber)> CreateFullExpeditionAsync(ELSExpeditionHeader expedition, int formSeriesId);
|
||||
Task<ExpeditionDto?> GetDtoByIdAsync(int id);
|
||||
Task<PagedResult<ExpeditionDto>> SearchAsync(string? expeditionNumber, string? status, DateTime? issueDateFrom, DateTime? issueDateTo, int? locationId, int page, int pageSize);
|
||||
/// <summary>
|
||||
/// Pasa la expedición a En tránsito y crea las reservas de stock asociadas.
|
||||
/// La operación es transaccional y falla completa si detecta inconsistencias.
|
||||
/// </summary>
|
||||
Task MarkInTransitAsync(int expeditionId);
|
||||
|
||||
}
|
||||
|
||||
@ -406,7 +406,11 @@ namespace Models.Repositories.Stock
|
||||
}
|
||||
public async Task MarkInTransitAsync(int expeditionId)
|
||||
{
|
||||
const byte expeditionReservationSourceType = 1;
|
||||
const int reservedStatus = 1;
|
||||
|
||||
var header = await _context.PhLsmExpeditionHeaders
|
||||
.Include(x => x.PhLsmExpeditionDetails)
|
||||
.FirstOrDefaultAsync(x => x.Id == expeditionId);
|
||||
|
||||
if (header == null)
|
||||
@ -415,11 +419,150 @@ namespace Models.Repositories.Stock
|
||||
if (header.Status != (int)ExpeditionStatus.Emitida)
|
||||
throw new InvalidOperationException("Solo las expediciones en estado 'Emitida' pueden pasar a 'En tránsito'.");
|
||||
|
||||
var details = header.PhLsmExpeditionDetails?.ToList() ?? new List<PhLsmExpeditionDetail>();
|
||||
|
||||
if (details.Count == 0)
|
||||
throw new InvalidOperationException("No se puede pasar la expedición a 'En tránsito' porque no tiene ítems para reservar.");
|
||||
|
||||
var invalidStockItems = details
|
||||
.Where(d => d.StockitemId <= 0)
|
||||
.Select(d => d.Id)
|
||||
.OrderBy(x => x)
|
||||
.ToList();
|
||||
if (invalidStockItems.Count > 0)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"No se puede pasar la expedición a 'En tránsito' porque existen detalles sin stockitem_id válido. " +
|
||||
$"Detalle(s): {string.Join(", ", invalidStockItems)}");
|
||||
}
|
||||
|
||||
var duplicateStockItems = details
|
||||
.GroupBy(d => d.StockitemId)
|
||||
.Where(g => g.Count() > 1)
|
||||
.Select(g => g.Key)
|
||||
.OrderBy(x => x)
|
||||
.ToList();
|
||||
|
||||
if (duplicateStockItems.Count > 0)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"No se puede pasar la expedición a 'En tránsito' porque el mismo StockItem aparece más de una vez en la expedición: " +
|
||||
string.Join(", ", duplicateStockItems));
|
||||
}
|
||||
|
||||
var detailByStockItem = details
|
||||
.Select(d => new
|
||||
{
|
||||
DetailId = d.Id,
|
||||
StockitemId = d.StockitemId,
|
||||
Quantity = d.Quantity
|
||||
})
|
||||
.ToList();
|
||||
|
||||
var stockItemIds = detailByStockItem
|
||||
.Select(x => x.StockitemId)
|
||||
.Distinct()
|
||||
.ToList();
|
||||
|
||||
var duplicatedReservations = await _context.PhLsmStockReservations
|
||||
.AsNoTracking()
|
||||
.Where(r =>
|
||||
r.SourceType == expeditionReservationSourceType &&
|
||||
r.SourceId == expeditionId &&
|
||||
r.Status == reservedStatus &&
|
||||
stockItemIds.Contains(r.StockitemId))
|
||||
.Select(r => r.StockitemId)
|
||||
.Distinct()
|
||||
.OrderBy(x => x)
|
||||
.ToListAsync();
|
||||
|
||||
if (duplicatedReservations.Count > 0)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"La expedición ya posee reservas activas para los siguientes StockItem: " +
|
||||
string.Join(", ", duplicatedReservations));
|
||||
}
|
||||
|
||||
var stockItems = await _context.PhLsmStockItems
|
||||
.Where(x => stockItemIds.Contains(x.Id))
|
||||
.ToListAsync();
|
||||
|
||||
var stockItemsById = stockItems.ToDictionary(x => x.Id);
|
||||
|
||||
var missingStockItems = stockItemIds
|
||||
.Where(id => !stockItemsById.ContainsKey(id))
|
||||
.OrderBy(x => x)
|
||||
.ToList();
|
||||
|
||||
if (missingStockItems.Count > 0)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"No se puede pasar la expedición a 'En tránsito' porque algunos StockItem no existen: " +
|
||||
string.Join(", ", missingStockItems));
|
||||
}
|
||||
|
||||
var insufficientAvailability = new List<string>();
|
||||
|
||||
foreach (var item in detailByStockItem.OrderBy(x => x.StockitemId))
|
||||
{
|
||||
var stockItem = stockItemsById[item.StockitemId];
|
||||
var availableQuantity = stockItem.Quantity - stockItem.ReservedQuantity;
|
||||
|
||||
if (item.Quantity > availableQuantity)
|
||||
{
|
||||
insufficientAvailability.Add(
|
||||
$"• StockItem {item.StockitemId} → solicitado: {item.Quantity}, disponible: {availableQuantity}.");
|
||||
}
|
||||
}
|
||||
|
||||
if (insufficientAvailability.Count > 0)
|
||||
{
|
||||
var lines = new List<string>
|
||||
{
|
||||
"No se puede pasar la expedición a 'En tránsito' porque algunos StockItem no tienen cantidad disponible suficiente para reservar."
|
||||
};
|
||||
|
||||
lines.AddRange(insufficientAvailability);
|
||||
|
||||
throw new InvalidOperationException(string.Join(Environment.NewLine, lines));
|
||||
}
|
||||
|
||||
using var tx = await _context.Database.BeginTransactionAsync();
|
||||
|
||||
try
|
||||
{
|
||||
var now = DateTime.Now;
|
||||
|
||||
var reservations = detailByStockItem.Select(item => new PhLsmStockReservation
|
||||
{
|
||||
SourceType = expeditionReservationSourceType,
|
||||
SourceId = expeditionId,
|
||||
StockitemId = item.StockitemId,
|
||||
ReservedQuantity = item.Quantity,
|
||||
Status = reservedStatus,
|
||||
Createdat = now
|
||||
}).ToList();
|
||||
|
||||
_context.PhLsmStockReservations.AddRange(reservations);
|
||||
|
||||
foreach (var item in detailByStockItem)
|
||||
{
|
||||
var stockItem = stockItemsById[item.StockitemId];
|
||||
stockItem.ReservedQuantity += item.Quantity;
|
||||
stockItem.Modifiedat = now;
|
||||
}
|
||||
|
||||
header.Status = (int)ExpeditionStatus.EnTransito;
|
||||
header.Modifiedat = DateTime.Now;
|
||||
header.Modifiedat = now;
|
||||
|
||||
await _context.SaveChangesAsync();
|
||||
await tx.CommitAsync();
|
||||
}
|
||||
catch
|
||||
{
|
||||
await tx.RollbackAsync();
|
||||
throw;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -1,7 +0,0 @@
|
||||
|
||||
namespace Models.Repositories.Stock
|
||||
{
|
||||
internal class PhLSMStockReservationRepository
|
||||
{
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user