using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Zentral.API.Modules.Purchases.Dtos; using Zentral.API.Modules.Warehouse.Services; using Zentral.API.Services; using Zentral.Domain.Entities.Purchases; using Zentral.Domain.Entities.Warehouse; using Zentral.Infrastructure.Data; using Microsoft.EntityFrameworkCore; namespace Zentral.API.Modules.Purchases.Services; public class PurchaseService { private readonly ZentralDbContext _db; private readonly AutoCodeService _autoCodeService; private readonly IWarehouseService _warehouseService; public PurchaseService(ZentralDbContext db, AutoCodeService autoCodeService, IWarehouseService warehouseService) { _db = db; _autoCodeService = autoCodeService; _warehouseService = warehouseService; } public async Task> GetAllAsync() { return await _db.PurchaseOrders .AsNoTracking() .Include(o => o.Supplier) .Include(o => o.DestinationWarehouse) .OrderByDescending(o => o.OrderDate) .Select(o => new PurchaseOrderDto { Id = o.Id, OrderNumber = o.OrderNumber, OrderDate = o.OrderDate, ExpectedDeliveryDate = o.ExpectedDeliveryDate, SupplierId = o.SupplierId, SupplierName = o.Supplier!.Name, Status = o.Status, DestinationWarehouseId = o.DestinationWarehouseId, DestinationWarehouseName = o.DestinationWarehouse != null ? o.DestinationWarehouse.Name : null, Notes = o.Notes, TotalNet = o.TotalNet, TotalTax = o.TotalTax, TotalGross = o.TotalGross, CreatedAt = o.CreatedAt, UpdatedAt = o.UpdatedAt }) .ToListAsync(); } public async Task GetByIdAsync(int id) { var order = await _db.PurchaseOrders .AsNoTracking() .Include(o => o.Supplier) .Include(o => o.DestinationWarehouse) .Include(o => o.Lines) .ThenInclude(l => l.WarehouseArticle) .FirstOrDefaultAsync(o => o.Id == id); if (order == null) return null; return new PurchaseOrderDto { Id = order.Id, OrderNumber = order.OrderNumber, OrderDate = order.OrderDate, ExpectedDeliveryDate = order.ExpectedDeliveryDate, SupplierId = order.SupplierId, SupplierName = order.Supplier!.Name, Status = order.Status, DestinationWarehouseId = order.DestinationWarehouseId, DestinationWarehouseName = order.DestinationWarehouse?.Name, Notes = order.Notes, TotalNet = order.TotalNet, TotalTax = order.TotalTax, TotalGross = order.TotalGross, CreatedAt = order.CreatedAt, UpdatedAt = order.UpdatedAt, Lines = order.Lines.Select(l => new PurchaseOrderLineDto { Id = l.Id, PurchaseOrderId = l.PurchaseOrderId, WarehouseArticleId = l.WarehouseArticleId, ArticleCode = l.WarehouseArticle!.Code, ArticleDescription = l.WarehouseArticle.Description, Description = l.Description, Quantity = l.Quantity, ReceivedQuantity = l.ReceivedQuantity, UnitPrice = l.UnitPrice, TaxRate = l.TaxRate, DiscountPercent = l.DiscountPercent, LineTotal = l.LineTotal }).ToList() }; } public async Task CreateAsync(CreatePurchaseOrderDto dto) { var code = await _autoCodeService.GenerateNextCodeAsync("purchase_order"); if (string.IsNullOrEmpty(code)) { code = $"ODA{DateTime.Now:yyyy}-{Guid.NewGuid().ToString().Substring(0, 5).ToUpper()}"; } var order = new PurchaseOrder { OrderNumber = code, OrderDate = dto.OrderDate, ExpectedDeliveryDate = dto.ExpectedDeliveryDate, SupplierId = dto.SupplierId, DestinationWarehouseId = dto.DestinationWarehouseId, Notes = dto.Notes, Status = PurchaseOrderStatus.Draft }; foreach (var lineDto in dto.Lines) { var line = new PurchaseOrderLine { WarehouseArticleId = lineDto.WarehouseArticleId, Description = lineDto.Description ?? string.Empty, Quantity = lineDto.Quantity, UnitPrice = lineDto.UnitPrice, TaxRate = lineDto.TaxRate, DiscountPercent = lineDto.DiscountPercent }; // If description is empty, fetch from article if (string.IsNullOrEmpty(line.Description)) { var article = await _db.WarehouseArticles.FindAsync(line.WarehouseArticleId); if (article != null) line.Description = article.Description; } // Calculate totals var netPrice = line.UnitPrice * (1 - line.DiscountPercent / 100); line.LineTotal = Math.Round(netPrice * line.Quantity, 2); order.Lines.Add(line); } CalculateOrderTotals(order); _db.PurchaseOrders.Add(order); await _db.SaveChangesAsync(); return await GetByIdAsync(order.Id) ?? throw new InvalidOperationException("Failed to retrieve created order"); } public async Task UpdateAsync(int id, UpdatePurchaseOrderDto dto) { var order = await _db.PurchaseOrders .Include(o => o.Lines) .FirstOrDefaultAsync(o => o.Id == id); if (order == null) return null; if (order.Status != PurchaseOrderStatus.Draft) throw new InvalidOperationException("Solo gli ordini in bozza possono essere modificati"); order.OrderDate = dto.OrderDate; order.ExpectedDeliveryDate = dto.ExpectedDeliveryDate; order.DestinationWarehouseId = dto.DestinationWarehouseId; order.Notes = dto.Notes; order.UpdatedAt = DateTime.Now; // Update lines foreach (var lineDto in dto.Lines) { if (lineDto.IsDeleted) { if (lineDto.Id.HasValue) { var lineToDelete = order.Lines.FirstOrDefault(l => l.Id == lineDto.Id.Value); if (lineToDelete != null) order.Lines.Remove(lineToDelete); } continue; } PurchaseOrderLine line; if (lineDto.Id.HasValue) { line = order.Lines.FirstOrDefault(l => l.Id == lineDto.Id.Value) ?? throw new KeyNotFoundException($"Line {lineDto.Id} not found"); } else { line = new PurchaseOrderLine(); order.Lines.Add(line); } line.WarehouseArticleId = lineDto.WarehouseArticleId; line.Description = lineDto.Description ?? string.Empty; line.Quantity = lineDto.Quantity; line.UnitPrice = lineDto.UnitPrice; line.TaxRate = lineDto.TaxRate; line.DiscountPercent = lineDto.DiscountPercent; if (string.IsNullOrEmpty(line.Description)) { var article = await _db.WarehouseArticles.FindAsync(line.WarehouseArticleId); if (article != null) line.Description = article.Description; } var netPrice = line.UnitPrice * (1 - line.DiscountPercent / 100); line.LineTotal = Math.Round(netPrice * line.Quantity, 2); } CalculateOrderTotals(order); await _db.SaveChangesAsync(); return await GetByIdAsync(id); } public async Task DeleteAsync(int id) { var order = await _db.PurchaseOrders.FindAsync(id); if (order == null) return false; if (order.Status != PurchaseOrderStatus.Draft && order.Status != PurchaseOrderStatus.Cancelled) throw new InvalidOperationException("Solo gli ordini in bozza o annullati possono essere eliminati"); _db.PurchaseOrders.Remove(order); await _db.SaveChangesAsync(); return true; } public async Task ConfirmOrderAsync(int id) { var order = await _db.PurchaseOrders.FindAsync(id); if (order == null) throw new KeyNotFoundException("Order not found"); if (order.Status != PurchaseOrderStatus.Draft) throw new InvalidOperationException("Solo gli ordini in bozza possono essere confermati"); order.Status = PurchaseOrderStatus.Confirmed; order.UpdatedAt = DateTime.Now; await _db.SaveChangesAsync(); return await GetByIdAsync(id) ?? throw new InvalidOperationException("Failed to retrieve order"); } public async Task ReceiveOrderAsync(int id) { var order = await _db.PurchaseOrders .Include(o => o.Lines) .FirstOrDefaultAsync(o => o.Id == id); if (order == null) throw new KeyNotFoundException("Order not found"); if (order.Status != PurchaseOrderStatus.Confirmed && order.Status != PurchaseOrderStatus.PartiallyReceived) throw new InvalidOperationException("L'ordine deve essere confermato per essere ricevuto"); // Create Stock Movement (Inbound) var warehouseId = order.DestinationWarehouseId; if (!warehouseId.HasValue) { var defaultWarehouse = await _warehouseService.GetDefaultWarehouseAsync(); warehouseId = defaultWarehouse?.Id; } if (!warehouseId.HasValue) throw new InvalidOperationException("Nessun magazzino di destinazione specificato o di default"); // Genera numero documento movimento var docNumber = await _warehouseService.GenerateDocumentNumberAsync(MovementType.Inbound); // Trova causale di default per acquisto (se esiste, altrimenti null o crea) var reason = (await _warehouseService.GetMovementReasonsAsync(MovementType.Inbound)) .FirstOrDefault(r => r.Code == "ACQ" || r.Description.Contains("Acquisto")); var movement = new StockMovement { DocumentNumber = docNumber, MovementDate = DateTime.Now, Type = MovementType.Inbound, Status = MovementStatus.Draft, DestinationWarehouseId = warehouseId, ReasonId = reason?.Id, ExternalReference = order.OrderNumber, Notes = $"Ricevimento merce da ordine {order.OrderNumber}" }; movement = await _warehouseService.CreateMovementAsync(movement); // Add lines to movement foreach (var line in order.Lines) { var remainingQty = line.Quantity - line.ReceivedQuantity; if (remainingQty <= 0) continue; // Update received quantity on order line line.ReceivedQuantity += remainingQty; // Add movement line directly via DbContext since IWarehouseService doesn't expose AddLine (it exposes UpdateMovement) // Or better, construct the movement with lines initially if possible. // Since CreateMovementAsync saves, we need to add lines and save again. var movementLine = new StockMovementLine { MovementId = movement.Id, ArticleId = line.WarehouseArticleId, Quantity = remainingQty, UnitCost = line.UnitPrice * (1 - line.DiscountPercent / 100), LineValue = Math.Round(remainingQty * (line.UnitPrice * (1 - line.DiscountPercent / 100)), 2) }; _db.StockMovementLines.Add(movementLine); } await _db.SaveChangesAsync(); // Confirm movement to update stock await _warehouseService.ConfirmMovementAsync(movement.Id); // Update order status var allReceived = order.Lines.All(l => l.ReceivedQuantity >= l.Quantity); order.Status = allReceived ? PurchaseOrderStatus.Received : PurchaseOrderStatus.PartiallyReceived; order.UpdatedAt = DateTime.Now; await _db.SaveChangesAsync(); return await GetByIdAsync(id) ?? throw new InvalidOperationException("Failed to retrieve order"); } private void CalculateOrderTotals(PurchaseOrder order) { order.TotalNet = 0; order.TotalTax = 0; order.TotalGross = 0; foreach (var line in order.Lines) { order.TotalNet += line.LineTotal; var taxAmount = line.LineTotal * (line.TaxRate / 100); order.TotalTax += taxAmount; } order.TotalGross = order.TotalNet + order.TotalTax; } }