using Core.Interfaces; using Domain.Constants; using Domain.Dtos.Sales; using Domain.Entities; using Domain.Generics; using Models.Interfaces; using System.Text.Json; namespace Core.Services { public class SalesDocumentService(IPhSSalesDocumentRepository salesDocumentRepository) : ISalesDocumentDom { private readonly IPhSSalesDocumentRepository _salesDocumentRepository = salesDocumentRepository; public Task> SearchAsync( int? customerId, string? customerText, int? quoteId, int? documentType, int? status, DateTime? issueDateFrom, DateTime? issueDateTo, int page = 1, int pageSize = 50) { return _salesDocumentRepository.SearchAsync( customerId, customerText, quoteId, documentType, status, issueDateFrom, issueDateTo, page, pageSize); } public Task> SearchDeliveryNoteCandidatesAsync( int? customerId, string? customerText, string? deliveryNoteNumber, int? quoteId, DateTime? issueDateFrom, DateTime? issueDateTo, int page = 1, int pageSize = 50) { return _salesDocumentRepository.SearchDeliveryNoteCandidatesAsync( customerId, customerText, deliveryNoteNumber, quoteId, issueDateFrom, issueDateTo, page, pageSize); } public async Task CreateFromDeliveryNotesAsync(SalesDocumentCreateFromDeliveryNotesRequest request) { ArgumentNullException.ThrowIfNull(request); var deliveryNoteIds = request.DeliveryNoteIds .Where(x => x > 0) .Distinct() .ToList(); if (deliveryNoteIds.Count == 0) throw new InvalidOperationException("Debe seleccionar al menos un remito emitido."); if (string.IsNullOrWhiteSpace(request.Currency)) throw new ArgumentException("La moneda es obligatoria.", nameof(request.Currency)); var deliveryNotes = await _salesDocumentRepository.GetDeliveryNotesForSalesDocumentAsync(deliveryNoteIds); if (deliveryNotes.Count != deliveryNoteIds.Count) throw new InvalidOperationException("Uno o más remitos seleccionados no existen."); var notIssued = deliveryNotes.Where(x => !string.Equals(x.Status, "Emitido", StringComparison.OrdinalIgnoreCase)).ToList(); if (notIssued.Count > 0) throw new InvalidOperationException("Solo se pueden facturar remitos en estado Emitido."); var alreadyAssociated = deliveryNotes.Where(x => x.SalesInvoiceId.HasValue && x.SalesInvoiceId.Value > 0).ToList(); if (alreadyAssociated.Count > 0) throw new InvalidOperationException("Uno o más remitos ya están asociados a un Sales Document."); var fiscalCustomerIds = deliveryNotes.Select(x => x.CustomerId).Distinct().ToList(); if (fiscalCustomerIds.Count != 1) throw new InvalidOperationException("No se pueden agrupar remitos de distintos clientes fiscales."); if (deliveryNotes.Any(x => x.Items.Count == 0)) throw new InvalidOperationException("Todos los remitos seleccionados deben tener ítems."); var now = DateTime.Now; var details = new List(); var coverages = new List(); var lineNumber = 1; foreach (var deliveryNote in deliveryNotes) { foreach (var item in deliveryNote.Items) { var unitPrice = item.ApprovedUnitPrice ?? item.OriginalUnitPrice ?? 0; var approvedAmount = item.ApprovedAmount ?? (unitPrice * item.Quantity); if (approvedAmount <= 0) throw new InvalidOperationException($"El remito {deliveryNote.DeliveryNoteNumber} contiene ítems sin precio aprobado."); var originSnapshot = JsonSerializer.Serialize(new { deliveryNoteId = deliveryNote.Id, deliveryNoteNumber = deliveryNote.DeliveryNoteNumber, deliveryNoteIssueDate = deliveryNote.IssueDate, quoteId = deliveryNote.QuoteId, quoteNumber = deliveryNote.QuoteNumber, customerId = deliveryNote.CustomerId, customerName = deliveryNote.CustomerName, deliveryNoteExtraInfo = deliveryNote.ExtraInfoJson }); details.Add(new ESalesDocumentDetail { LineNumber = lineNumber++, OriginType = SalesDocumentOriginType.DeliveryNote.ToStorageCode(), OriginId = deliveryNote.Id, QuoteDetailId = item.QuoteDetailId, ProductId = item.ProductId, Description = item.Description.Trim(), Quantity = item.Quantity, AuthorizedUnitPrice = unitPrice, AuthorizedAmount = approvedAmount, BilledPercentage = 100, UnitPrice = unitPrice, NetAmount = approvedAmount, TaxAmount = 0, TotalAmount = approvedAmount, OriginSnapshotJson = originSnapshot, Createdat = now }); if (deliveryNote.QuoteId.HasValue) { coverages.Add(new ESalesDocumentCoverage { QuoteId = deliveryNote.QuoteId.Value, QuoteDetailId = item.QuoteDetailId, CoverageType = (int)SalesDocumentCoverageType.Direct, CoveragePercentage = 100, CoverageAmount = approvedAmount, PeriodFrom = request.PeriodFrom, PeriodTo = request.PeriodTo, Notes = $"Coverage desde remito {deliveryNote.DeliveryNoteNumber}", Createdat = now }); } } } var totalAmount = details.Sum(x => x.TotalAmount); if (totalAmount <= 0) throw new InvalidOperationException("El total del documento debe ser mayor a cero."); var operations = deliveryNotes.Select(x => new SalesDocumentDeliveryNoteOperationSnapshotDto { DeliveryNoteId = x.Id, DeliveryNoteNumber = x.DeliveryNoteNumber, QuoteId = x.QuoteId, QuoteNumber = x.QuoteNumber, CustomerId = x.CustomerId, CustomerName = x.CustomerName, IssueDate = x.IssueDate, Amount = x.Items.Sum(i => i.ApprovedAmount ?? ((i.ApprovedUnitPrice ?? i.OriginalUnitPrice ?? 0) * i.Quantity)), Coverage = x.CustomerName }).ToList(); var extraInfoJson = JsonSerializer.Serialize(new { source = "DeliveryNotes", grouped = deliveryNotes.Count > 1, coverageDefinition = "Entidad financiadora / pagadora responsable", operations }); var entity = new ESalesDocument { DocumentType = request.DocumentType, Status = (int)SalesDocumentStatus.Draft, QuoteId = deliveryNotes.Count == 1 ? deliveryNotes[0].QuoteId : null, CustomerId = fiscalCustomerIds[0], BillToCustomerId = fiscalCustomerIds[0], IssueDate = request.IssueDate ?? now, Currency = request.Currency.Trim(), ExchangeRate = request.ExchangeRate <= 0 ? 1 : request.ExchangeRate, NetAmount = totalAmount, TaxAmount = 0, TotalAmount = totalAmount, Observations = request.Observations, ExtraInfoJson = extraInfoJson, PeriodFrom = request.PeriodFrom, PeriodTo = request.PeriodTo, Createdat = now, PhSSalesDocumentDetails = details, PhSSalesDocumentCoverages = coverages }; var created = await _salesDocumentRepository.CreateFromDeliveryNotesAsync(entity, deliveryNoteIds); return new SalesDocumentCreateResponse { Id = created.Id, InternalDocumentNumber = created.InternalDocumentNumber }; } public async Task CreateAsync(SalesDocumentCreateRequest request) { ArgumentNullException.ThrowIfNull(request); if (request.CustomerId <= 0) throw new ArgumentException("Debe seleccionar un cliente.", nameof(request.CustomerId)); if (request.BillToCustomerId <= 0) throw new ArgumentException("Debe seleccionar un cliente de facturación.", nameof(request.BillToCustomerId)); if (string.IsNullOrWhiteSpace(request.Currency)) throw new ArgumentException("La moneda es obligatoria.", nameof(request.Currency)); if (request.Details is null || request.Details.Count == 0) throw new InvalidOperationException("Debe incluir al menos un detail."); if (request.Coverage is null || request.Coverage.Count == 0) throw new InvalidOperationException("Debe incluir coverage."); foreach (var detail in request.Details) ValidateDetail(detail); var netAmount = request.Details.Sum(x => x.NetAmount); var taxAmount = request.Details.Sum(x => x.TaxAmount); var totalAmount = request.Details.Sum(x => x.TotalAmount); if (totalAmount <= 0) throw new InvalidOperationException("El total del documento debe ser mayor a cero."); var now = DateTime.Now; var entity = new ESalesDocument { FormseriesId = request.FormseriesId, DocumentType = request.DocumentType, FiscalVoucherType = request.FiscalVoucherType, FiscalVoucherLetter = request.FiscalVoucherLetter, Status = (int)SalesDocumentStatus.Draft, QuoteId = request.QuoteId, CustomerId = request.CustomerId, BillToCustomerId = request.BillToCustomerId, IssueDate = request.IssueDate ?? now, Currency = request.Currency.Trim(), ExchangeRate = request.ExchangeRate <= 0 ? 1 : request.ExchangeRate, NetAmount = netAmount, TaxAmount = taxAmount, TotalAmount = totalAmount, AssociatedDocumentType = request.AssociatedDocumentType, AssociatedDocumentNumber = request.AssociatedDocumentNumber, AssociatedDocumentDate = request.AssociatedDocumentDate, Observations = request.Observations, ExtraInfoJson = request.ExtraInfoJson, PeriodFrom = request.PeriodFrom, PeriodTo = request.PeriodTo, Createdat = now, PhSSalesDocumentDetails = request.Details.Select(x => new ESalesDocumentDetail { LineNumber = x.LineNumber, OriginType = x.OriginType.ToStorageCode(), OriginId = ResolveOriginId(x), QuoteDetailId = ResolveQuoteDetailId(x), ProductId = x.ProductId, Description = x.Description.Trim(), Quantity = x.Quantity, AuthorizedUnitPrice = x.AuthorizedUnitPrice, AuthorizedAmount = x.AuthorizedAmount, BilledPercentage = x.BilledPercentage, UnitPrice = x.UnitPrice, NetAmount = x.NetAmount, TaxAmount = x.TaxAmount, TotalAmount = x.TotalAmount, OriginSnapshotJson = x.OriginSnapshotJson, Createdat = now }).ToList(), PhSSalesDocumentCoverages = request.Coverage.Select(x => new ESalesDocumentCoverage { QuoteId = x.QuoteId, QuoteDetailId = x.QuoteDetailId, CoverageType = x.CoverageType, CoveragePercentage = x.CoveragePercentage, CoverageAmount = x.CoverageAmount, PeriodFrom = x.PeriodFrom, PeriodTo = x.PeriodTo, Notes = x.Notes, Createdat = now }).ToList() }; var created = await _salesDocumentRepository.CreateAsync(entity); return new SalesDocumentCreateResponse { Id = created.Id, InternalDocumentNumber = created.InternalDocumentNumber }; } public Task GetDtoByIdAsync(int id) { if (id <= 0) throw new ArgumentOutOfRangeException(nameof(id)); return _salesDocumentRepository.GetDtoByIdAsync(id); } private static void ValidateDetail(SalesDocumentCreateDetailRequest detail) { if (detail.LineNumber <= 0) throw new ArgumentException("El número de línea del detail debe ser mayor a cero.", nameof(detail.LineNumber)); if (!Enum.IsDefined(typeof(SalesDocumentOriginType), detail.OriginType)) throw new ArgumentException("El tipo de origen del detail no es válido.", nameof(detail.OriginType)); if (string.IsNullOrWhiteSpace(detail.Description)) throw new ArgumentException("La descripción del detail es obligatoria.", nameof(detail.Description)); if (detail.Quantity <= 0) throw new ArgumentException("La cantidad del detail debe ser mayor a cero.", nameof(detail.Quantity)); var hasOriginId = detail.OriginId.HasValue && detail.OriginId.Value > 0; var hasQuoteDetailId = detail.QuoteDetailId.HasValue && detail.QuoteDetailId.Value > 0; if (detail.OriginType != SalesDocumentOriginType.Manual && !hasOriginId && !hasQuoteDetailId) throw new ArgumentException("Debe informar OriginId o QuoteDetailId para trazabilidad del origen.", nameof(detail.OriginId)); if (detail.OriginType == SalesDocumentOriginType.QuoteDetail && !hasQuoteDetailId && !hasOriginId) throw new ArgumentException("Debe informar QuoteDetailId u OriginId para líneas originadas en presupuesto.", nameof(detail.QuoteDetailId)); } private static int? ResolveOriginId(SalesDocumentCreateDetailRequest detail) { if (detail.OriginId.HasValue && detail.OriginId.Value > 0) return detail.OriginId; return detail.OriginType == SalesDocumentOriginType.QuoteDetail ? detail.QuoteDetailId : null; } private static int? ResolveQuoteDetailId(SalesDocumentCreateDetailRequest detail) { if (detail.QuoteDetailId.HasValue && detail.QuoteDetailId.Value > 0) return detail.QuoteDetailId; return detail.OriginType == SalesDocumentOriginType.QuoteDetail ? detail.OriginId : null; } } }