using Domain.Entities; using Domain.Generics; using Models.Helpers; using Models.Interfaces; using System.Reflection; using Transversal.Services; namespace PhronCare.Core.Services.Sales { public class QuoteService( IPhSQuoteHeaderRepository quoteHeaderRepository, IPhSQuoteRepository quoteRepository ) : IQuoteDom { #region Declaraciones private readonly IPhSQuoteHeaderRepository _quoteHeaderRepository = quoteHeaderRepository; private readonly IPhSQuoteRepository _quoteRepository = quoteRepository; #endregion #region Presupuestos public async Task> GetAllQuotesAsync(int page = 1, int pageSize = 50) { return await _quoteHeaderRepository.GetAllAsync(page, pageSize); } public async Task GetQuoteByIdAsync(int id) { return await _quoteHeaderRepository.GetByIdAsync(id); } public async Task> GetQuotesByCustomerAsync(int customerId) { return await _quoteHeaderRepository.GetByCustomerIdAsync(customerId); } public async Task> SearchQuotesAsync( int? customerId, string? quoteNumber, int? professionalId, int? institutionId, int? patientId, DateTime? issueDateFrom, DateTime? issueDateTo, string? status, int page = 1, int pageSize = 50) { return await _quoteHeaderRepository.SearchAsync( customerId, quoteNumber, professionalId, institutionId, patientId, issueDateFrom, issueDateTo, status, page, pageSize); } public async Task UpdateQuoteAsync(EQuoteHeader quote) { await _quoteHeaderRepository.UpdateAsync(quote); } public async Task DeleteQuoteAsync(int id) { await _quoteHeaderRepository.DeleteAsync(id); } #endregion #region Exportación public async Task ExportFilteredQuotesToExcelAsync(QuoteSearchParams searchParams) { try { var searchResult = await SearchQuotesAsync( searchParams.CustomerId, searchParams.QuoteNumber, searchParams.ProfessionalId, searchParams.InstitutionId, searchParams.PatientId, searchParams.IssueDateFrom, searchParams.IssueDateTo, searchParams.Status, searchParams.Page, searchParams.PageSize); if (searchResult?.Items == null || !searchResult.Items.Any()) { throw new Exception("No se encontraron presupuestos para exportar."); } var stream = new XLSXExportBase(); var quotesData = searchResult.Items.Select(q => new { NúmeroPresupuesto = q.Quotenumber, Estado = q.Status, FechaEmisión = q.Issuedate.ToString("yyyy-MM-dd"), FechaTentativa = q.Estimateddate?.ToString("yyyy-MM-dd"), ImporteAprobado = q.Approvedamount, Profesional = q.PhSQuoteRoles.FirstOrDefault(r => r.Entitytype == PhSEntityTypes.Professional)?.Entitytype, Institución = q.PhSQuoteRoles.FirstOrDefault(r => r.Entitytype == PhSEntityTypes.Institution)?.Entitytype, Paciente = q.PhSQuoteRoles.FirstOrDefault(r => r.Entitytype == PhSEntityTypes.Patient)?.Entitytype }).ToList(); return stream.ExportExcel(quotesData); } catch (Exception ex) { var methodName = MethodBase.GetCurrentMethod()?.Name ?? "UnknownMethod"; throw new Exception($"{methodName} Message: {ex.Message}", ex); } } #endregion #region Guardado completo de presupuesto (encabezado + detalles + roles + ajustes + impuestos) public async Task CreateFullQuoteAsync(EQuoteHeader quote, int formSeriesId) { // 1. Validaciones antes de iniciar transacción ValidateQuote(quote); return await _quoteRepository.CreateFullQuoteAsync(quote, formSeriesId); } #endregion #region Validaciones QuoteCreate private void ValidateQuote(EQuoteHeader quote) { if (quote == null) throw new ArgumentNullException(nameof(quote), "El presupuesto no puede ser nulo."); if (quote.CustomerId <= 0) throw new ArgumentException("Debe seleccionar un cliente."); if (quote.PeopleId <= 0) throw new ArgumentException("Debe seleccionar un vendedor."); if (string.IsNullOrWhiteSpace(quote.Currency)) throw new ArgumentException("La moneda es obligatoria."); if (quote.PhSQuoteDetails == null || !quote.PhSQuoteDetails.Any()) throw new InvalidOperationException("Debe incluir al menos un producto."); foreach (var detail in quote.PhSQuoteDetails) { if (detail.Quantity <= 0) throw new ArgumentException($"La cantidad para el producto {detail.ProductId} debe ser mayor a cero."); if (detail.Unitprice < 0) throw new ArgumentException($"El precio unitario del producto {detail.ProductId} no puede ser negativo."); } if (quote.PhSQuoteRoles == null || !quote.PhSQuoteRoles.Any()) throw new InvalidOperationException("Debe asignar al menos un rol (profesional, paciente o institución)."); var hasProfessional = quote.PhSQuoteRoles.Any(r => r.Entitytype == PhSEntityTypes.Professional); var hasPatient = quote.PhSQuoteRoles.Any(r => r.Entitytype == PhSEntityTypes.Patient); var hasInstitution = quote.PhSQuoteRoles.Any(r => r.Entitytype == PhSEntityTypes.Institution); if (!hasProfessional) throw new InvalidOperationException("Debe asignar un profesional."); if (!hasInstitution) throw new InvalidOperationException("Debe asignar un paciente."); if (!hasPatient) throw new InvalidOperationException("Debe asignar un paciente."); if (quote.PhSQuoteTaxes != null) { foreach (var tax in quote.PhSQuoteTaxes) { if (tax.Taxrate < 0 || tax.Taxrate > 100) throw new ArgumentException($"La alícuota del impuesto '{tax.Taxname}' no es válida."); } } if (quote.Total < 0) throw new ArgumentException("El total del presupuesto no puede ser negativo."); } #endregion } }