feat(application): IProductTypeRepository + IProductQueryRepository stub + queries (PRD-001)
This commit is contained in:
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
10
src/api/SIGCM2.Application/Common/ProductTypesQuery.cs
Normal file
10
src/api/SIGCM2.Application/Common/ProductTypesQuery.cs
Normal 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);
|
||||
@@ -0,0 +1,3 @@
|
||||
namespace SIGCM2.Application.ProductTypes.GetById;
|
||||
|
||||
public sealed record GetProductTypeByIdQuery(int Id);
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
}
|
||||
Reference in New Issue
Block a user