using Core.Interfaces; using Domain.Constants; using Domain.Dtos.Sales; using Domain.Entities; using Models.Interfaces; namespace Core.Services { public class SalesDocumentService(IPhSSalesDocumentRepository salesDocumentRepository) : ISalesDocumentDom { private readonly IPhSSalesDocumentRepository _salesDocumentRepository = salesDocumentRepository; 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; } } }