feat(application): IProductTypeRepository + IProductQueryRepository stub + queries (PRD-001)

This commit is contained in:
2026-04-19 09:38:51 -03:00
parent 132d17c99f
commit 3c9e852379
13 changed files with 375 additions and 0 deletions

View File

@@ -0,0 +1,15 @@
namespace SIGCM2.Application.Abstractions.Persistence;
/// <summary>
/// PRD-002 handoff contract — query-only access to Product data needed by ProductType handlers.
/// PRD-001 binds to NullProductQueryRepository (always returns false).
/// PRD-002 binds to Dapper impl against dbo.Product (when that table exists).
/// </summary>
public interface IProductQueryRepository
{
/// <summary>
/// Returns true if at least one active Product with the given ProductTypeId exists.
/// Used by DeactivateProductTypeCommandHandler to guard against orphaning active products.
/// </summary>
Task<bool> ExistsActiveByProductTypeAsync(int productTypeId, CancellationToken ct = default);
}

View File

@@ -0,0 +1,31 @@
using SIGCM2.Application.Common;
using SIGCM2.Domain.Entities;
namespace SIGCM2.Application.Abstractions.Persistence;
/// <summary>
/// Write-side repository for ProductType.
/// All reads needed by write handlers are included here.
/// Query-side (for listing, filtering) uses GetPagedAsync with ProductTypesQuery.
/// </summary>
public interface IProductTypeRepository
{
/// <summary>Inserts a new ProductType and returns the DB-assigned Id.</summary>
Task<int> AddAsync(ProductType productType, CancellationToken ct = default);
/// <summary>Returns the ProductType with the given Id, or null if not found.</summary>
Task<ProductType?> GetByIdAsync(int id, CancellationToken ct = default);
/// <summary>Returns a paged result of ProductTypes matching the query.</summary>
Task<PagedResult<ProductType>> GetPagedAsync(ProductTypesQuery query, CancellationToken ct = default);
/// <summary>Persists all changes to an existing ProductType row.</summary>
Task UpdateAsync(ProductType productType, CancellationToken ct = default);
/// <summary>
/// Returns true if an active ProductType with the given nombre exists.
/// Pass excludeId to skip the self-comparison during rename (update scenario).
/// Case-insensitive — delegates to DB collation (SQL_Latin1_General_CP1_CI_AI).
/// </summary>
Task<bool> ExistsByNombreAsync(string nombre, int? excludeId = null, CancellationToken ct = default);
}

View File

@@ -0,0 +1,10 @@
namespace SIGCM2.Application.Common;
/// <summary>
/// Query parameters for listing ProductTypes (used by IProductTypeRepository.GetPagedAsync).
/// </summary>
public sealed record ProductTypesQuery(
int Page = 1,
int PageSize = 20,
bool? Activo = true,
string? Search = null);

View File

@@ -0,0 +1,3 @@
namespace SIGCM2.Application.ProductTypes.GetById;
public sealed record GetProductTypeByIdQuery(int Id);

View File

@@ -0,0 +1,30 @@
using SIGCM2.Application.Abstractions;
using SIGCM2.Application.Abstractions.Persistence;
using SIGCM2.Domain.Exceptions;
namespace SIGCM2.Application.ProductTypes.GetById;
public sealed class GetProductTypeByIdQueryHandler
: ICommandHandler<GetProductTypeByIdQuery, ProductTypeDetailDto>
{
private readonly IProductTypeRepository _repo;
public GetProductTypeByIdQueryHandler(IProductTypeRepository repo)
{
_repo = repo;
}
public async Task<ProductTypeDetailDto> Handle(GetProductTypeByIdQuery query)
{
var pt = await _repo.GetByIdAsync(query.Id)
?? throw new ProductTypeNotFoundException(query.Id);
return new ProductTypeDetailDto(
pt.Id, pt.Nombre,
pt.HasDuration, pt.RequiresText, pt.RequiresCategory, pt.IsBundle,
pt.AllowImages,
pt.MaxImages, pt.MaxImageSizeMB, pt.MaxImageWidth, pt.MaxImageHeight,
pt.IsActive,
pt.FechaCreacion, pt.FechaModificacion);
}
}

View File

@@ -0,0 +1,17 @@
namespace SIGCM2.Application.ProductTypes.GetById;
public sealed record ProductTypeDetailDto(
int Id,
string Nombre,
bool HasDuration,
bool RequiresText,
bool RequiresCategory,
bool IsBundle,
bool AllowImages,
int? MaxImages,
decimal? MaxImageSizeMB,
int? MaxImageWidth,
int? MaxImageHeight,
bool IsActive,
DateTime FechaCreacion,
DateTime? FechaModificacion);

View File

@@ -0,0 +1,7 @@
namespace SIGCM2.Application.ProductTypes.List;
public sealed record ListProductTypesQuery(
int Page = 1,
int PageSize = 20,
bool? Activo = true,
string? Search = null);

View File

@@ -0,0 +1,32 @@
using SIGCM2.Application.Abstractions;
using SIGCM2.Application.Abstractions.Persistence;
using SIGCM2.Application.Common;
namespace SIGCM2.Application.ProductTypes.List;
public sealed class ListProductTypesQueryHandler
: ICommandHandler<ListProductTypesQuery, PagedResult<ProductTypeListItemDto>>
{
private readonly IProductTypeRepository _repo;
public ListProductTypesQueryHandler(IProductTypeRepository repo)
{
_repo = repo;
}
public async Task<PagedResult<ProductTypeListItemDto>> Handle(ListProductTypesQuery query)
{
var page = Math.Max(1, query.Page);
var pageSize = Math.Clamp(query.PageSize, 1, 100);
var repoQuery = new ProductTypesQuery(page, pageSize, query.Activo, query.Search);
var paged = await _repo.GetPagedAsync(repoQuery);
var items = paged.Items.Select(p => new ProductTypeListItemDto(
p.Id, p.Nombre,
p.HasDuration, p.RequiresText, p.RequiresCategory, p.IsBundle,
p.AllowImages, p.IsActive)).ToList();
return new PagedResult<ProductTypeListItemDto>(items, paged.Page, paged.PageSize, paged.Total);
}
}

View File

@@ -0,0 +1,11 @@
namespace SIGCM2.Application.ProductTypes.List;
public sealed record ProductTypeListItemDto(
int Id,
string Nombre,
bool HasDuration,
bool RequiresText,
bool RequiresCategory,
bool IsBundle,
bool AllowImages,
bool IsActive);

View File

@@ -0,0 +1,14 @@
using SIGCM2.Application.Abstractions.Persistence;
namespace SIGCM2.Application.Products;
/// <summary>
/// STUB — PRD-002 replaces the DI binding with a real Dapper impl against dbo.Product.
/// Returns false for all queries so DeactivateProductTypeCommandHandler guard always passes.
/// This is intentional for PRD-001: the mechanism is installed; the data feed arrives in PRD-002.
/// </summary>
public sealed class NullProductQueryRepository : IProductQueryRepository
{
public Task<bool> ExistsActiveByProductTypeAsync(int productTypeId, CancellationToken ct = default)
=> Task.FromResult(false);
}