ADM-009: Tablas Fiscales (IVA + IIBB) — append-only versioned ref data #22
@@ -27,6 +27,24 @@ using SIGCM2.Application.PuntosDeVenta.GetById;
|
||||
using SIGCM2.Application.PuntosDeVenta.List;
|
||||
using SIGCM2.Application.PuntosDeVenta.Reactivate;
|
||||
using SIGCM2.Application.PuntosDeVenta.Update;
|
||||
using SIGCM2.Application.TiposDeIva.Create;
|
||||
using SIGCM2.Application.TiposDeIva.Deactivate;
|
||||
using SIGCM2.Application.TiposDeIva.Dtos;
|
||||
using SIGCM2.Application.TiposDeIva.GetById;
|
||||
using SIGCM2.Application.TiposDeIva.GetHistorial;
|
||||
using SIGCM2.Application.TiposDeIva.List;
|
||||
using SIGCM2.Application.TiposDeIva.NuevaVersion;
|
||||
using SIGCM2.Application.TiposDeIva.Reactivate;
|
||||
using SIGCM2.Application.TiposDeIva.Update;
|
||||
using SIGCM2.Application.IngresosBrutos.Create;
|
||||
using SIGCM2.Application.IngresosBrutos.Deactivate;
|
||||
using SIGCM2.Application.IngresosBrutos.Dtos;
|
||||
using SIGCM2.Application.IngresosBrutos.GetById;
|
||||
using SIGCM2.Application.IngresosBrutos.GetHistorial;
|
||||
using SIGCM2.Application.IngresosBrutos.List;
|
||||
using SIGCM2.Application.IngresosBrutos.NuevaVersion;
|
||||
using SIGCM2.Application.IngresosBrutos.Reactivate;
|
||||
using SIGCM2.Application.IngresosBrutos.Update;
|
||||
using SIGCM2.Application.Secciones.Create;
|
||||
using SIGCM2.Application.Secciones.Deactivate;
|
||||
using SIGCM2.Application.Secciones.GetById;
|
||||
@@ -104,6 +122,26 @@ public static class DependencyInjection
|
||||
services.AddScoped<ICommandHandler<ListPuntosDeVentaQuery, PagedResult<PuntoDeVentaListItemDto>>, ListPuntosDeVentaQueryHandler>();
|
||||
services.AddScoped<ICommandHandler<GetPuntoDeVentaByIdQuery, PuntoDeVentaDetailDto>, GetPuntoDeVentaByIdQueryHandler>();
|
||||
|
||||
// Tipos de IVA (ADM-009)
|
||||
services.AddScoped<ICommandHandler<CreateTipoDeIvaCommand, TipoDeIvaDto>, CreateTipoDeIvaCommandHandler>();
|
||||
services.AddScoped<ICommandHandler<UpdateTipoDeIvaCommand, TipoDeIvaDto>, UpdateTipoDeIvaCommandHandler>();
|
||||
services.AddScoped<ICommandHandler<NuevaVersionTipoDeIvaCommand, NuevaVersionResultDto>, NuevaVersionTipoDeIvaCommandHandler>();
|
||||
services.AddScoped<ICommandHandler<DeactivateTipoDeIvaCommand, TipoDeIvaDto>, DeactivateTipoDeIvaCommandHandler>();
|
||||
services.AddScoped<ICommandHandler<ReactivateTipoDeIvaCommand, TipoDeIvaDto>, ReactivateTipoDeIvaCommandHandler>();
|
||||
services.AddScoped<ICommandHandler<GetTipoDeIvaByIdQuery, TipoDeIvaDto>, GetTipoDeIvaByIdQueryHandler>();
|
||||
services.AddScoped<ICommandHandler<ListTiposDeIvaQuery, PagedResult<TipoDeIvaDto>>, ListTiposDeIvaQueryHandler>();
|
||||
services.AddScoped<ICommandHandler<GetHistorialTipoDeIvaQuery, IReadOnlyList<HistorialCadenaDto>>, GetHistorialTipoDeIvaQueryHandler>();
|
||||
|
||||
// Ingresos Brutos (ADM-009)
|
||||
services.AddScoped<ICommandHandler<CreateIngresosBrutosCommand, IngresosBrutosDto>, CreateIngresosBrutosCommandHandler>();
|
||||
services.AddScoped<ICommandHandler<UpdateIngresosBrutosCommand, IngresosBrutosDto>, UpdateIngresosBrutosCommandHandler>();
|
||||
services.AddScoped<ICommandHandler<NuevaVersionIngresosBrutosCommand, NuevaVersionIibbResultDto>, NuevaVersionIngresosBrutosCommandHandler>();
|
||||
services.AddScoped<ICommandHandler<DeactivateIngresosBrutosCommand, IngresosBrutosDto>, DeactivateIngresosBrutosCommandHandler>();
|
||||
services.AddScoped<ICommandHandler<ReactivateIngresosBrutosCommand, IngresosBrutosDto>, ReactivateIngresosBrutosCommandHandler>();
|
||||
services.AddScoped<ICommandHandler<GetIngresosBrutosByIdQuery, IngresosBrutosDto>, GetIngresosBrutosByIdQueryHandler>();
|
||||
services.AddScoped<ICommandHandler<ListIngresosBrutosQuery, PagedResult<IngresosBrutosDto>>, ListIngresosBrutosQueryHandler>();
|
||||
services.AddScoped<ICommandHandler<GetHistorialIngresosBrutosQuery, IReadOnlyList<HistorialCadenaIibbDto>>, GetHistorialIngresosBrutosQueryHandler>();
|
||||
|
||||
// FluentValidation validators (scans entire Application assembly)
|
||||
services.AddValidatorsFromAssemblyContaining<LoginCommandValidator>();
|
||||
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
using SIGCM2.Domain.Fiscal;
|
||||
|
||||
namespace SIGCM2.Application.IngresosBrutos.Create;
|
||||
|
||||
public sealed record CreateIngresosBrutosCommand(
|
||||
ProvinciaArgentina Provincia,
|
||||
string Descripcion,
|
||||
decimal Alicuota,
|
||||
DateOnly VigenciaDesde,
|
||||
DateOnly? VigenciaHasta = null);
|
||||
@@ -0,0 +1,57 @@
|
||||
using System.Transactions;
|
||||
using SIGCM2.Application.Abstractions;
|
||||
using SIGCM2.Application.Abstractions.Persistence;
|
||||
using SIGCM2.Application.Audit;
|
||||
using SIGCM2.Application.IngresosBrutos.Dtos;
|
||||
|
||||
namespace SIGCM2.Application.IngresosBrutos.Create;
|
||||
|
||||
public sealed class CreateIngresosBrutosCommandHandler
|
||||
: ICommandHandler<CreateIngresosBrutosCommand, IngresosBrutosDto>
|
||||
{
|
||||
private readonly IIngresosBrutosRepository _repo;
|
||||
private readonly IAuditLogger _audit;
|
||||
|
||||
public CreateIngresosBrutosCommandHandler(IIngresosBrutosRepository repo, IAuditLogger audit)
|
||||
{
|
||||
_repo = repo;
|
||||
_audit = audit;
|
||||
}
|
||||
|
||||
public async Task<IngresosBrutosDto> Handle(CreateIngresosBrutosCommand command)
|
||||
{
|
||||
var entity = Domain.Entities.IngresosBrutos.ForCreation(
|
||||
command.Provincia,
|
||||
command.Descripcion,
|
||||
command.Alicuota,
|
||||
command.VigenciaDesde,
|
||||
command.VigenciaHasta);
|
||||
|
||||
using var tx = new TransactionScope(
|
||||
TransactionScopeOption.Required,
|
||||
new TransactionOptions { IsolationLevel = System.Transactions.IsolationLevel.ReadCommitted },
|
||||
TransactionScopeAsyncFlowOption.Enabled);
|
||||
|
||||
var newId = await _repo.InsertAsync(entity);
|
||||
|
||||
await _audit.LogAsync(
|
||||
action: "ingresos_brutos.create",
|
||||
targetType: "IngresosBrutos",
|
||||
targetId: newId.ToString(),
|
||||
metadata: new { entity.Provincia, entity.Alicuota, entity.VigenciaDesde });
|
||||
|
||||
tx.Complete();
|
||||
|
||||
return IngresosBrutosMapper.ToDto(Domain.Entities.IngresosBrutos.FromDb(
|
||||
id: newId,
|
||||
provincia: entity.Provincia,
|
||||
descripcion: entity.Descripcion,
|
||||
alicuota: entity.Alicuota,
|
||||
activo: entity.Activo,
|
||||
vigenciaDesde: entity.VigenciaDesde,
|
||||
vigenciaHasta: entity.VigenciaHasta,
|
||||
predecesorId: entity.PredecesorId,
|
||||
fechaCreacion: DateTime.UtcNow,
|
||||
fechaModificacion: null));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
using FluentValidation;
|
||||
|
||||
namespace SIGCM2.Application.IngresosBrutos.Create;
|
||||
|
||||
public sealed class CreateIngresosBrutosCommandValidator : AbstractValidator<CreateIngresosBrutosCommand>
|
||||
{
|
||||
private const int DescripcionMaxLength = 255;
|
||||
|
||||
public CreateIngresosBrutosCommandValidator()
|
||||
{
|
||||
RuleFor(x => x.Descripcion)
|
||||
.NotEmpty().WithMessage("La descripción es requerida.")
|
||||
.MaximumLength(DescripcionMaxLength)
|
||||
.WithMessage($"La descripción no puede superar los {DescripcionMaxLength} caracteres.");
|
||||
|
||||
RuleFor(x => x.Alicuota)
|
||||
.InclusiveBetween(0m, 100m)
|
||||
.WithMessage("La alícuota debe estar entre 0 y 100.");
|
||||
|
||||
RuleFor(x => x.VigenciaDesde)
|
||||
.NotEqual(default(DateOnly))
|
||||
.WithMessage("La fecha de vigencia desde es requerida.");
|
||||
|
||||
RuleFor(x => x.VigenciaHasta)
|
||||
.GreaterThanOrEqualTo(x => x.VigenciaDesde)
|
||||
.WithMessage("VigenciaHasta no puede ser anterior a VigenciaDesde.")
|
||||
.When(x => x.VigenciaHasta.HasValue);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
namespace SIGCM2.Application.IngresosBrutos.Deactivate;
|
||||
|
||||
public sealed record DeactivateIngresosBrutosCommand(int Id);
|
||||
@@ -0,0 +1,46 @@
|
||||
using System.Transactions;
|
||||
using SIGCM2.Application.Abstractions;
|
||||
using SIGCM2.Application.Abstractions.Persistence;
|
||||
using SIGCM2.Application.Audit;
|
||||
using SIGCM2.Application.IngresosBrutos.Dtos;
|
||||
using SIGCM2.Domain.Exceptions;
|
||||
|
||||
namespace SIGCM2.Application.IngresosBrutos.Deactivate;
|
||||
|
||||
public sealed class DeactivateIngresosBrutosCommandHandler
|
||||
: ICommandHandler<DeactivateIngresosBrutosCommand, IngresosBrutosDto>
|
||||
{
|
||||
private readonly IIngresosBrutosRepository _repo;
|
||||
private readonly IAuditLogger _audit;
|
||||
|
||||
public DeactivateIngresosBrutosCommandHandler(IIngresosBrutosRepository repo, IAuditLogger audit)
|
||||
{
|
||||
_repo = repo;
|
||||
_audit = audit;
|
||||
}
|
||||
|
||||
public async Task<IngresosBrutosDto> Handle(DeactivateIngresosBrutosCommand command)
|
||||
{
|
||||
var entity = await _repo.GetByIdAsync(command.Id)
|
||||
?? throw new IngresosBrutosNotFoundException(command.Id);
|
||||
|
||||
if (!entity.Activo)
|
||||
return IngresosBrutosMapper.ToDto(entity);
|
||||
|
||||
using var tx = new TransactionScope(
|
||||
TransactionScopeOption.Required,
|
||||
new TransactionOptions { IsolationLevel = System.Transactions.IsolationLevel.ReadCommitted },
|
||||
TransactionScopeAsyncFlowOption.Enabled);
|
||||
|
||||
await _repo.SetActivoAsync(command.Id, false);
|
||||
|
||||
await _audit.LogAsync(
|
||||
action: "ingresos_brutos.deactivate",
|
||||
targetType: "IngresosBrutos",
|
||||
targetId: command.Id.ToString());
|
||||
|
||||
tx.Complete();
|
||||
|
||||
return IngresosBrutosMapper.ToDto(entity.Deactivate());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
using SIGCM2.Domain.Fiscal;
|
||||
|
||||
namespace SIGCM2.Application.IngresosBrutos.Dtos;
|
||||
|
||||
public sealed record HistorialCadenaIibbDto(
|
||||
int Id,
|
||||
ProvinciaArgentina Provincia,
|
||||
decimal Alicuota,
|
||||
DateOnly VigenciaDesde,
|
||||
DateOnly? VigenciaHasta,
|
||||
int? PredecesorId,
|
||||
/// <summary>1-based index in the version chain (1 = root, N = current).</summary>
|
||||
int Version
|
||||
);
|
||||
@@ -0,0 +1,16 @@
|
||||
using SIGCM2.Domain.Fiscal;
|
||||
|
||||
namespace SIGCM2.Application.IngresosBrutos.Dtos;
|
||||
|
||||
public sealed record IngresosBrutosDto(
|
||||
int Id,
|
||||
ProvinciaArgentina Provincia,
|
||||
string Descripcion,
|
||||
decimal Alicuota,
|
||||
bool Activo,
|
||||
DateOnly VigenciaDesde,
|
||||
DateOnly? VigenciaHasta,
|
||||
int? PredecesorId,
|
||||
DateTime FechaCreacion,
|
||||
DateTime? FechaModificacion
|
||||
);
|
||||
@@ -0,0 +1,38 @@
|
||||
using SIGCM2.Domain.Entities;
|
||||
|
||||
namespace SIGCM2.Application.IngresosBrutos.Dtos;
|
||||
|
||||
public static class IngresosBrutosMapper
|
||||
{
|
||||
public static IngresosBrutosDto ToDto(Domain.Entities.IngresosBrutos entity) => new(
|
||||
Id: entity.Id,
|
||||
Provincia: entity.Provincia,
|
||||
Descripcion: entity.Descripcion,
|
||||
Alicuota: entity.Alicuota,
|
||||
Activo: entity.Activo,
|
||||
VigenciaDesde: entity.VigenciaDesde,
|
||||
VigenciaHasta: entity.VigenciaHasta,
|
||||
PredecesorId: entity.PredecesorId,
|
||||
FechaCreacion: entity.FechaCreacion,
|
||||
FechaModificacion: entity.FechaModificacion
|
||||
);
|
||||
|
||||
public static IReadOnlyList<HistorialCadenaIibbDto> ToHistorialChain(IReadOnlyList<Domain.Entities.IngresosBrutos> chain)
|
||||
{
|
||||
var result = new List<HistorialCadenaIibbDto>(chain.Count);
|
||||
for (var i = 0; i < chain.Count; i++)
|
||||
{
|
||||
var item = chain[i];
|
||||
result.Add(new HistorialCadenaIibbDto(
|
||||
Id: item.Id,
|
||||
Provincia: item.Provincia,
|
||||
Alicuota: item.Alicuota,
|
||||
VigenciaDesde: item.VigenciaDesde,
|
||||
VigenciaHasta: item.VigenciaHasta,
|
||||
PredecesorId: item.PredecesorId,
|
||||
Version: i + 1
|
||||
));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
namespace SIGCM2.Application.IngresosBrutos.Dtos;
|
||||
|
||||
public sealed record NuevaVersionIibbResultDto(
|
||||
int PredecesoraId,
|
||||
int NuevaVersionId
|
||||
);
|
||||
@@ -0,0 +1,3 @@
|
||||
namespace SIGCM2.Application.IngresosBrutos.GetById;
|
||||
|
||||
public sealed record GetIngresosBrutosByIdQuery(int Id);
|
||||
@@ -0,0 +1,25 @@
|
||||
using SIGCM2.Application.Abstractions;
|
||||
using SIGCM2.Application.Abstractions.Persistence;
|
||||
using SIGCM2.Application.IngresosBrutos.Dtos;
|
||||
using SIGCM2.Domain.Exceptions;
|
||||
|
||||
namespace SIGCM2.Application.IngresosBrutos.GetById;
|
||||
|
||||
public sealed class GetIngresosBrutosByIdQueryHandler
|
||||
: ICommandHandler<GetIngresosBrutosByIdQuery, IngresosBrutosDto>
|
||||
{
|
||||
private readonly IIngresosBrutosRepository _repo;
|
||||
|
||||
public GetIngresosBrutosByIdQueryHandler(IIngresosBrutosRepository repo)
|
||||
{
|
||||
_repo = repo;
|
||||
}
|
||||
|
||||
public async Task<IngresosBrutosDto> Handle(GetIngresosBrutosByIdQuery query)
|
||||
{
|
||||
var entity = await _repo.GetByIdAsync(query.Id)
|
||||
?? throw new IngresosBrutosNotFoundException(query.Id);
|
||||
|
||||
return IngresosBrutosMapper.ToDto(entity);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
namespace SIGCM2.Application.IngresosBrutos.GetHistorial;
|
||||
|
||||
public sealed record GetHistorialIngresosBrutosQuery(int Id);
|
||||
@@ -0,0 +1,22 @@
|
||||
using SIGCM2.Application.Abstractions;
|
||||
using SIGCM2.Application.Abstractions.Persistence;
|
||||
using SIGCM2.Application.IngresosBrutos.Dtos;
|
||||
|
||||
namespace SIGCM2.Application.IngresosBrutos.GetHistorial;
|
||||
|
||||
public sealed class GetHistorialIngresosBrutosQueryHandler
|
||||
: ICommandHandler<GetHistorialIngresosBrutosQuery, IReadOnlyList<HistorialCadenaIibbDto>>
|
||||
{
|
||||
private readonly IIngresosBrutosRepository _repo;
|
||||
|
||||
public GetHistorialIngresosBrutosQueryHandler(IIngresosBrutosRepository repo)
|
||||
{
|
||||
_repo = repo;
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyList<HistorialCadenaIibbDto>> Handle(GetHistorialIngresosBrutosQuery query)
|
||||
{
|
||||
var chain = await _repo.GetHistorialAsync(query.Id);
|
||||
return IngresosBrutosMapper.ToHistorialChain(chain);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
using SIGCM2.Domain.Fiscal;
|
||||
|
||||
namespace SIGCM2.Application.IngresosBrutos.List;
|
||||
|
||||
public sealed record ListIngresosBrutosQuery(
|
||||
int Page,
|
||||
int PageSize,
|
||||
bool? Activo,
|
||||
ProvinciaArgentina? Provincia);
|
||||
@@ -0,0 +1,30 @@
|
||||
using SIGCM2.Application.Abstractions;
|
||||
using SIGCM2.Application.Abstractions.Persistence;
|
||||
using SIGCM2.Application.Common;
|
||||
using SIGCM2.Application.IngresosBrutos.Dtos;
|
||||
|
||||
namespace SIGCM2.Application.IngresosBrutos.List;
|
||||
|
||||
public sealed class ListIngresosBrutosQueryHandler
|
||||
: ICommandHandler<ListIngresosBrutosQuery, PagedResult<IngresosBrutosDto>>
|
||||
{
|
||||
private readonly IIngresosBrutosRepository _repo;
|
||||
|
||||
public ListIngresosBrutosQueryHandler(IIngresosBrutosRepository repo)
|
||||
{
|
||||
_repo = repo;
|
||||
}
|
||||
|
||||
public async Task<PagedResult<IngresosBrutosDto>> Handle(ListIngresosBrutosQuery query)
|
||||
{
|
||||
var page = Math.Max(1, query.Page);
|
||||
var pageSize = Math.Clamp(query.PageSize, 1, 100);
|
||||
|
||||
var repoQuery = new IngresosBrutosQuery(page, pageSize, query.Activo, query.Provincia);
|
||||
var paged = await _repo.ListAsync(repoQuery);
|
||||
|
||||
var items = paged.Items.Select(IngresosBrutosMapper.ToDto).ToList();
|
||||
|
||||
return new PagedResult<IngresosBrutosDto>(items, paged.Page, paged.PageSize, paged.Total);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
namespace SIGCM2.Application.IngresosBrutos.NuevaVersion;
|
||||
|
||||
public sealed record NuevaVersionIngresosBrutosCommand(
|
||||
int PredecesoraId,
|
||||
decimal NuevaAlicuota,
|
||||
DateOnly VigenciaDesde);
|
||||
@@ -0,0 +1,71 @@
|
||||
using System.Transactions;
|
||||
using SIGCM2.Application.Abstractions;
|
||||
using SIGCM2.Application.Abstractions.Persistence;
|
||||
using SIGCM2.Application.Audit;
|
||||
using SIGCM2.Application.IngresosBrutos.Dtos;
|
||||
using SIGCM2.Domain.Exceptions;
|
||||
|
||||
namespace SIGCM2.Application.IngresosBrutos.NuevaVersion;
|
||||
|
||||
public sealed class NuevaVersionIngresosBrutosCommandHandler
|
||||
: ICommandHandler<NuevaVersionIngresosBrutosCommand, NuevaVersionIibbResultDto>
|
||||
{
|
||||
private readonly IIngresosBrutosRepository _repo;
|
||||
private readonly IAuditLogger _audit;
|
||||
|
||||
public NuevaVersionIngresosBrutosCommandHandler(IIngresosBrutosRepository repo, IAuditLogger audit)
|
||||
{
|
||||
_repo = repo;
|
||||
_audit = audit;
|
||||
}
|
||||
|
||||
public async Task<NuevaVersionIibbResultDto> Handle(NuevaVersionIngresosBrutosCommand command)
|
||||
{
|
||||
// Step 1: load predecesora
|
||||
var predecesora = await _repo.GetByIdAsync(command.PredecesoraId)
|
||||
?? throw new IngresosBrutosNotFoundException(command.PredecesoraId);
|
||||
|
||||
// Step 2: guard — predecesora must be open and active
|
||||
if (!predecesora.Activo || predecesora.VigenciaHasta is not null)
|
||||
throw new PredecesorYaCerradoException(command.PredecesoraId);
|
||||
|
||||
// Steps 3–4: domain validation + tuple creation (throws ArgumentException if vigencia invalid)
|
||||
var (predecesoraCerrada, nuevaVersion) = predecesora.NuevaVersion(
|
||||
command.NuevaAlicuota,
|
||||
command.VigenciaDesde);
|
||||
|
||||
using var tx = new TransactionScope(
|
||||
TransactionScopeOption.Required,
|
||||
new TransactionOptions { IsolationLevel = System.Transactions.IsolationLevel.ReadCommitted },
|
||||
TransactionScopeAsyncFlowOption.Enabled);
|
||||
|
||||
// Step 5: optimistic close — race guard
|
||||
var closed = await _repo.UpdateCierreVigenciaAsync(
|
||||
command.PredecesoraId,
|
||||
predecesoraCerrada.VigenciaHasta!.Value);
|
||||
|
||||
if (!closed)
|
||||
throw new PredecesorYaCerradoException(command.PredecesoraId);
|
||||
|
||||
// Step 6: insert new version
|
||||
var nuevoId = await _repo.InsertAsync(nuevaVersion);
|
||||
|
||||
// Step 7: audit (fail-closed)
|
||||
await _audit.LogAsync(
|
||||
action: "ingresos_brutos.nueva_version",
|
||||
targetType: "IngresosBrutos",
|
||||
targetId: nuevoId.ToString(),
|
||||
metadata: new
|
||||
{
|
||||
predecesoraId = command.PredecesoraId,
|
||||
nuevoId,
|
||||
alicuotaNueva = command.NuevaAlicuota,
|
||||
vigenciaDesde = command.VigenciaDesde,
|
||||
});
|
||||
|
||||
// Step 8: commit
|
||||
tx.Complete();
|
||||
|
||||
return new NuevaVersionIibbResultDto(command.PredecesoraId, nuevoId);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
using FluentValidation;
|
||||
|
||||
namespace SIGCM2.Application.IngresosBrutos.NuevaVersion;
|
||||
|
||||
public sealed class NuevaVersionIngresosBrutosCommandValidator : AbstractValidator<NuevaVersionIngresosBrutosCommand>
|
||||
{
|
||||
public NuevaVersionIngresosBrutosCommandValidator()
|
||||
{
|
||||
RuleFor(x => x.PredecesoraId)
|
||||
.GreaterThan(0).WithMessage("El id de la predecesora debe ser mayor a 0.");
|
||||
|
||||
RuleFor(x => x.NuevaAlicuota)
|
||||
.InclusiveBetween(0m, 100m)
|
||||
.WithMessage("La nueva alícuota debe estar entre 0 y 100.");
|
||||
|
||||
RuleFor(x => x.VigenciaDesde)
|
||||
.NotEqual(default(DateOnly))
|
||||
.WithMessage("La fecha de vigencia desde es requerida.");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
namespace SIGCM2.Application.IngresosBrutos.Reactivate;
|
||||
|
||||
public sealed record ReactivateIngresosBrutosCommand(int Id);
|
||||
@@ -0,0 +1,46 @@
|
||||
using System.Transactions;
|
||||
using SIGCM2.Application.Abstractions;
|
||||
using SIGCM2.Application.Abstractions.Persistence;
|
||||
using SIGCM2.Application.Audit;
|
||||
using SIGCM2.Application.IngresosBrutos.Dtos;
|
||||
using SIGCM2.Domain.Exceptions;
|
||||
|
||||
namespace SIGCM2.Application.IngresosBrutos.Reactivate;
|
||||
|
||||
public sealed class ReactivateIngresosBrutosCommandHandler
|
||||
: ICommandHandler<ReactivateIngresosBrutosCommand, IngresosBrutosDto>
|
||||
{
|
||||
private readonly IIngresosBrutosRepository _repo;
|
||||
private readonly IAuditLogger _audit;
|
||||
|
||||
public ReactivateIngresosBrutosCommandHandler(IIngresosBrutosRepository repo, IAuditLogger audit)
|
||||
{
|
||||
_repo = repo;
|
||||
_audit = audit;
|
||||
}
|
||||
|
||||
public async Task<IngresosBrutosDto> Handle(ReactivateIngresosBrutosCommand command)
|
||||
{
|
||||
var entity = await _repo.GetByIdAsync(command.Id)
|
||||
?? throw new IngresosBrutosNotFoundException(command.Id);
|
||||
|
||||
if (entity.Activo)
|
||||
return IngresosBrutosMapper.ToDto(entity);
|
||||
|
||||
using var tx = new TransactionScope(
|
||||
TransactionScopeOption.Required,
|
||||
new TransactionOptions { IsolationLevel = System.Transactions.IsolationLevel.ReadCommitted },
|
||||
TransactionScopeAsyncFlowOption.Enabled);
|
||||
|
||||
await _repo.SetActivoAsync(command.Id, true);
|
||||
|
||||
await _audit.LogAsync(
|
||||
action: "ingresos_brutos.reactivate",
|
||||
targetType: "IngresosBrutos",
|
||||
targetId: command.Id.ToString());
|
||||
|
||||
tx.Complete();
|
||||
|
||||
return IngresosBrutosMapper.ToDto(entity.Reactivate());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
namespace SIGCM2.Application.IngresosBrutos.Update;
|
||||
|
||||
/// <summary>
|
||||
/// Updates only cosmetic fields: Descripcion, Activo.
|
||||
/// Alicuota and Provincia are NOT part of this command — they are immutable.
|
||||
/// </summary>
|
||||
public sealed record UpdateIngresosBrutosCommand(
|
||||
int Id,
|
||||
string Descripcion,
|
||||
bool Activo);
|
||||
@@ -0,0 +1,51 @@
|
||||
using System.Transactions;
|
||||
using SIGCM2.Application.Abstractions;
|
||||
using SIGCM2.Application.Abstractions.Persistence;
|
||||
using SIGCM2.Application.Audit;
|
||||
using SIGCM2.Application.IngresosBrutos.Dtos;
|
||||
using SIGCM2.Domain.Exceptions;
|
||||
|
||||
namespace SIGCM2.Application.IngresosBrutos.Update;
|
||||
|
||||
public sealed class UpdateIngresosBrutosCommandHandler
|
||||
: ICommandHandler<UpdateIngresosBrutosCommand, IngresosBrutosDto>
|
||||
{
|
||||
private readonly IIngresosBrutosRepository _repo;
|
||||
private readonly IAuditLogger _audit;
|
||||
|
||||
public UpdateIngresosBrutosCommandHandler(IIngresosBrutosRepository repo, IAuditLogger audit)
|
||||
{
|
||||
_repo = repo;
|
||||
_audit = audit;
|
||||
}
|
||||
|
||||
public async Task<IngresosBrutosDto> Handle(UpdateIngresosBrutosCommand command)
|
||||
{
|
||||
var entity = await _repo.GetByIdAsync(command.Id)
|
||||
?? throw new IngresosBrutosNotFoundException(command.Id);
|
||||
|
||||
var updated = entity.WithDescripcion(command.Descripcion);
|
||||
updated = command.Activo ? updated.Reactivate() : updated.Deactivate();
|
||||
|
||||
using var tx = new TransactionScope(
|
||||
TransactionScopeOption.Required,
|
||||
new TransactionOptions { IsolationLevel = System.Transactions.IsolationLevel.ReadCommitted },
|
||||
TransactionScopeAsyncFlowOption.Enabled);
|
||||
|
||||
await _repo.UpdateCosmeticoAsync(command.Id, command.Descripcion, command.Activo);
|
||||
|
||||
await _audit.LogAsync(
|
||||
action: "ingresos_brutos.update",
|
||||
targetType: "IngresosBrutos",
|
||||
targetId: command.Id.ToString(),
|
||||
metadata: new
|
||||
{
|
||||
before = new { entity.Descripcion, entity.Activo },
|
||||
after = new { command.Descripcion, command.Activo },
|
||||
});
|
||||
|
||||
tx.Complete();
|
||||
|
||||
return IngresosBrutosMapper.ToDto(updated);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
using FluentValidation;
|
||||
|
||||
namespace SIGCM2.Application.IngresosBrutos.Update;
|
||||
|
||||
public sealed class UpdateIngresosBrutosCommandValidator : AbstractValidator<UpdateIngresosBrutosCommand>
|
||||
{
|
||||
private const int DescripcionMaxLength = 255;
|
||||
|
||||
public UpdateIngresosBrutosCommandValidator()
|
||||
{
|
||||
RuleFor(x => x.Id)
|
||||
.GreaterThan(0).WithMessage("El id debe ser mayor a 0.");
|
||||
|
||||
RuleFor(x => x.Descripcion)
|
||||
.NotEmpty().WithMessage("La descripción es requerida.")
|
||||
.MaximumLength(DescripcionMaxLength)
|
||||
.WithMessage($"La descripción no puede superar los {DescripcionMaxLength} caracteres.");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
namespace SIGCM2.Application.TiposDeIva.Create;
|
||||
|
||||
public sealed record CreateTipoDeIvaCommand(
|
||||
string Codigo,
|
||||
string Descripcion,
|
||||
decimal Porcentaje,
|
||||
bool AplicaIVA,
|
||||
DateOnly VigenciaDesde,
|
||||
DateOnly? VigenciaHasta = null);
|
||||
@@ -0,0 +1,61 @@
|
||||
using System.Transactions;
|
||||
using SIGCM2.Application.Abstractions;
|
||||
using SIGCM2.Application.Abstractions.Persistence;
|
||||
using SIGCM2.Application.Audit;
|
||||
using SIGCM2.Application.TiposDeIva.Dtos;
|
||||
using SIGCM2.Domain.Entities;
|
||||
using SIGCM2.Domain.Exceptions;
|
||||
|
||||
namespace SIGCM2.Application.TiposDeIva.Create;
|
||||
|
||||
public sealed class CreateTipoDeIvaCommandHandler : ICommandHandler<CreateTipoDeIvaCommand, TipoDeIvaDto>
|
||||
{
|
||||
private readonly ITipoDeIvaRepository _repo;
|
||||
private readonly IAuditLogger _audit;
|
||||
|
||||
public CreateTipoDeIvaCommandHandler(ITipoDeIvaRepository repo, IAuditLogger audit)
|
||||
{
|
||||
_repo = repo;
|
||||
_audit = audit;
|
||||
}
|
||||
|
||||
public async Task<TipoDeIvaDto> Handle(CreateTipoDeIvaCommand command)
|
||||
{
|
||||
var entity = TipoDeIva.ForCreation(
|
||||
command.Codigo,
|
||||
command.Descripcion,
|
||||
command.Porcentaje,
|
||||
command.AplicaIVA,
|
||||
command.VigenciaDesde,
|
||||
command.VigenciaHasta);
|
||||
|
||||
using var tx = new TransactionScope(
|
||||
TransactionScopeOption.Required,
|
||||
new TransactionOptions { IsolationLevel = System.Transactions.IsolationLevel.ReadCommitted },
|
||||
TransactionScopeAsyncFlowOption.Enabled);
|
||||
|
||||
var newId = await _repo.InsertAsync(entity);
|
||||
|
||||
// fail-closed: if LogAsync throws, tx rolls back
|
||||
await _audit.LogAsync(
|
||||
action: "tipo_iva.create",
|
||||
targetType: "TipoDeIva",
|
||||
targetId: newId.ToString(),
|
||||
metadata: new { entity.Codigo, entity.Porcentaje, entity.VigenciaDesde });
|
||||
|
||||
tx.Complete();
|
||||
|
||||
return TipoDeIvaMapper.ToDto(TipoDeIva.FromDb(
|
||||
id: newId,
|
||||
codigo: entity.Codigo,
|
||||
descripcion: entity.Descripcion,
|
||||
porcentaje: entity.Porcentaje,
|
||||
aplicaIVA: entity.AplicaIVA,
|
||||
activo: entity.Activo,
|
||||
vigenciaDesde: entity.VigenciaDesde,
|
||||
vigenciaHasta: entity.VigenciaHasta,
|
||||
predecesorId: entity.PredecesorId,
|
||||
fechaCreacion: DateTime.UtcNow,
|
||||
fechaModificacion: null));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
using FluentValidation;
|
||||
|
||||
namespace SIGCM2.Application.TiposDeIva.Create;
|
||||
|
||||
public sealed class CreateTipoDeIvaCommandValidator : AbstractValidator<CreateTipoDeIvaCommand>
|
||||
{
|
||||
private const int DescripcionMaxLength = 255;
|
||||
|
||||
public CreateTipoDeIvaCommandValidator()
|
||||
{
|
||||
RuleFor(x => x.Codigo)
|
||||
.NotEmpty().WithMessage("El código es requerido.")
|
||||
.Matches(@"^(EXENTO|NO_GRAVADO|IVA_\d+)$")
|
||||
.WithMessage("El código debe cumplir el formato EXENTO, NO_GRAVADO o IVA_{número}.");
|
||||
|
||||
RuleFor(x => x.Descripcion)
|
||||
.NotEmpty().WithMessage("La descripción es requerida.")
|
||||
.MaximumLength(DescripcionMaxLength)
|
||||
.WithMessage($"La descripción no puede superar los {DescripcionMaxLength} caracteres.");
|
||||
|
||||
RuleFor(x => x.Porcentaje)
|
||||
.InclusiveBetween(0m, 100m)
|
||||
.WithMessage("El porcentaje debe estar entre 0 y 100.");
|
||||
|
||||
RuleFor(x => x.VigenciaDesde)
|
||||
.NotEqual(default(DateOnly))
|
||||
.WithMessage("La fecha de vigencia desde es requerida.");
|
||||
|
||||
RuleFor(x => x.VigenciaHasta)
|
||||
.GreaterThanOrEqualTo(x => x.VigenciaDesde)
|
||||
.WithMessage("VigenciaHasta no puede ser anterior a VigenciaDesde.")
|
||||
.When(x => x.VigenciaHasta.HasValue);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
namespace SIGCM2.Application.TiposDeIva.Deactivate;
|
||||
|
||||
public sealed record DeactivateTipoDeIvaCommand(int Id);
|
||||
@@ -0,0 +1,46 @@
|
||||
using System.Transactions;
|
||||
using SIGCM2.Application.Abstractions;
|
||||
using SIGCM2.Application.Abstractions.Persistence;
|
||||
using SIGCM2.Application.Audit;
|
||||
using SIGCM2.Application.TiposDeIva.Dtos;
|
||||
using SIGCM2.Domain.Exceptions;
|
||||
|
||||
namespace SIGCM2.Application.TiposDeIva.Deactivate;
|
||||
|
||||
public sealed class DeactivateTipoDeIvaCommandHandler : ICommandHandler<DeactivateTipoDeIvaCommand, TipoDeIvaDto>
|
||||
{
|
||||
private readonly ITipoDeIvaRepository _repo;
|
||||
private readonly IAuditLogger _audit;
|
||||
|
||||
public DeactivateTipoDeIvaCommandHandler(ITipoDeIvaRepository repo, IAuditLogger audit)
|
||||
{
|
||||
_repo = repo;
|
||||
_audit = audit;
|
||||
}
|
||||
|
||||
public async Task<TipoDeIvaDto> Handle(DeactivateTipoDeIvaCommand command)
|
||||
{
|
||||
var entity = await _repo.GetByIdAsync(command.Id)
|
||||
?? throw new TipoDeIvaNotFoundException(command.Id);
|
||||
|
||||
// Idempotent: already inactive → return as-is without writing an audit event
|
||||
if (!entity.Activo)
|
||||
return TipoDeIvaMapper.ToDto(entity);
|
||||
|
||||
using var tx = new TransactionScope(
|
||||
TransactionScopeOption.Required,
|
||||
new TransactionOptions { IsolationLevel = System.Transactions.IsolationLevel.ReadCommitted },
|
||||
TransactionScopeAsyncFlowOption.Enabled);
|
||||
|
||||
await _repo.SetActivoAsync(command.Id, false);
|
||||
|
||||
await _audit.LogAsync(
|
||||
action: "tipo_iva.deactivate",
|
||||
targetType: "TipoDeIva",
|
||||
targetId: command.Id.ToString());
|
||||
|
||||
tx.Complete();
|
||||
|
||||
return TipoDeIvaMapper.ToDto(entity.Deactivate());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
namespace SIGCM2.Application.TiposDeIva.Dtos;
|
||||
|
||||
public sealed record HistorialCadenaDto(
|
||||
int Id,
|
||||
string Codigo,
|
||||
decimal Porcentaje,
|
||||
DateOnly VigenciaDesde,
|
||||
DateOnly? VigenciaHasta,
|
||||
int? PredecesorId,
|
||||
/// <summary>1-based index in the version chain (1 = root, N = current).</summary>
|
||||
int Version
|
||||
);
|
||||
@@ -0,0 +1,6 @@
|
||||
namespace SIGCM2.Application.TiposDeIva.Dtos;
|
||||
|
||||
public sealed record NuevaVersionResultDto(
|
||||
int PredecesoraId,
|
||||
int NuevaVersionId
|
||||
);
|
||||
15
src/api/SIGCM2.Application/TiposDeIva/Dtos/TipoDeIvaDto.cs
Normal file
15
src/api/SIGCM2.Application/TiposDeIva/Dtos/TipoDeIvaDto.cs
Normal file
@@ -0,0 +1,15 @@
|
||||
namespace SIGCM2.Application.TiposDeIva.Dtos;
|
||||
|
||||
public sealed record TipoDeIvaDto(
|
||||
int Id,
|
||||
string Codigo,
|
||||
string Descripcion,
|
||||
decimal Porcentaje,
|
||||
bool AplicaIVA,
|
||||
bool Activo,
|
||||
DateOnly VigenciaDesde,
|
||||
DateOnly? VigenciaHasta,
|
||||
int? PredecesorId,
|
||||
DateTime FechaCreacion,
|
||||
DateTime? FechaModificacion
|
||||
);
|
||||
@@ -0,0 +1,39 @@
|
||||
using SIGCM2.Domain.Entities;
|
||||
|
||||
namespace SIGCM2.Application.TiposDeIva.Dtos;
|
||||
|
||||
public static class TipoDeIvaMapper
|
||||
{
|
||||
public static TipoDeIvaDto ToDto(TipoDeIva entity) => new(
|
||||
Id: entity.Id,
|
||||
Codigo: entity.Codigo,
|
||||
Descripcion: entity.Descripcion,
|
||||
Porcentaje: entity.Porcentaje,
|
||||
AplicaIVA: entity.AplicaIVA,
|
||||
Activo: entity.Activo,
|
||||
VigenciaDesde: entity.VigenciaDesde,
|
||||
VigenciaHasta: entity.VigenciaHasta,
|
||||
PredecesorId: entity.PredecesorId,
|
||||
FechaCreacion: entity.FechaCreacion,
|
||||
FechaModificacion: entity.FechaModificacion
|
||||
);
|
||||
|
||||
public static IReadOnlyList<HistorialCadenaDto> ToHistorialChain(IReadOnlyList<TipoDeIva> chain)
|
||||
{
|
||||
var result = new List<HistorialCadenaDto>(chain.Count);
|
||||
for (var i = 0; i < chain.Count; i++)
|
||||
{
|
||||
var item = chain[i];
|
||||
result.Add(new HistorialCadenaDto(
|
||||
Id: item.Id,
|
||||
Codigo: item.Codigo,
|
||||
Porcentaje: item.Porcentaje,
|
||||
VigenciaDesde: item.VigenciaDesde,
|
||||
VigenciaHasta: item.VigenciaHasta,
|
||||
PredecesorId: item.PredecesorId,
|
||||
Version: i + 1
|
||||
));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
namespace SIGCM2.Application.TiposDeIva.GetById;
|
||||
|
||||
public sealed record GetTipoDeIvaByIdQuery(int Id);
|
||||
@@ -0,0 +1,24 @@
|
||||
using SIGCM2.Application.Abstractions;
|
||||
using SIGCM2.Application.Abstractions.Persistence;
|
||||
using SIGCM2.Application.TiposDeIva.Dtos;
|
||||
using SIGCM2.Domain.Exceptions;
|
||||
|
||||
namespace SIGCM2.Application.TiposDeIva.GetById;
|
||||
|
||||
public sealed class GetTipoDeIvaByIdQueryHandler : ICommandHandler<GetTipoDeIvaByIdQuery, TipoDeIvaDto>
|
||||
{
|
||||
private readonly ITipoDeIvaRepository _repo;
|
||||
|
||||
public GetTipoDeIvaByIdQueryHandler(ITipoDeIvaRepository repo)
|
||||
{
|
||||
_repo = repo;
|
||||
}
|
||||
|
||||
public async Task<TipoDeIvaDto> Handle(GetTipoDeIvaByIdQuery query)
|
||||
{
|
||||
var entity = await _repo.GetByIdAsync(query.Id)
|
||||
?? throw new TipoDeIvaNotFoundException(query.Id);
|
||||
|
||||
return TipoDeIvaMapper.ToDto(entity);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
namespace SIGCM2.Application.TiposDeIva.GetHistorial;
|
||||
|
||||
public sealed record GetHistorialTipoDeIvaQuery(int Id);
|
||||
@@ -0,0 +1,22 @@
|
||||
using SIGCM2.Application.Abstractions;
|
||||
using SIGCM2.Application.Abstractions.Persistence;
|
||||
using SIGCM2.Application.TiposDeIva.Dtos;
|
||||
|
||||
namespace SIGCM2.Application.TiposDeIva.GetHistorial;
|
||||
|
||||
public sealed class GetHistorialTipoDeIvaQueryHandler
|
||||
: ICommandHandler<GetHistorialTipoDeIvaQuery, IReadOnlyList<HistorialCadenaDto>>
|
||||
{
|
||||
private readonly ITipoDeIvaRepository _repo;
|
||||
|
||||
public GetHistorialTipoDeIvaQueryHandler(ITipoDeIvaRepository repo)
|
||||
{
|
||||
_repo = repo;
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyList<HistorialCadenaDto>> Handle(GetHistorialTipoDeIvaQuery query)
|
||||
{
|
||||
var chain = await _repo.GetHistorialAsync(query.Id);
|
||||
return TipoDeIvaMapper.ToHistorialChain(chain);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
namespace SIGCM2.Application.TiposDeIva.List;
|
||||
|
||||
public sealed record ListTiposDeIvaQuery(
|
||||
int Page,
|
||||
int PageSize,
|
||||
bool? Activo,
|
||||
string? Codigo);
|
||||
@@ -0,0 +1,30 @@
|
||||
using SIGCM2.Application.Abstractions;
|
||||
using SIGCM2.Application.Abstractions.Persistence;
|
||||
using SIGCM2.Application.Common;
|
||||
using SIGCM2.Application.TiposDeIva.Dtos;
|
||||
|
||||
namespace SIGCM2.Application.TiposDeIva.List;
|
||||
|
||||
public sealed class ListTiposDeIvaQueryHandler
|
||||
: ICommandHandler<ListTiposDeIvaQuery, PagedResult<TipoDeIvaDto>>
|
||||
{
|
||||
private readonly ITipoDeIvaRepository _repo;
|
||||
|
||||
public ListTiposDeIvaQueryHandler(ITipoDeIvaRepository repo)
|
||||
{
|
||||
_repo = repo;
|
||||
}
|
||||
|
||||
public async Task<PagedResult<TipoDeIvaDto>> Handle(ListTiposDeIvaQuery query)
|
||||
{
|
||||
var page = Math.Max(1, query.Page);
|
||||
var pageSize = Math.Clamp(query.PageSize, 1, 100);
|
||||
|
||||
var repoQuery = new TiposDeIvaQuery(page, pageSize, query.Activo, query.Codigo);
|
||||
var paged = await _repo.ListAsync(repoQuery);
|
||||
|
||||
var items = paged.Items.Select(TipoDeIvaMapper.ToDto).ToList();
|
||||
|
||||
return new PagedResult<TipoDeIvaDto>(items, paged.Page, paged.PageSize, paged.Total);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
namespace SIGCM2.Application.TiposDeIva.NuevaVersion;
|
||||
|
||||
public sealed record NuevaVersionTipoDeIvaCommand(
|
||||
int PredecesoraId,
|
||||
decimal NuevoPorcentaje,
|
||||
DateOnly VigenciaDesde);
|
||||
@@ -0,0 +1,72 @@
|
||||
using System.Transactions;
|
||||
using SIGCM2.Application.Abstractions;
|
||||
using SIGCM2.Application.Abstractions.Persistence;
|
||||
using SIGCM2.Application.Audit;
|
||||
using SIGCM2.Application.TiposDeIva.Dtos;
|
||||
using SIGCM2.Domain.Exceptions;
|
||||
|
||||
namespace SIGCM2.Application.TiposDeIva.NuevaVersion;
|
||||
|
||||
public sealed class NuevaVersionTipoDeIvaCommandHandler
|
||||
: ICommandHandler<NuevaVersionTipoDeIvaCommand, NuevaVersionResultDto>
|
||||
{
|
||||
private readonly ITipoDeIvaRepository _repo;
|
||||
private readonly IAuditLogger _audit;
|
||||
|
||||
public NuevaVersionTipoDeIvaCommandHandler(ITipoDeIvaRepository repo, IAuditLogger audit)
|
||||
{
|
||||
_repo = repo;
|
||||
_audit = audit;
|
||||
}
|
||||
|
||||
public async Task<NuevaVersionResultDto> Handle(NuevaVersionTipoDeIvaCommand command)
|
||||
{
|
||||
// Step 1: load predecesora
|
||||
var predecesora = await _repo.GetByIdAsync(command.PredecesoraId)
|
||||
?? throw new TipoDeIvaNotFoundException(command.PredecesoraId);
|
||||
|
||||
// Step 2: guard — predecesora must be open and active
|
||||
if (!predecesora.Activo || predecesora.VigenciaHasta is not null)
|
||||
throw new PredecesorYaCerradoException(command.PredecesoraId);
|
||||
|
||||
// Steps 3–4: delegate validation + tuple creation to domain (throws ArgumentException on invalid vigencia)
|
||||
var (predecesoraCerrada, nuevaVersion) = predecesora.NuevaVersion(
|
||||
command.NuevoPorcentaje,
|
||||
command.VigenciaDesde);
|
||||
|
||||
using var tx = new TransactionScope(
|
||||
TransactionScopeOption.Required,
|
||||
new TransactionOptions { IsolationLevel = System.Transactions.IsolationLevel.ReadCommitted },
|
||||
TransactionScopeAsyncFlowOption.Enabled);
|
||||
|
||||
// Step 5: optimistic close — race guard
|
||||
var closed = await _repo.UpdateCierreVigenciaAsync(
|
||||
command.PredecesoraId,
|
||||
predecesoraCerrada.VigenciaHasta!.Value);
|
||||
|
||||
if (!closed)
|
||||
throw new PredecesorYaCerradoException(command.PredecesoraId);
|
||||
|
||||
// Step 6: insert new version
|
||||
var nuevoId = await _repo.InsertAsync(nuevaVersion);
|
||||
|
||||
// Step 7: audit (fail-closed — if this throws, tx is NOT completed)
|
||||
await _audit.LogAsync(
|
||||
action: "tipo_iva.nueva_version",
|
||||
targetType: "TipoDeIva",
|
||||
targetId: nuevoId.ToString(),
|
||||
metadata: new
|
||||
{
|
||||
predecesoraId = command.PredecesoraId,
|
||||
nuevoId,
|
||||
porcentajeNuevo = command.NuevoPorcentaje,
|
||||
vigenciaDesde = command.VigenciaDesde,
|
||||
});
|
||||
|
||||
// Step 8: commit
|
||||
tx.Complete();
|
||||
|
||||
// Step 9: return result
|
||||
return new NuevaVersionResultDto(command.PredecesoraId, nuevoId);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
using FluentValidation;
|
||||
|
||||
namespace SIGCM2.Application.TiposDeIva.NuevaVersion;
|
||||
|
||||
public sealed class NuevaVersionTipoDeIvaCommandValidator : AbstractValidator<NuevaVersionTipoDeIvaCommand>
|
||||
{
|
||||
public NuevaVersionTipoDeIvaCommandValidator()
|
||||
{
|
||||
RuleFor(x => x.PredecesoraId)
|
||||
.GreaterThan(0).WithMessage("El id de la predecesora debe ser mayor a 0.");
|
||||
|
||||
RuleFor(x => x.NuevoPorcentaje)
|
||||
.InclusiveBetween(0m, 100m)
|
||||
.WithMessage("El nuevo porcentaje debe estar entre 0 y 100.");
|
||||
|
||||
RuleFor(x => x.VigenciaDesde)
|
||||
.NotEqual(default(DateOnly))
|
||||
.WithMessage("La fecha de vigencia desde es requerida.");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
namespace SIGCM2.Application.TiposDeIva.Reactivate;
|
||||
|
||||
public sealed record ReactivateTipoDeIvaCommand(int Id);
|
||||
@@ -0,0 +1,46 @@
|
||||
using System.Transactions;
|
||||
using SIGCM2.Application.Abstractions;
|
||||
using SIGCM2.Application.Abstractions.Persistence;
|
||||
using SIGCM2.Application.Audit;
|
||||
using SIGCM2.Application.TiposDeIva.Dtos;
|
||||
using SIGCM2.Domain.Exceptions;
|
||||
|
||||
namespace SIGCM2.Application.TiposDeIva.Reactivate;
|
||||
|
||||
public sealed class ReactivateTipoDeIvaCommandHandler : ICommandHandler<ReactivateTipoDeIvaCommand, TipoDeIvaDto>
|
||||
{
|
||||
private readonly ITipoDeIvaRepository _repo;
|
||||
private readonly IAuditLogger _audit;
|
||||
|
||||
public ReactivateTipoDeIvaCommandHandler(ITipoDeIvaRepository repo, IAuditLogger audit)
|
||||
{
|
||||
_repo = repo;
|
||||
_audit = audit;
|
||||
}
|
||||
|
||||
public async Task<TipoDeIvaDto> Handle(ReactivateTipoDeIvaCommand command)
|
||||
{
|
||||
var entity = await _repo.GetByIdAsync(command.Id)
|
||||
?? throw new TipoDeIvaNotFoundException(command.Id);
|
||||
|
||||
// Idempotent: already active → return as-is without writing an audit event
|
||||
if (entity.Activo)
|
||||
return TipoDeIvaMapper.ToDto(entity);
|
||||
|
||||
using var tx = new TransactionScope(
|
||||
TransactionScopeOption.Required,
|
||||
new TransactionOptions { IsolationLevel = System.Transactions.IsolationLevel.ReadCommitted },
|
||||
TransactionScopeAsyncFlowOption.Enabled);
|
||||
|
||||
await _repo.SetActivoAsync(command.Id, true);
|
||||
|
||||
await _audit.LogAsync(
|
||||
action: "tipo_iva.reactivate",
|
||||
targetType: "TipoDeIva",
|
||||
targetId: command.Id.ToString());
|
||||
|
||||
tx.Complete();
|
||||
|
||||
return TipoDeIvaMapper.ToDto(entity.Reactivate());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
namespace SIGCM2.Application.TiposDeIva.Update;
|
||||
|
||||
/// <summary>
|
||||
/// Updates only cosmetic fields: Codigo, Descripcion, AplicaIVA, Activo.
|
||||
/// Porcentaje is NOT part of this command — it is immutable and can only change via NuevaVersion.
|
||||
/// </summary>
|
||||
public sealed record UpdateTipoDeIvaCommand(
|
||||
int Id,
|
||||
string Codigo,
|
||||
string Descripcion,
|
||||
bool AplicaIVA,
|
||||
bool Activo);
|
||||
@@ -0,0 +1,62 @@
|
||||
using System.Transactions;
|
||||
using SIGCM2.Application.Abstractions;
|
||||
using SIGCM2.Application.Abstractions.Persistence;
|
||||
using SIGCM2.Application.Audit;
|
||||
using SIGCM2.Application.TiposDeIva.Dtos;
|
||||
using SIGCM2.Domain.Exceptions;
|
||||
|
||||
namespace SIGCM2.Application.TiposDeIva.Update;
|
||||
|
||||
public sealed class UpdateTipoDeIvaCommandHandler : ICommandHandler<UpdateTipoDeIvaCommand, TipoDeIvaDto>
|
||||
{
|
||||
private readonly ITipoDeIvaRepository _repo;
|
||||
private readonly IAuditLogger _audit;
|
||||
|
||||
public UpdateTipoDeIvaCommandHandler(ITipoDeIvaRepository repo, IAuditLogger audit)
|
||||
{
|
||||
_repo = repo;
|
||||
_audit = audit;
|
||||
}
|
||||
|
||||
public async Task<TipoDeIvaDto> Handle(UpdateTipoDeIvaCommand command)
|
||||
{
|
||||
var entity = await _repo.GetByIdAsync(command.Id)
|
||||
?? throw new TipoDeIvaNotFoundException(command.Id);
|
||||
|
||||
var updated = entity
|
||||
.WithCodigo(command.Codigo)
|
||||
.WithDescripcion(command.Descripcion)
|
||||
.WithAplicaIVA(command.AplicaIVA);
|
||||
|
||||
// Apply Activo change if needed
|
||||
updated = command.Activo
|
||||
? updated.Reactivate()
|
||||
: updated.Deactivate();
|
||||
|
||||
using var tx = new TransactionScope(
|
||||
TransactionScopeOption.Required,
|
||||
new TransactionOptions { IsolationLevel = System.Transactions.IsolationLevel.ReadCommitted },
|
||||
TransactionScopeAsyncFlowOption.Enabled);
|
||||
|
||||
await _repo.UpdateCosmeticoAsync(
|
||||
command.Id,
|
||||
command.Codigo,
|
||||
command.Descripcion,
|
||||
command.AplicaIVA,
|
||||
command.Activo);
|
||||
|
||||
await _audit.LogAsync(
|
||||
action: "tipo_iva.update",
|
||||
targetType: "TipoDeIva",
|
||||
targetId: command.Id.ToString(),
|
||||
metadata: new
|
||||
{
|
||||
before = new { entity.Codigo, entity.Descripcion, entity.AplicaIVA, entity.Activo },
|
||||
after = new { command.Codigo, command.Descripcion, command.AplicaIVA, command.Activo },
|
||||
});
|
||||
|
||||
tx.Complete();
|
||||
|
||||
return TipoDeIvaMapper.ToDto(updated);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
using FluentValidation;
|
||||
|
||||
namespace SIGCM2.Application.TiposDeIva.Update;
|
||||
|
||||
public sealed class UpdateTipoDeIvaCommandValidator : AbstractValidator<UpdateTipoDeIvaCommand>
|
||||
{
|
||||
private const int DescripcionMaxLength = 255;
|
||||
|
||||
public UpdateTipoDeIvaCommandValidator()
|
||||
{
|
||||
RuleFor(x => x.Id)
|
||||
.GreaterThan(0).WithMessage("El id debe ser mayor a 0.");
|
||||
|
||||
RuleFor(x => x.Codigo)
|
||||
.NotEmpty().WithMessage("El código es requerido.")
|
||||
.Matches(@"^(EXENTO|NO_GRAVADO|IVA_\d+)$")
|
||||
.WithMessage("El código debe cumplir el formato EXENTO, NO_GRAVADO o IVA_{número}.");
|
||||
|
||||
RuleFor(x => x.Descripcion)
|
||||
.NotEmpty().WithMessage("La descripción es requerida.")
|
||||
.MaximumLength(DescripcionMaxLength)
|
||||
.WithMessage($"La descripción no puede superar los {DescripcionMaxLength} caracteres.");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user