Add Save Cascade Quote Insert
All checks were successful
CI/CD Pipeline / Build and Deploy with Docker Compose (push) Successful in 5m39s

with Validations
This commit is contained in:
Leandro Hernan Rojas 2025-05-07 18:35:49 -03:00
parent 09090bef21
commit 3d6af27e34
20 changed files with 549 additions and 720 deletions

View File

@ -20,29 +20,14 @@ namespace Models.Interfaces
string? status, string? status,
int page = 1, int page = 1,
int pageSize = 50); int pageSize = 50);
Task<EQuoteHeader> CreateQuoteAsync(EQuoteHeader quote, int formSeriesId);
Task UpdateQuoteAsync(EQuoteHeader quote); Task UpdateQuoteAsync(EQuoteHeader quote);
Task DeleteQuoteAsync(int id); Task DeleteQuoteAsync(int id);
#endregion #endregion
#region Ajustes
Task<IEnumerable<EQuoteAdjustment>> GetAdjustmentsByQuoteIdAsync(int quoteId);
Task<EQuoteAdjustment> AddAdjustmentAsync(EQuoteAdjustment adjustment);
Task UpdateAdjustmentAsync(EQuoteAdjustment adjustment);
Task DeleteAdjustmentAsync(int adjustmentId);
#endregion
#region Impuestos
Task<IEnumerable<EQuoteTax>> GetTaxesByQuoteIdAsync(int quoteId);
Task<EQuoteTax> AddTaxAsync(EQuoteTax tax);
Task UpdateTaxAsync(EQuoteTax tax);
Task DeleteTaxAsync(int taxId);
#endregion
#region Exportación #region Exportación
Task<byte[]> ExportFilteredQuotesToExcelAsync(QuoteSearchParams searchParams); Task<byte[]> ExportFilteredQuotesToExcelAsync(QuoteSearchParams searchParams);
#endregion
#region Guardado completo de presupuesto (encabezado + detalles + roles + ajustes + impuestos)
Task<string> CreateFullQuoteAsync(EQuoteHeader quote, int formSeriesId); Task<string> CreateFullQuoteAsync(EQuoteHeader quote, int formSeriesId);
#endregion #endregion
} }
} }

View File

@ -9,16 +9,12 @@ namespace PhronCare.Core.Services.Sales
{ {
public class QuoteService( public class QuoteService(
IPhSQuoteHeaderRepository quoteHeaderRepository, IPhSQuoteHeaderRepository quoteHeaderRepository,
IPhSQuoteDetailRepository quoteDetailRepository, IPhSQuoteRepository quoteRepository
IPhSQuoteRoleRepository quoteRoleRepository,
IPhSFormSeriesRepository formSeriesRepository
) : IQuoteDom ) : IQuoteDom
{ {
#region Declaraciones #region Declaraciones
private readonly IPhSQuoteHeaderRepository _quoteHeaderRepository = quoteHeaderRepository; private readonly IPhSQuoteHeaderRepository _quoteHeaderRepository = quoteHeaderRepository;
private readonly IPhSQuoteDetailRepository _quoteDetailRepository = quoteDetailRepository; private readonly IPhSQuoteRepository _quoteRepository = quoteRepository;
private readonly IPhSQuoteRoleRepository _quoteRoleRepository = quoteRoleRepository;
private readonly IPhSFormSeriesRepository _formSeriesRepository = formSeriesRepository;
#endregion #endregion
#region Presupuestos #region Presupuestos
@ -26,17 +22,14 @@ namespace PhronCare.Core.Services.Sales
{ {
return await _quoteHeaderRepository.GetAllAsync(page, pageSize); return await _quoteHeaderRepository.GetAllAsync(page, pageSize);
} }
public async Task<EQuoteHeader?> GetQuoteByIdAsync(int id) public async Task<EQuoteHeader?> GetQuoteByIdAsync(int id)
{ {
return await _quoteHeaderRepository.GetByIdAsync(id); return await _quoteHeaderRepository.GetByIdAsync(id);
} }
public async Task<IEnumerable<EQuoteHeader>> GetQuotesByCustomerAsync(int customerId) public async Task<IEnumerable<EQuoteHeader>> GetQuotesByCustomerAsync(int customerId)
{ {
return await _quoteHeaderRepository.GetByCustomerIdAsync(customerId); return await _quoteHeaderRepository.GetByCustomerIdAsync(customerId);
} }
public async Task<PagedResult<EQuoteHeader>> SearchQuotesAsync( public async Task<PagedResult<EQuoteHeader>> SearchQuotesAsync(
int? customerId, int? customerId,
string? quoteNumber, string? quoteNumber,
@ -61,94 +54,16 @@ namespace PhronCare.Core.Services.Sales
page, page,
pageSize); pageSize);
} }
public async Task<EQuoteHeader> CreateQuoteAsync(EQuoteHeader quote, int formSeriesId)
{
// Obtener el próximo número de documento
var nextNumber = await _formSeriesRepository.GetNextInternalNumberAsync(formSeriesId);
quote.Quotenumber = nextNumber.ToString();
// Crear encabezado
var newQuote = await _quoteHeaderRepository.AddAsync(quote);
// Crear detalles asociados
if (quote.PhSQuoteDetails != null)
{
foreach (var detail in quote.PhSQuoteDetails)
{
detail.QuoteheaderId = newQuote.Id;
await _quoteDetailRepository.AddAsync(detail);
}
}
// Crear roles asociados
if (quote.PhSQuoteRoles != null)
{
foreach (var role in quote.PhSQuoteRoles)
{
role.QuoteheaderId = newQuote.Id;
await _quoteRoleRepository.AddAsync(role);
}
}
return newQuote;
}
public async Task UpdateQuoteAsync(EQuoteHeader quote) public async Task UpdateQuoteAsync(EQuoteHeader quote)
{ {
await _quoteHeaderRepository.UpdateAsync(quote); await _quoteHeaderRepository.UpdateAsync(quote);
} }
public async Task DeleteQuoteAsync(int id) public async Task DeleteQuoteAsync(int id)
{ {
await _quoteHeaderRepository.DeleteAsync(id); await _quoteHeaderRepository.DeleteAsync(id);
} }
#endregion #endregion
#region Ajustes
public async Task<IEnumerable<EQuoteAdjustment>> GetAdjustmentsByQuoteIdAsync(int quoteId)
{
return await _quoteHeaderRepository.GetAdjustmentsByQuoteIdAsync(quoteId);
}
public async Task<EQuoteAdjustment> AddAdjustmentAsync(EQuoteAdjustment adjustment)
{
return await _quoteHeaderRepository.AddAdjustmentAsync(adjustment);
}
public async Task UpdateAdjustmentAsync(EQuoteAdjustment adjustment)
{
await _quoteHeaderRepository.UpdateAdjustmentAsync(adjustment);
}
public async Task DeleteAdjustmentAsync(int adjustmentId)
{
await _quoteHeaderRepository.DeleteAdjustmentAsync(adjustmentId);
}
#endregion
#region Impuestos
public async Task<IEnumerable<EQuoteTax>> GetTaxesByQuoteIdAsync(int quoteId)
{
return await _quoteHeaderRepository.GetTaxesByQuoteIdAsync(quoteId);
}
public async Task<EQuoteTax> AddTaxAsync(EQuoteTax tax)
{
return await _quoteHeaderRepository.AddTaxAsync(tax);
}
public async Task UpdateTaxAsync(EQuoteTax tax)
{
await _quoteHeaderRepository.UpdateTaxAsync(tax);
}
public async Task DeleteTaxAsync(int taxId)
{
await _quoteHeaderRepository.DeleteTaxAsync(taxId);
}
#endregion
#region Exportación #region Exportación
public async Task<byte[]> ExportFilteredQuotesToExcelAsync(QuoteSearchParams searchParams) public async Task<byte[]> ExportFilteredQuotesToExcelAsync(QuoteSearchParams searchParams)
{ {
@ -198,66 +113,65 @@ namespace PhronCare.Core.Services.Sales
#region Guardado completo de presupuesto (encabezado + detalles + roles + ajustes + impuestos) #region Guardado completo de presupuesto (encabezado + detalles + roles + ajustes + impuestos)
public async Task<string> CreateFullQuoteAsync(EQuoteHeader quote, int formSeriesId) public async Task<string> CreateFullQuoteAsync(EQuoteHeader quote, int formSeriesId)
{ {
using var transaction = await _quoteHeaderRepository.BeginTransactionAsync(); // 1. Validaciones antes de iniciar transacción
ValidateQuote(quote);
return await _quoteRepository.CreateFullQuoteAsync(quote, formSeriesId);
}
#endregion
try #region Validaciones QuoteCreate
private void ValidateQuote(EQuoteHeader quote)
{ {
// Obtener el próximo número de presupuesto desde la serie if (quote == null)
var nextNumber = await _formSeriesRepository.GetNextInternalNumberAsync(formSeriesId); throw new ArgumentNullException(nameof(quote), "El presupuesto no puede ser nulo.");
quote.Quotenumber = nextNumber.ToString();
// Crear encabezado principal if (quote.CustomerId <= 0)
var headerEntity = await _quoteHeaderRepository.AddAsync(quote); 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.");
// Crear detalles
if (quote.PhSQuoteDetails?.Any() == true)
{
foreach (var detail in quote.PhSQuoteDetails) foreach (var detail in quote.PhSQuoteDetails)
{ {
detail.QuoteheaderId = headerEntity.Id; if (detail.Quantity <= 0)
await _quoteDetailRepository.AddAsync(detail); 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.");
} }
// Crear roles if (quote.PhSQuoteRoles == null || !quote.PhSQuoteRoles.Any())
if (quote.PhSQuoteRoles?.Any() == true) throw new InvalidOperationException("Debe asignar al menos un rol (profesional, paciente o institución).");
{
foreach (var role in quote.PhSQuoteRoles)
{
role.QuoteheaderId = headerEntity.Id;
await _quoteRoleRepository.AddAsync(role);
}
}
// Crear ajustes (rebajas) var hasProfessional = quote.PhSQuoteRoles.Any(r => r.Entitytype == PhSEntityTypes.Professional);
if (quote.PhSQuoteAdjustments?.Any() == true) var hasPatient = quote.PhSQuoteRoles.Any(r => r.Entitytype == PhSEntityTypes.Patient);
{ var hasInstitution = quote.PhSQuoteRoles.Any(r => r.Entitytype == PhSEntityTypes.Institution);
foreach (var adj in quote.PhSQuoteAdjustments)
{
adj.QuoteheaderId = headerEntity.Id;
await _quoteHeaderRepository.AddAdjustmentAsync(adj);
}
}
// Crear impuestos if (!hasProfessional)
if (quote.PhSQuoteTaxes?.Any() == true) 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) foreach (var tax in quote.PhSQuoteTaxes)
{ {
tax.QuoteheaderId = headerEntity.Id; if (tax.Taxrate < 0 || tax.Taxrate > 100)
await _quoteHeaderRepository.AddTaxAsync(tax); throw new ArgumentException($"La alícuota del impuesto '{tax.Taxname}' no es válida.");
} }
} }
await transaction.CommitAsync(); if (quote.Total < 0)
return headerEntity.Quotenumber; throw new ArgumentException("El total del presupuesto no puede ser negativo.");
}
catch
{
await transaction.RollbackAsync();
throw;
}
} }
#endregion #endregion
} }
} }

View File

@ -18,7 +18,7 @@
/// <summary> /// <summary>
/// Código del motivo de ajuste (FK a PhS_AdjustmentReasons) /// Código del motivo de ajuste (FK a PhS_AdjustmentReasons)
/// </summary> /// </summary>
public string ReasonCode { get; set; } = null!; public string ReasonCode { get; set; } = String.Empty;
/// <summary> /// <summary>
/// Importe del ajuste realizado (positivo o nulo) /// Importe del ajuste realizado (positivo o nulo)
@ -28,7 +28,7 @@
/// <summary> /// <summary>
/// Descripción adicional del ajuste /// Descripción adicional del ajuste
/// </summary> /// </summary>
public string? Description { get; set; } public string? Description { get; set; } = String.Empty;
/// <summary> /// <summary>
/// Fecha de registro del ajuste /// Fecha de registro del ajuste

View File

@ -20,7 +20,7 @@
/// <summary> /// <summary>
/// Descripción modificable del producto (puede diferir del original) /// Descripción modificable del producto (puede diferir del original)
/// </summary> /// </summary>
public string? ProductDescription { get; set; } public string? ProductDescription { get; set; }=String.Empty;
/// <summary> /// <summary>
/// Cantidad /// Cantidad
@ -52,8 +52,8 @@
/// </summary> /// </summary>
public DateTime? Modifiedat { get; set; } public DateTime? Modifiedat { get; set; }
public virtual EProduct Product { get; set; } = null!; //public virtual EProduct Product { get; set; } = null!;
public virtual EQuoteHeader PhSQuoteheader { get; set; } = null!; //public virtual EQuoteHeader PhSQuoteheader { get; set; } = null!;
} }
} }

View File

@ -1,4 +1,6 @@
namespace Domain.Entities using System.Text.Json.Serialization;
namespace Domain.Entities
{ {
/// <summary> /// <summary>
/// Tabla de cabeceras de presupuestos /// Tabla de cabeceras de presupuestos
@ -19,7 +21,8 @@
/// <summary> /// <summary>
/// Número visible del presupuesto /// Número visible del presupuesto
/// </summary> /// </summary>
public string Quotenumber { get; set; } = null!;
public string Quotenumber { get; set; } = String.Empty;
/// <summary> /// <summary>
/// Cliente asociado /// Cliente asociado
@ -54,17 +57,17 @@
/// <summary> /// <summary>
/// Código de moneda pactada (ISO 4217). Ej: ARS, USD /// Código de moneda pactada (ISO 4217). Ej: ARS, USD
/// </summary> /// </summary>
public string Currency { get; set; } = null!; public string Currency { get; set; }= String.Empty;
/// <summary> /// <summary>
/// Tipo de cambio pactado para conversión a pesos argentinos /// Tipo de cambio pactado para conversión a pesos argentinos
/// </summary> /// </summary>
public decimal? Exchangerate { get; set; } public decimal Exchangerate { get; set; }
/// <summary> /// <summary>
/// Importe neto antes de aplicar impuestos, expresado en la moneda pactada del presupuesto /// Importe neto antes de aplicar impuestos, expresado en la moneda pactada del presupuesto
/// </summary> /// </summary>
public decimal? Netamount { get; set; } public decimal Netamount { get; set; }
/// <summary> /// <summary>
/// Importe total del presupuesto expresado en la moneda pactada (extranjera), incluyendo impuestos y ajustes comerciales /// Importe total del presupuesto expresado en la moneda pactada (extranjera), incluyendo impuestos y ajustes comerciales
@ -84,7 +87,7 @@
/// <summary> /// <summary>
/// Estado: E (Emitido), A (Aprobado), AC (Aprobado para cirugia), etc. /// Estado: E (Emitido), A (Aprobado), AC (Aprobado para cirugia), etc.
/// </summary> /// </summary>
public string Status { get; set; } = null!; public string Status { get; set; } = String.Empty;
/// <summary> /// <summary>
/// Indica si la cirugía se realizará fuera de la ciudad/localidad habitual (“out of town”) /// Indica si la cirugía se realizará fuera de la ciudad/localidad habitual (“out of town”)
@ -94,7 +97,7 @@
/// <summary> /// <summary>
/// Instrucción dirigida al área de logística para detallar qué debe prepararse o despacharse (ej: “CMF 1.5 + INSTRUMENTAL”) /// Instrucción dirigida al área de logística para detallar qué debe prepararse o despacharse (ej: “CMF 1.5 + INSTRUMENTAL”)
/// </summary> /// </summary>
public string? DispatchInstruction { get; set; } public string? DispatchInstruction { get; set; } = String.Empty;
/// <summary> /// <summary>
/// Cantidad de impresiones /// Cantidad de impresiones
@ -104,7 +107,7 @@
/// <summary> /// <summary>
/// Observaciones internas /// Observaciones internas
/// </summary> /// </summary>
public string? Observations { get; set; } public string? Observations { get; set; } = String.Empty;
/// <summary> /// <summary>
/// Fecha de creación /// Fecha de creación
@ -115,13 +118,9 @@
/// Fecha de modificación /// Fecha de modificación
/// </summary> /// </summary>
public DateTime? Modifiedat { get; set; } public DateTime? Modifiedat { get; set; }
public virtual ICollection<EQuoteAdjustment> PhSQuoteAdjustments { get; set; } = new List<EQuoteAdjustment>(); public virtual ICollection<EQuoteAdjustment> PhSQuoteAdjustments { get; set; } = new List<EQuoteAdjustment>();
public virtual ICollection<EQuoteDetail> PhSQuoteDetails { get; set; } = new List<EQuoteDetail>(); public virtual ICollection<EQuoteDetail> PhSQuoteDetails { get; set; } = new List<EQuoteDetail>();
public virtual ICollection<EQuoteRole> PhSQuoteRoles { get; set; } = new List<EQuoteRole>(); public virtual ICollection<EQuoteRole> PhSQuoteRoles { get; set; } = new List<EQuoteRole>();
public virtual ICollection<EQuoteTax> PhSQuoteTaxes { get; set; } = new List<EQuoteTax>(); public virtual ICollection<EQuoteTax> PhSQuoteTaxes { get; set; } = new List<EQuoteTax>();
} }
} }

View File

@ -21,7 +21,7 @@ namespace Domain.Entities
/// <summary> /// <summary>
/// Tipo de entidad asociada (Ej: PhS_Professionals, PhS_Institutions, PhS_Patients) /// Tipo de entidad asociada (Ej: PhS_Professionals, PhS_Institutions, PhS_Patients)
/// </summary> /// </summary>
public string Entitytype { get; set; } = null!; public string Entitytype { get; set; } = String.Empty;
/// <summary> /// <summary>
/// ID de la entidad asociada /// ID de la entidad asociada

View File

@ -15,12 +15,12 @@
/// <summary> /// <summary>
/// Nombre descriptivo del impuesto /// Nombre descriptivo del impuesto
/// </summary> /// </summary>
public string Taxname { get; set; } = null!; public string Taxname { get; set; } = String.Empty;
/// <summary> /// <summary>
/// Código o identificador oficial del impuesto (ej. AFIP) /// Código o identificador oficial del impuesto (ej. AFIP)
/// </summary> /// </summary>
public string? Taxcode { get; set; } public string? Taxcode { get; set; } = String.Empty;
/// <summary> /// <summary>
/// Base imponible del impuesto (importe gravado) /// Base imponible del impuesto (importe gravado)

View File

@ -1,7 +1,10 @@
namespace Models.Interfaces using Models.Models;
namespace Models.Interfaces
{ {
public interface IPhSFormSeriesRepository public interface IPhSFormSeriesRepository
{ {
Task<PhSFormSeries?> GetByIdAsync(int formSeriesId);
Task<int> GetNextInternalNumberAsync(int formSeriesId); Task<int> GetNextInternalNumberAsync(int formSeriesId);
} }
} }

View File

@ -1,6 +1,5 @@
using Domain.Entities; using Domain.Entities;
using Domain.Generics; using Domain.Generics;
using Microsoft.EntityFrameworkCore.Storage;
namespace Models.Interfaces namespace Models.Interfaces
{ {
@ -12,36 +11,34 @@ namespace Models.Interfaces
Task<PagedResult<EQuoteHeader>> SearchAsync(int? customerId, string? quoteNumber, int? professionalId, Task<PagedResult<EQuoteHeader>> SearchAsync(int? customerId, string? quoteNumber, int? professionalId,
int? institutionId, int? patientId, DateTime? issueDateFrom, DateTime? issueDateTo, string? status, int? institutionId, int? patientId, DateTime? issueDateFrom, DateTime? issueDateTo, string? status,
int page = 1, int pageSize = 50); int page = 1, int pageSize = 50);
Task<EQuoteHeader> AddAsync(EQuoteHeader quoteHeader);
Task UpdateAsync(EQuoteHeader quoteHeader); Task UpdateAsync(EQuoteHeader quoteHeader);
Task DeleteAsync(int id); Task DeleteAsync(int id);
// Ajustes // Ajustes
Task<IEnumerable<EQuoteAdjustment>> GetAdjustmentsByQuoteIdAsync(int quoteId); //Task<IEnumerable<EQuoteAdjustment>> GetAdjustmentsByQuoteIdAsync(int quoteId);
Task<EQuoteAdjustment> AddAdjustmentAsync(EQuoteAdjustment adjustment); //Task<EQuoteAdjustment> AddAdjustmentAsync(EQuoteAdjustment adjustment);
Task UpdateAdjustmentAsync(EQuoteAdjustment adjustment); //Task UpdateAdjustmentAsync(EQuoteAdjustment adjustment);
Task DeleteAdjustmentAsync(int adjustmentId); //Task DeleteAdjustmentAsync(int adjustmentId);
/// <summary> ///// <summary>
/// Obtiene todos los impuestos asociados a un presupuesto dado por su ID. ///// Obtiene todos los impuestos asociados a un presupuesto dado por su ID.
/// </summary> ///// </summary>
Task<IEnumerable<EQuoteTax>> GetTaxesByQuoteIdAsync(int quoteId); //Task<IEnumerable<EQuoteTax>> GetTaxesByQuoteIdAsync(int quoteId);
/// <summary> ///// <summary>
/// Agrega un nuevo impuesto al presupuesto correspondiente. ///// Agrega un nuevo impuesto al presupuesto correspondiente.
/// </summary> ///// </summary>
Task<EQuoteTax> AddTaxAsync(EQuoteTax tax); //Task<EQuoteTax> AddTaxAsync(EQuoteTax tax);
/// <summary> ///// <summary>
/// Actualiza los datos de un impuesto existente en un presupuesto. ///// Actualiza los datos de un impuesto existente en un presupuesto.
/// </summary> ///// </summary>
Task UpdateTaxAsync(EQuoteTax tax); //Task UpdateTaxAsync(EQuoteTax tax);
/// <summary> ///// <summary>
/// Elimina un impuesto asociado a un presupuesto a partir de su ID. ///// Elimina un impuesto asociado a un presupuesto a partir de su ID.
/// </summary> ///// </summary>
Task DeleteTaxAsync(int taxId); //Task DeleteTaxAsync(int taxId);
Task<string> CreateFullQuoteAsync(EQuoteHeader quote, int formSeriesId); //Task<string> CreateFullQuoteAsync(EQuoteHeader quote, int formSeriesId);
Task<IDbContextTransaction> BeginTransactionAsync(); //Task<IDbContextTransaction> BeginTransactionAsync();
} }
} }

View File

@ -0,0 +1,9 @@
using Domain.Entities;
namespace Models.Interfaces
{
public interface IPhSQuoteRepository
{
Task<string> CreateFullQuoteAsync(EQuoteHeader quote, int formSeriesId);
}
}

View File

@ -29,7 +29,12 @@ namespace PhronCare.Core.Data.Repositories.Sales
return (int)nextNumberParam.Value; return (int)nextNumberParam.Value;
} }
public async Task<PhSFormSeries?> GetByIdAsync(int formSeriesId)
{
return await _context.PhSFormSeries
.AsNoTracking()
.FirstOrDefaultAsync(s => s.Id == formSeriesId);
}
#endregion #endregion
} }
} }

View File

@ -1,17 +1,17 @@
using Domain.Entities; using Microsoft.EntityFrameworkCore;
using Domain.Generics;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Storage;
using Models.Helpers;
using Models.Interfaces; using Models.Interfaces;
using Models.Helpers;
using Models.Models; using Models.Models;
using Domain.Entities;
using Domain.Generics;
namespace PhronCare.Core.Data.Repositories.Sales namespace PhronCare.Core.Data.Repositories.Sales
{ {
public class PhSQuoteHeaderRepository(PhronCareOperationsHubContext context, IPhSFormSeriesRepository formSeriesRepository) : IPhSQuoteHeaderRepository public class PhSQuoteHeaderRepository(PhronCareOperationsHubContext context) : IPhSQuoteHeaderRepository
{ {
private readonly PhronCareOperationsHubContext _context = context; private readonly PhronCareOperationsHubContext _context = context;
private readonly IPhSFormSeriesRepository _formSeriesRepository = formSeriesRepository; //private readonly IPhSFormSeriesRepository _formSeriesRepository = formSeriesRepository;
#region Metodos
public async Task<PagedResult<EQuoteHeader>> GetAllAsync(int page = 1, int pageSize = 50) public async Task<PagedResult<EQuoteHeader>> GetAllAsync(int page = 1, int pageSize = 50)
{ {
var query = _context.PhSQuoteHeaders var query = _context.PhSQuoteHeaders
@ -100,13 +100,7 @@ namespace PhronCare.Core.Data.Repositories.Sales
PageSize = pagedEntities.PageSize PageSize = pagedEntities.PageSize
}; };
} }
public async Task<EQuoteHeader> AddAsync(EQuoteHeader quoteHeader)
{
var dbEntity = EntityMapper.MapEntity<EQuoteHeader, PhSQuoteHeader>(quoteHeader);
_context.PhSQuoteHeaders.Add(dbEntity);
await _context.SaveChangesAsync();
return EntityMapper.MapEntity<PhSQuoteHeader, EQuoteHeader>(dbEntity);
}
public async Task UpdateAsync(EQuoteHeader quoteHeader) public async Task UpdateAsync(EQuoteHeader quoteHeader)
{ {
var dbEntity = EntityMapper.MapEntity<EQuoteHeader, PhSQuoteHeader>(quoteHeader); var dbEntity = EntityMapper.MapEntity<EQuoteHeader, PhSQuoteHeader>(quoteHeader);
@ -122,172 +116,173 @@ namespace PhronCare.Core.Data.Repositories.Sales
await _context.SaveChangesAsync(); await _context.SaveChangesAsync();
} }
} }
#endregion
#region <DECRECATED>
// ---------------------------- // ----------------------------
// Métodos para Ajustes // Métodos para Ajustes
// ---------------------------- // ----------------------------
public async Task<IEnumerable<EQuoteAdjustment>> GetAdjustmentsByQuoteIdAsync(int quoteId) //public async Task<IEnumerable<EQuoteAdjustment>> GetAdjustmentsByQuoteIdAsync(int quoteId)
{ //{
var adjustments = await _context.PhSQuoteAdjustments // var adjustments = await _context.PhSQuoteAdjustments
.Where(a => a.QuoteheaderId == quoteId) // .Where(a => a.QuoteheaderId == quoteId)
.ToListAsync(); // .ToListAsync();
return adjustments.Select(EntityMapper.MapEntity<PhSQuoteAdjustment, EQuoteAdjustment>); // return adjustments.Select(EntityMapper.MapEntity<PhSQuoteAdjustment, EQuoteAdjustment>);
} //}
public async Task<EQuoteAdjustment> AddAdjustmentAsync(EQuoteAdjustment adjustment) //public async Task<EQuoteAdjustment> AddAdjustmentAsync(EQuoteAdjustment adjustment)
{ //{
var dbEntity = EntityMapper.MapEntity<EQuoteAdjustment, PhSQuoteAdjustment>(adjustment); // var dbEntity = EntityMapper.MapEntity<EQuoteAdjustment, PhSQuoteAdjustment>(adjustment);
_context.PhSQuoteAdjustments.Add(dbEntity); // _context.PhSQuoteAdjustments.Add(dbEntity);
await _context.SaveChangesAsync(); // await _context.SaveChangesAsync();
return EntityMapper.MapEntity<PhSQuoteAdjustment, EQuoteAdjustment>(dbEntity); // return EntityMapper.MapEntity<PhSQuoteAdjustment, EQuoteAdjustment>(dbEntity);
} //}
public async Task UpdateAdjustmentAsync(EQuoteAdjustment adjustment) //public async Task UpdateAdjustmentAsync(EQuoteAdjustment adjustment)
{ //{
var dbEntity = EntityMapper.MapEntity<EQuoteAdjustment, PhSQuoteAdjustment>(adjustment); // var dbEntity = EntityMapper.MapEntity<EQuoteAdjustment, PhSQuoteAdjustment>(adjustment);
_context.PhSQuoteAdjustments.Update(dbEntity); // _context.PhSQuoteAdjustments.Update(dbEntity);
await _context.SaveChangesAsync(); // await _context.SaveChangesAsync();
} //}
public async Task DeleteAdjustmentAsync(int adjustmentId) //public async Task DeleteAdjustmentAsync(int adjustmentId)
{ //{
var entity = await _context.PhSQuoteAdjustments.FindAsync(adjustmentId); // var entity = await _context.PhSQuoteAdjustments.FindAsync(adjustmentId);
if (entity != null) // if (entity != null)
{ // {
_context.PhSQuoteAdjustments.Remove(entity); // _context.PhSQuoteAdjustments.Remove(entity);
await _context.SaveChangesAsync(); // await _context.SaveChangesAsync();
} // }
} //}
// ---------------------------- //// ----------------------------
// Métodos para Impuestos //// Métodos para Impuestos
// ---------------------------- //// ----------------------------
/// <summary> ///// <summary>
/// Obtiene todos los impuestos asociados a un presupuesto dado por su ID. ///// Obtiene todos los impuestos asociados a un presupuesto dado por su ID.
/// </summary> ///// </summary>
public async Task<IEnumerable<EQuoteTax>> GetTaxesByQuoteIdAsync(int quoteId) //public async Task<IEnumerable<EQuoteTax>> GetTaxesByQuoteIdAsync(int quoteId)
{ //{
var taxes = await _context.PhSQuoteTaxes // var taxes = await _context.PhSQuoteTaxes
.Where(t => t.QuoteheaderId == quoteId) // .Where(t => t.QuoteheaderId == quoteId)
.ToListAsync(); // .ToListAsync();
return taxes.Select(EntityMapper.MapEntity<PhSQuoteTaxis, EQuoteTax>); // return taxes.Select(EntityMapper.MapEntity<PhSQuoteTaxis, EQuoteTax>);
} //}
/// <summary> ///// <summary>
/// Agrega un nuevo impuesto al presupuesto correspondiente. ///// Agrega un nuevo impuesto al presupuesto correspondiente.
/// </summary> ///// </summary>
public async Task<EQuoteTax> AddTaxAsync(EQuoteTax tax) //public async Task<EQuoteTax> AddTaxAsync(EQuoteTax tax)
{ //{
var dbEntity = EntityMapper.MapEntity<EQuoteTax, PhSQuoteTaxis>(tax); // var dbEntity = EntityMapper.MapEntity<EQuoteTax, PhSQuoteTaxis>(tax);
_context.PhSQuoteTaxes.Add(dbEntity); // _context.PhSQuoteTaxes.Add(dbEntity);
await _context.SaveChangesAsync(); // await _context.SaveChangesAsync();
return EntityMapper.MapEntity<PhSQuoteTaxis, EQuoteTax>(dbEntity); // return EntityMapper.MapEntity<PhSQuoteTaxis, EQuoteTax>(dbEntity);
} //}
/// <summary> ///// <summary>
/// Actualiza los datos de un impuesto existente en un presupuesto. ///// Actualiza los datos de un impuesto existente en un presupuesto.
/// </summary> ///// </summary>
public async Task UpdateTaxAsync(EQuoteTax tax) //public async Task UpdateTaxAsync(EQuoteTax tax)
{ //{
var dbEntity = EntityMapper.MapEntity<EQuoteTax, PhSQuoteTaxis>(tax); // var dbEntity = EntityMapper.MapEntity<EQuoteTax, PhSQuoteTaxis>(tax);
_context.PhSQuoteTaxes.Update(dbEntity); // _context.PhSQuoteTaxes.Update(dbEntity);
await _context.SaveChangesAsync(); // await _context.SaveChangesAsync();
} //}
/// <summary> ///// <summary>
/// Elimina un impuesto asociado a un presupuesto a partir de su ID. ///// Elimina un impuesto asociado a un presupuesto a partir de su ID.
/// </summary> ///// </summary>
public async Task DeleteTaxAsync(int taxId) //public async Task DeleteTaxAsync(int taxId)
{ //{
var entity = await _context.PhSQuoteTaxes.FindAsync(taxId); // var entity = await _context.PhSQuoteTaxes.FindAsync(taxId);
if (entity != null) // if (entity != null)
{ // {
_context.PhSQuoteTaxes.Remove(entity); // _context.PhSQuoteTaxes.Remove(entity);
await _context.SaveChangesAsync(); // await _context.SaveChangesAsync();
} // }
} //}
#region Guardado completo de presupuesto (encabezado + detalles + roles + ajustes + impuestos) //#region Guardado completo de presupuesto (encabezado + detalles + roles + ajustes + impuestos)
/// <summary> ///// <summary>
/// Crea un nuevo presupuesto, incluyendo encabezado, detalles, roles, ajustes e impuestos asociados. ///// Crea un nuevo presupuesto, incluyendo encabezado, detalles, roles, ajustes e impuestos asociados.
/// Genera automáticamente el número de presupuesto en base a la serie indicada. ///// Genera automáticamente el número de presupuesto en base a la serie indicada.
/// </summary> ///// </summary>
/// <param name="quote">Presupuesto a registrar, incluyendo entidades relacionadas.</param> ///// <param name="quote">Presupuesto a registrar, incluyendo entidades relacionadas.</param>
/// <param name="formSeriesId">Identificador de la serie de numeración a utilizar.</param> ///// <param name="formSeriesId">Identificador de la serie de numeración a utilizar.</param>
/// <returns>Cadena con el número generado del presupuesto.</returns> ///// <returns>Cadena con el número generado del presupuesto.</returns>
public async Task<string> CreateFullQuoteAsync(EQuoteHeader quote, int formSeriesId) //public async Task<string> CreateFullQuoteAsync(EQuoteHeader quote, int formSeriesId)
{ //{
using var transaction = await _context.Database.BeginTransactionAsync(); // using var transaction = await _context.Database.BeginTransactionAsync();
try // try
{ // {
// Obtener el próximo número de presupuesto desde SP // // Obtener el próximo número de presupuesto desde SP
var nextNumber = await _formSeriesRepository.GetNextInternalNumberAsync(formSeriesId); // var nextNumber = await _formSeriesRepository.GetNextInternalNumberAsync(formSeriesId);
quote.Quotenumber = nextNumber.ToString(); // quote.Quotenumber = nextNumber.ToString();
// Map y guardado de Header // // Map y guardado de Header
var headerEntity = EntityMapper.MapEntity<EQuoteHeader, PhSQuoteHeader>(quote); // var headerEntity = EntityMapper.MapEntity<EQuoteHeader, PhSQuoteHeader>(quote);
_context.PhSQuoteHeaders.Add(headerEntity); // _context.PhSQuoteHeaders.Add(headerEntity);
await _context.SaveChangesAsync(); // await _context.SaveChangesAsync();
// Guardado de Detalles // // Guardado de Detalles
if (quote.PhSQuoteDetails?.Any() == true) // if (quote.PhSQuoteDetails?.Any() == true)
{ // {
foreach (var detail in quote.PhSQuoteDetails) // foreach (var detail in quote.PhSQuoteDetails)
{ // {
detail.QuoteheaderId = headerEntity.Id; // detail.QuoteheaderId = headerEntity.Id;
var dbDetail = EntityMapper.MapEntity<EQuoteDetail, PhSQuoteDetail>(detail); // var dbDetail = EntityMapper.MapEntity<EQuoteDetail, PhSQuoteDetail>(detail);
_context.PhSQuoteDetails.Add(dbDetail); // _context.PhSQuoteDetails.Add(dbDetail);
} // }
} // }
// Guardado de Roles // // Guardado de Roles
if (quote.PhSQuoteRoles?.Any() == true) // if (quote.PhSQuoteRoles?.Any() == true)
{ // {
foreach (var role in quote.PhSQuoteRoles) // foreach (var role in quote.PhSQuoteRoles)
{ // {
role.QuoteheaderId = headerEntity.Id; // role.QuoteheaderId = headerEntity.Id;
var dbRole = EntityMapper.MapEntity<EQuoteRole, PhSQuoteRole>(role); // var dbRole = EntityMapper.MapEntity<EQuoteRole, PhSQuoteRole>(role);
_context.PhSQuoteRoles.Add(dbRole); // _context.PhSQuoteRoles.Add(dbRole);
} // }
} // }
// Guardado de Ajustes // // Guardado de Ajustes
if (quote.PhSQuoteAdjustments?.Any() == true) // if (quote.PhSQuoteAdjustments?.Any() == true)
{ // {
foreach (var adj in quote.PhSQuoteAdjustments) // foreach (var adj in quote.PhSQuoteAdjustments)
{ // {
adj.QuoteheaderId = headerEntity.Id; // adj.QuoteheaderId = headerEntity.Id;
var dbAdj = EntityMapper.MapEntity<EQuoteAdjustment, PhSQuoteAdjustment>(adj); // var dbAdj = EntityMapper.MapEntity<EQuoteAdjustment, PhSQuoteAdjustment>(adj);
_context.PhSQuoteAdjustments.Add(dbAdj); // _context.PhSQuoteAdjustments.Add(dbAdj);
} // }
} // }
// Guardado de Impuestos // // Guardado de Impuestos
if (quote.PhSQuoteTaxes?.Any() == true) // if (quote.PhSQuoteTaxes?.Any() == true)
{ // {
foreach (var tax in quote.PhSQuoteTaxes) // foreach (var tax in quote.PhSQuoteTaxes)
{ // {
tax.QuoteheaderId = headerEntity.Id; // tax.QuoteheaderId = headerEntity.Id;
var dbTax = EntityMapper.MapEntity<EQuoteTax, PhSQuoteTaxis>(tax); // var dbTax = EntityMapper.MapEntity<EQuoteTax, PhSQuoteTaxis>(tax);
_context.PhSQuoteTaxes.Add(dbTax); // _context.PhSQuoteTaxes.Add(dbTax);
} // }
} // }
await _context.SaveChangesAsync(); // await _context.SaveChangesAsync();
await transaction.CommitAsync(); // await transaction.CommitAsync();
return headerEntity.Quotenumber; // return headerEntity.Quotenumber;
} // }
catch // catch
{ // {
await transaction.RollbackAsync(); // await transaction.RollbackAsync();
throw; // throw;
} // }
} //}
public async Task<IDbContextTransaction> BeginTransactionAsync() //public async Task<IDbContextTransaction> BeginTransactionAsync()
{ //{
return await _context.Database.BeginTransactionAsync(); // return await _context.Database.BeginTransactionAsync();
} //}
#endregion #endregion
} }
} }

View File

@ -0,0 +1,95 @@
using Domain.Entities;
using Models.Helpers;
using Models.Interfaces;
using Models.Models;
namespace Models.Repositories
{
public class PhSQuoteRepository(PhronCareOperationsHubContext context,
IPhSFormSeriesRepository formSeriesRepository) : IPhSQuoteRepository
{
private readonly PhronCareOperationsHubContext _context = context;
private readonly IPhSFormSeriesRepository _formSeriesRepository = formSeriesRepository;
#region Guardado completo de presupuesto (encabezado + detalles + roles + ajustes + impuestos)
/// <summary>
/// Crea un nuevo presupuesto, incluyendo encabezado, detalles, roles, ajustes e impuestos asociados.
/// Genera automáticamente el número de presupuesto en base a la serie indicada.
/// <param name="quote">Presupuesto a registrar, incluyendo entidades relacionadas.</param>
/// <param name="formSeriesId">Identificador de la serie de numeración a utilizar.</param>
/// <returns>Cadena con el número generado del presupuesto.</returns>
/// </summary>
public async Task<string> CreateFullQuoteAsync(EQuoteHeader quote, int formSeriesId)
{
using var transaction = await _context.Database.BeginTransactionAsync();
try
{
var nextNumber = await _formSeriesRepository.GetNextInternalNumberAsync(formSeriesId);
var series = await _formSeriesRepository.GetByIdAsync(formSeriesId)
?? throw new InvalidOperationException("Serie no encontrada");
int padding = 8; /* format "00000000" */
quote.Quotenumber = $"{series.Letter}-{nextNumber.ToString($"D{padding}")}";
var headerEntity = EntityMapper.MapEntity<EQuoteHeader, PhSQuoteHeader>(quote);
_context.PhSQuoteHeaders.Add(headerEntity);
#region Nota: Esta seccion queda para futura modificacion en caso de cambiar CASCADE INSERT de las entidades relacionadas
//// Guardado de Detalles
//if (quote.PhSQuoteDetails?.Any() == true)
//{
// foreach (var detail in quote.PhSQuoteDetails)
// {
// detail.QuoteheaderId = headerEntity.Id;
// var dbDetail = EntityMapper.MapEntity<EQuoteDetail, PhSQuoteDetail>(detail);
// _context.PhSQuoteDetails.Add(dbDetail);
// }
//}
//// Guardado de Roles
//if (quote.PhSQuoteRoles?.Any() == true)
//{
// foreach (var role in quote.PhSQuoteRoles)
// {
// role.QuoteheaderId = headerEntity.Id;
// var dbRole = EntityMapper.MapEntity<EQuoteRole, PhSQuoteRole>(role);
// _context.PhSQuoteRoles.Add(dbRole);
// }
//}
//// Guardado de Ajustes
//if (quote.PhSQuoteAdjustments?.Any() == true)
//{
// foreach (var adj in quote.PhSQuoteAdjustments)
// {
// adj.QuoteheaderId = headerEntity.Id;
// var dbAdj = EntityMapper.MapEntity<EQuoteAdjustment, PhSQuoteAdjustment>(adj);
// _context.PhSQuoteAdjustments.Add(dbAdj);
// }
//}
//// Guardado de Impuestos
//if (quote.PhSQuoteTaxes?.Any() == true)
//{
// foreach (var tax in quote.PhSQuoteTaxes)
// {
// tax.QuoteheaderId = headerEntity.Id;
// var dbTax = EntityMapper.MapEntity<EQuoteTax, PhSQuoteTaxis>(tax);
// _context.PhSQuoteTaxes.Add(dbTax);
// }
//}
#endregion
await _context.SaveChangesAsync();
await transaction.CommitAsync();
return headerEntity.Quotenumber;
}
catch
{
await transaction.RollbackAsync();
throw;
}
}
#endregion
}
}

View File

@ -91,32 +91,6 @@ namespace phronCare.API.Controllers.Sales
#region Crear / Actualizar / Eliminar #region Crear / Actualizar / Eliminar
[HttpPost("create")]
public async Task<IActionResult> Create([FromBody] EQuoteHeader quote, [FromQuery] int formSeriesId)
{
try
{
if (quote == null)
return BadRequest("El presupuesto no puede ser nulo.");
var result = await _quoteService.CreateQuoteAsync(quote, formSeriesId);
return Ok(result);
}
catch (ArgumentNullException ex)
{
return BadRequest($"Validación fallida: {ex.Message}");
}
catch (InvalidOperationException ex)
{
return BadRequest($"Error de negocio: {ex.Message}");
}
catch (Exception ex)
{
var methodName = MethodBase.GetCurrentMethod()?.Name ?? "UnknownMethod";
return StatusCode(500, $"{methodName} Message: {ex.Message}");
}
}
[HttpPut("update")] [HttpPut("update")]
public async Task<IActionResult> Update([FromBody] EQuoteHeader quote) public async Task<IActionResult> Update([FromBody] EQuoteHeader quote)
{ {
@ -152,145 +126,6 @@ namespace phronCare.API.Controllers.Sales
#endregion #endregion
#region Impuestos (QuoteTaxes)
[HttpGet("{quoteId:int}/taxes")]
public async Task<IActionResult> GetTaxes(int quoteId)
{
try
{
var taxes = await _quoteService.GetTaxesByQuoteIdAsync(quoteId);
return Ok(taxes);
}
catch (Exception ex)
{
var methodName = MethodBase.GetCurrentMethod()?.Name ?? "UnknownMethod";
return StatusCode(500, $"{methodName} Message: {ex.Message}");
}
}
[HttpPost("{quoteId:int}/taxes")]
public async Task<IActionResult> AddTax(int quoteId, [FromBody] EQuoteTax tax)
{
try
{
if (tax == null || quoteId != tax.QuoteheaderId)
return BadRequest("Datos inválidos para el impuesto.");
var result = await _quoteService.AddTaxAsync(tax);
return Ok(result);
}
catch (Exception ex)
{
var methodName = MethodBase.GetCurrentMethod()?.Name ?? "UnknownMethod";
return StatusCode(500, $"{methodName} Message: {ex.Message}");
}
}
[HttpPut("taxes")]
public async Task<IActionResult> UpdateTax([FromBody] EQuoteTax tax)
{
try
{
if (tax == null || tax.Id <= 0)
return BadRequest("Datos inválidos para actualizar el impuesto.");
await _quoteService.UpdateTaxAsync(tax);
return Ok("Impuesto actualizado correctamente.");
}
catch (Exception ex)
{
var methodName = MethodBase.GetCurrentMethod()?.Name ?? "UnknownMethod";
return StatusCode(500, $"{methodName} Message: {ex.Message}");
}
}
[HttpDelete("taxes/{taxId:int}")]
public async Task<IActionResult> DeleteTax(int taxId)
{
try
{
await _quoteService.DeleteTaxAsync(taxId);
return Ok("Impuesto eliminado correctamente.");
}
catch (Exception ex)
{
var methodName = MethodBase.GetCurrentMethod()?.Name ?? "UnknownMethod";
return StatusCode(500, $"{methodName} Message: {ex.Message}");
}
}
#endregion
#region Ajustes Comerciales (QuoteAdjustments)
[HttpGet("{quoteId:int}/adjustments")]
public async Task<IActionResult> GetAdjustments(int quoteId)
{
try
{
var adjustments = await _quoteService.GetAdjustmentsByQuoteIdAsync(quoteId);
return Ok(adjustments);
}
catch (Exception ex)
{
var methodName = MethodBase.GetCurrentMethod()?.Name ?? "UnknownMethod";
return StatusCode(500, $"{methodName} Message: {ex.Message}");
}
}
[HttpPost("{quoteId:int}/adjustments")]
public async Task<IActionResult> AddAdjustment(int quoteId, [FromBody] EQuoteAdjustment adjustment)
{
try
{
if (adjustment == null || quoteId != adjustment.QuoteheaderId)
return BadRequest("Datos inválidos para el ajuste.");
var result = await _quoteService.AddAdjustmentAsync(adjustment);
return Ok(result);
}
catch (Exception ex)
{
var methodName = MethodBase.GetCurrentMethod()?.Name ?? "UnknownMethod";
return StatusCode(500, $"{methodName} Message: {ex.Message}");
}
}
[HttpPut("adjustments")]
public async Task<IActionResult> UpdateAdjustment([FromBody] EQuoteAdjustment adjustment)
{
try
{
if (adjustment == null || adjustment.Id <= 0)
return BadRequest("Datos inválidos para actualizar el ajuste.");
await _quoteService.UpdateAdjustmentAsync(adjustment);
return Ok("Ajuste actualizado correctamente.");
}
catch (Exception ex)
{
var methodName = MethodBase.GetCurrentMethod()?.Name ?? "UnknownMethod";
return StatusCode(500, $"{methodName} Message: {ex.Message}");
}
}
[HttpDelete("adjustments/{adjustmentId:int}")]
public async Task<IActionResult> DeleteAdjustment(int adjustmentId)
{
try
{
await _quoteService.DeleteAdjustmentAsync(adjustmentId);
return Ok("Ajuste eliminado correctamente.");
}
catch (Exception ex)
{
var methodName = MethodBase.GetCurrentMethod()?.Name ?? "UnknownMethod";
return StatusCode(500, $"{methodName} Message: {ex.Message}");
}
}
#endregion
#region Exportación #region Exportación
@ -314,15 +149,23 @@ namespace phronCare.API.Controllers.Sales
#region Endpoint de emision de presupuesto (encabezado + detalles + roles + ajustes + impuestos) #region Endpoint de emision de presupuesto (encabezado + detalles + roles + ajustes + impuestos)
[HttpPost("createfull")] [HttpPost("createfull")]
public async Task<IActionResult> CreateFullQuote([FromBody] EQuoteHeader quote, [FromQuery] int formSeriesId) public async Task<IActionResult> CreateFullQuote([FromBody] CreateFullQuoteRequest request)
{ {
try try
{ {
if (quote == null) // Validamos que el request y el objeto Quote no sean nulos
return BadRequest("El presupuesto no puede ser nulo."); if (request == null || request.Quote == null)
return BadRequest("El payload no puede contener elementos nulos.");
// Desempaquetamos los datos
var quote = request.Quote;
var formSeriesId = request.FormSeriesId;
// Llamada al servicio de negocio
var quoteNumber = await _quoteService.CreateFullQuoteAsync(quote, formSeriesId); var quoteNumber = await _quoteService.CreateFullQuoteAsync(quote, formSeriesId);
return Ok(new { QuoteNumber = quoteNumber });
// Devolvemos el número generado
return Ok(new { Success = true, QuoteNumber = quoteNumber });
} }
catch (ArgumentNullException ex) catch (ArgumentNullException ex)
{ {
@ -334,10 +177,16 @@ namespace phronCare.API.Controllers.Sales
} }
catch (Exception ex) catch (Exception ex)
{ {
var methodName = MethodBase.GetCurrentMethod()?.Name ?? "UnknownMethod"; return StatusCode(500, $"Ocurrió un error interno: {ex.Message}");
return StatusCode(500, $"{methodName} Message: {ex.Message}");
} }
} }
public class CreateFullQuoteRequest
{
public EQuoteHeader Quote { get; set; } = default!;
public int FormSeriesId { get; set; }
}
#endregion #endregion
} }
} }

View File

@ -244,8 +244,8 @@ static void RepositorysAndServices(WebApplicationBuilder builder)
builder.Services.AddScoped<IQuoteDom, QuoteService>(); builder.Services.AddScoped<IQuoteDom, QuoteService>();
builder.Services.AddScoped<IPhSQuoteHeaderRepository, PhSQuoteHeaderRepository>(); builder.Services.AddScoped<IPhSQuoteHeaderRepository, PhSQuoteHeaderRepository>();
builder.Services.AddScoped<IPhSQuoteDetailRepository, PhSQuoteDetailRepository>(); builder.Services.AddScoped<IPhSQuoteRepository, PhSQuoteRepository>();
builder.Services.AddScoped<IPhSQuoteRoleRepository, PhSQuoteRoleRepository>(); //builder.Services.AddScoped<IPhSQuoteRoleRepository, PhSQuoteRoleRepository>();
builder.Services.AddScoped<IPhSFormSeriesRepository, PhSFormSeriesRepository>(); builder.Services.AddScoped<IPhSFormSeriesRepository, PhSFormSeriesRepository>();
// Registrar el service de lookup // Registrar el service de lookup

View File

@ -1556,112 +1556,6 @@
} }
] ]
}, },
{
"ContainingType": "phronCare.API.Controllers.Sales.QuoteController",
"Method": "GetAdjustments",
"RelativePath": "api/Quote/{quoteId}/adjustments",
"HttpMethod": "GET",
"IsController": true,
"Order": 0,
"Parameters": [
{
"Name": "quoteId",
"Type": "System.Int32",
"IsRequired": true
}
],
"ReturnTypes": []
},
{
"ContainingType": "phronCare.API.Controllers.Sales.QuoteController",
"Method": "AddAdjustment",
"RelativePath": "api/Quote/{quoteId}/adjustments",
"HttpMethod": "POST",
"IsController": true,
"Order": 0,
"Parameters": [
{
"Name": "quoteId",
"Type": "System.Int32",
"IsRequired": true
},
{
"Name": "adjustment",
"Type": "Domain.Entities.EQuoteAdjustment",
"IsRequired": true
}
],
"ReturnTypes": []
},
{
"ContainingType": "phronCare.API.Controllers.Sales.QuoteController",
"Method": "GetTaxes",
"RelativePath": "api/Quote/{quoteId}/taxes",
"HttpMethod": "GET",
"IsController": true,
"Order": 0,
"Parameters": [
{
"Name": "quoteId",
"Type": "System.Int32",
"IsRequired": true
}
],
"ReturnTypes": []
},
{
"ContainingType": "phronCare.API.Controllers.Sales.QuoteController",
"Method": "AddTax",
"RelativePath": "api/Quote/{quoteId}/taxes",
"HttpMethod": "POST",
"IsController": true,
"Order": 0,
"Parameters": [
{
"Name": "quoteId",
"Type": "System.Int32",
"IsRequired": true
},
{
"Name": "tax",
"Type": "Domain.Entities.EQuoteTax",
"IsRequired": true
}
],
"ReturnTypes": []
},
{
"ContainingType": "phronCare.API.Controllers.Sales.QuoteController",
"Method": "UpdateAdjustment",
"RelativePath": "api/Quote/adjustments",
"HttpMethod": "PUT",
"IsController": true,
"Order": 0,
"Parameters": [
{
"Name": "adjustment",
"Type": "Domain.Entities.EQuoteAdjustment",
"IsRequired": true
}
],
"ReturnTypes": []
},
{
"ContainingType": "phronCare.API.Controllers.Sales.QuoteController",
"Method": "DeleteAdjustment",
"RelativePath": "api/Quote/adjustments/{adjustmentId}",
"HttpMethod": "DELETE",
"IsController": true,
"Order": 0,
"Parameters": [
{
"Name": "adjustmentId",
"Type": "System.Int32",
"IsRequired": true
}
],
"ReturnTypes": []
},
{ {
"ContainingType": "phronCare.API.Controllers.Sales.QuoteController", "ContainingType": "phronCare.API.Controllers.Sales.QuoteController",
"Method": "GetAll", "Method": "GetAll",
@ -1683,27 +1577,6 @@
], ],
"ReturnTypes": [] "ReturnTypes": []
}, },
{
"ContainingType": "phronCare.API.Controllers.Sales.QuoteController",
"Method": "Create",
"RelativePath": "api/Quote/create",
"HttpMethod": "POST",
"IsController": true,
"Order": 0,
"Parameters": [
{
"Name": "quote",
"Type": "Domain.Entities.EQuoteHeader",
"IsRequired": true
},
{
"Name": "formSeriesId",
"Type": "System.Int32",
"IsRequired": false
}
],
"ReturnTypes": []
},
{ {
"ContainingType": "phronCare.API.Controllers.Sales.QuoteController", "ContainingType": "phronCare.API.Controllers.Sales.QuoteController",
"Method": "CreateFullQuote", "Method": "CreateFullQuote",
@ -1713,14 +1586,9 @@
"Order": 0, "Order": 0,
"Parameters": [ "Parameters": [
{ {
"Name": "quote", "Name": "request",
"Type": "Domain.Entities.EQuoteHeader", "Type": "phronCare.API.Controllers.Sales.QuoteController\u002BCreateFullQuoteRequest",
"IsRequired": true "IsRequired": true
},
{
"Name": "formSeriesId",
"Type": "System.Int32",
"IsRequired": false
} }
], ],
"ReturnTypes": [] "ReturnTypes": []
@ -1818,38 +1686,6 @@
], ],
"ReturnTypes": [] "ReturnTypes": []
}, },
{
"ContainingType": "phronCare.API.Controllers.Sales.QuoteController",
"Method": "UpdateTax",
"RelativePath": "api/Quote/taxes",
"HttpMethod": "PUT",
"IsController": true,
"Order": 0,
"Parameters": [
{
"Name": "tax",
"Type": "Domain.Entities.EQuoteTax",
"IsRequired": true
}
],
"ReturnTypes": []
},
{
"ContainingType": "phronCare.API.Controllers.Sales.QuoteController",
"Method": "DeleteTax",
"RelativePath": "api/Quote/taxes/{taxId}",
"HttpMethod": "DELETE",
"IsController": true,
"Order": 0,
"Parameters": [
{
"Name": "taxId",
"Type": "System.Int32",
"IsRequired": true
}
],
"ReturnTypes": []
},
{ {
"ContainingType": "phronCare.API.Controllers.Sales.QuoteController", "ContainingType": "phronCare.API.Controllers.Sales.QuoteController",
"Method": "Update", "Method": "Update",

View File

@ -4,10 +4,14 @@
@using Blazored.Typeahead @using Blazored.Typeahead
@using Services.Lookups @using Services.Lookups
@using phronCare.UIBlazor.Pages.Sales.Modals @using phronCare.UIBlazor.Pages.Sales.Modals
@using phronCare.UIBlazor.Services.Sales.Quotes
@inject ISalesLookupService SalesLookupService @inject ISalesLookupService SalesLookupService
@inject IQuoteService QuoteService
@inject IToastService toastService
@inject NavigationManager Navigation
@inject IModalService Modal @inject IModalService Modal
<EditForm Model="_quoteModel"> <EditForm Model="_quoteModel" >
<div class="container mt-4" style="zoom:0.8;"> <div class="container mt-4" style="zoom:0.8;">
<div class="card"> <div class="card">
<div class="card-header"> <div class="card-header">
@ -110,9 +114,11 @@
<div class="col-md-4"> <div class="col-md-4">
<label class="form-label">Moneda</label> <label class="form-label">Moneda</label>
<InputSelect class="form-select" @bind-Value="_quoteModel.Currency"> <InputSelect class="form-select" @bind-Value="_quoteModel.Currency">
<option value="">-- Seleccionar moneda --</option>
<option value="ARS">ARS</option> <option value="ARS">ARS</option>
<option value="USD">USD</option> <option value="USD">USD</option>
</InputSelect> </InputSelect>
</div> </div>
<div class="col-md-4"> <div class="col-md-4">
<label class="form-label">Tipo de cambio</label> <label class="form-label">Tipo de cambio</label>
@ -296,7 +302,7 @@
</div> </div>
<div class="card-footer text-end"> <div class="card-footer text-end">
<button type="submit" class="btn btn-primary">Guardar</button> <button type="button" class="btn btn-primary" @onclick="HandleValidSubmit">Guardar</button>
<button type="button" class="btn btn-secondary ms-2">Cancelar</button> <button type="button" class="btn btn-secondary ms-2">Cancelar</button>
</div> </div>
</div> </div>
@ -369,12 +375,12 @@
=> SetLookupSelection(item, sel => _selectedCustomer = sel, id => _quoteModel.CustomerId = id); => SetLookupSelection(item, sel => _selectedCustomer = sel, id => _quoteModel.CustomerId = id);
private Task OnPersonSelected(ELookUpItem item) private Task OnPersonSelected(ELookUpItem item)
=> SetLookupSelection(item, sel => _selectedPerson = sel, id => _quoteModel.PeopleId = id); => SetLookupSelection(item, sel => _selectedPerson = sel, id => _quoteModel.PeopleId = id);
private Task OnProfessionalSelected(ELookUpItem item) // private Task OnProfessionalSelected(ELookUpItem item)
=> SetLookupSelection(item, sel => _selectedProfessional = sel); // => SetLookupSelection(item, sel => _selectedProfessional = sel);
private Task OnInstitutionSelected(ELookUpItem item) // private Task OnInstitutionSelected(ELookUpItem item)
=> SetLookupSelection(item, sel => _selectedInstitution = sel); // => SetLookupSelection(item, sel => _selectedInstitution = sel);
private Task OnPatientSelected(ELookUpItem item) // private Task OnPatientSelected(ELookUpItem item)
=> SetLookupSelection(item, sel => _selectedPatient = sel); // => SetLookupSelection(item, sel => _selectedPatient = sel);
private Task SetLookupSelection(ELookUpItem? item, Action<ELookUpItem?> setSelected, Action<int>? setModelId = null) private Task SetLookupSelection(ELookUpItem? item, Action<ELookUpItem?> setSelected, Action<int>? setModelId = null)
{ {
setSelected(item); setSelected(item);
@ -439,4 +445,65 @@
_quoteModel.PhSQuoteTaxes.Remove(tax); _quoteModel.PhSQuoteTaxes.Remove(tax);
RecalculateTotals(); RecalculateTotals();
} }
private async Task HandleValidSubmit()
{
// Si necesitas validar algo extra antes de llamar al servicio, hazlo aquí
int selectedSeriesId = 1/* obtenlo de donde corresponda, p.ej. de un dropdown */;
var result = await QuoteService.CreateFullQuoteAsync(_quoteModel, selectedSeriesId);
if (!result.Success)
{
toastService.ShowError(result.ErrorMessage);
return;
}
toastService.ShowSuccess($"Presupuesto creado: {result.QuoteNumber}");
//Navigation.NavigateTo($"/sales/quotes/details/{result.QuoteNumber}");
}
/// <summary>
/// Agrega o actualiza un rol dentro de _quoteModel.PhSQuoteRoles
/// </summary>
private void AddOrUpdateRole(string entityType, int entityId, string roleName)
{
var existing = _quoteModel.PhSQuoteRoles
.FirstOrDefault(r => r.Entitytype == entityType);
if (existing != null)
{
existing.EntityId = entityId;
existing.Role = roleName;
}
else
{
_quoteModel.PhSQuoteRoles.Add(new EQuoteRole
{
// QuoteheaderId lo llenará EF en el servidor
Entitytype = entityType,
EntityId = entityId,
Role = roleName
});
}
}
private Task OnProfessionalSelected(ELookUpItem item)
{
_selectedProfessional = item;
AddOrUpdateRole("PhS_Professionals", item.Id, "Medico");
return Task.CompletedTask;
}
private Task OnInstitutionSelected(ELookUpItem item)
{
_selectedInstitution = item;
AddOrUpdateRole("PhS_Institutions", item.Id, "Hospital");
return Task.CompletedTask;
}
private Task OnPatientSelected(ELookUpItem item)
{
_selectedPatient = item;
AddOrUpdateRole("PhS_Patients", item.Id, "Paciente");
return Task.CompletedTask;
}
} }

View File

@ -10,6 +10,7 @@ using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
using Blazored.Modal; using Blazored.Modal;
using Blazored.Toast; using Blazored.Toast;
using phronCare.UIBlazor.Services.Sales.Quotes;
var builder = WebAssemblyHostBuilder.CreateDefault(args); var builder = WebAssemblyHostBuilder.CreateDefault(args);
builder.RootComponents.Add<App>("#app"); builder.RootComponents.Add<App>("#app");
@ -52,6 +53,7 @@ await builder.Build().RunAsync();
static void InjectDependencies(WebAssemblyHostBuilder builder) static void InjectDependencies(WebAssemblyHostBuilder builder)
{ {
builder.Services.AddScoped<ISalesLookupService, SalesLookupService>(); builder.Services.AddScoped<ISalesLookupService, SalesLookupService>();
builder.Services.AddScoped<IQuoteService,QuoteService>();
builder.Services.AddScoped<TicketsService>(); builder.Services.AddScoped<TicketsService>();
builder.Services.AddScoped<CustomerService>(); builder.Services.AddScoped<CustomerService>();
@ -66,4 +68,5 @@ static void InjectDependencies(WebAssemblyHostBuilder builder)
builder.Services.AddScoped<PeopleService>(); builder.Services.AddScoped<PeopleService>();
builder.Services.AddScoped<ProfessionalSpecialtyService>(); builder.Services.AddScoped<ProfessionalSpecialtyService>();
builder.Services.AddScoped<ProductCategoryService>(); builder.Services.AddScoped<ProductCategoryService>();
} }

View File

@ -0,0 +1,10 @@
using Domain.Entities;
namespace phronCare.UIBlazor.Services.Sales.Quotes
{
public interface IQuoteService
{
Task<CreateQuoteResult> CreateFullQuoteAsync(EQuoteHeader quote, int formSeriesId);
// Aquí podrías agregar otros métodos: GetById, Search, etc.
}
}

View File

@ -0,0 +1,62 @@
using Domain.Entities;
using System.Net.Http.Json;
namespace phronCare.UIBlazor.Services.Sales.Quotes
{
public class QuoteService : IQuoteService
{
private readonly HttpClient _http;
public QuoteService(HttpClient http)
{
_http = http;
}
/// <summary>
/// Envía el EQuoteHeader junto con el formSeriesId al backend
/// y recibe el número de presupuesto generado o un error.
/// </summary>
public async Task<CreateQuoteResult> CreateFullQuoteAsync(EQuoteHeader quote, int formSeriesId)
{
// 1) Creamos el DTO request tal cual lo espera el controller
var request = new CreateFullQuoteRequest
{
Quote = quote,
FormSeriesId = formSeriesId
};
// 2) Enviamos ese DTO al endpoint
var response = await _http.PostAsJsonAsync("/api/quote/createfull", request);
//var url = $"/api/quote/createfull?formSeriesId={formSeriesId}";
//var response = await _http.PostAsJsonAsync(url, quote);
if (!response.IsSuccessStatusCode)
{
// Leer mensaje de error del servidor
var serverMessage = await response.Content.ReadAsStringAsync();
return new CreateQuoteResult
{
Success = false,
ErrorMessage = serverMessage
};
}
// Deserializar el resultado esperado
var result = await response.Content.ReadFromJsonAsync<CreateQuoteResult>();
return result!;
}
}
public class CreateQuoteResult
{
public bool Success { get; set; }
public string QuoteNumber { get; set; } = string.Empty;
public string ErrorMessage { get; set; } = string.Empty;
}
public class CreateFullQuoteRequest
{
public EQuoteHeader Quote { get; set; } = default!;
public int FormSeriesId { get; set; }
}
}