2026-05-07 20:47:34 -03:00
|
|
|
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<SalesDocumentCreateResponse> 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.");
|
|
|
|
|
|
2026-05-08 00:15:11 -03:00
|
|
|
foreach (var detail in request.Details)
|
|
|
|
|
ValidateDetail(detail);
|
|
|
|
|
|
2026-05-07 20:47:34 -03:00
|
|
|
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,
|
2026-05-08 00:15:11 -03:00
|
|
|
OriginType = x.OriginType.ToStorageCode(),
|
|
|
|
|
OriginId = ResolveOriginId(x),
|
|
|
|
|
QuoteDetailId = ResolveQuoteDetailId(x),
|
2026-05-07 20:47:34 -03:00
|
|
|
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<SalesDocumentDto?> GetDtoByIdAsync(int id)
|
|
|
|
|
{
|
|
|
|
|
if (id <= 0)
|
|
|
|
|
throw new ArgumentOutOfRangeException(nameof(id));
|
|
|
|
|
|
|
|
|
|
return _salesDocumentRepository.GetDtoByIdAsync(id);
|
|
|
|
|
}
|
2026-05-08 00:15:11 -03:00
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-07 20:47:34 -03:00
|
|
|
}
|
|
|
|
|
}
|