diff --git a/src/APITemplate/Api/Extensions/ApiServiceCollectionExtensions.cs b/src/APITemplate/Api/Extensions/ApiServiceCollectionExtensions.cs index e0814f34..b23b520a 100644 --- a/src/APITemplate/Api/Extensions/ApiServiceCollectionExtensions.cs +++ b/src/APITemplate/Api/Extensions/ApiServiceCollectionExtensions.cs @@ -4,7 +4,7 @@ using SharedKernel.Infrastructure.Health; using StackExchange.Redis; using IdentityCacheTags = Identity.Events.CacheTags; -using ProductCatalogCacheTags = ProductCatalog.Events.CacheTags; +using ProductCatalogCacheTags = ProductCatalog.Common.Events.CacheTags; using ReviewsCacheTags = Reviews.Common.Events.CacheTags; namespace APITemplate.Api.Extensions; diff --git a/src/APITemplate/Api/Program.cs b/src/APITemplate/Api/Program.cs index 281f5d9c..8efa39ce 100644 --- a/src/APITemplate/Api/Program.cs +++ b/src/APITemplate/Api/Program.cs @@ -10,7 +10,7 @@ using JasperFx; using JasperFx.Resources; using ProductCatalog; -using ProductCatalog.Features.Product; +using ProductCatalog.Features.CreateProducts; using ProductCatalog.Handlers; using Reviews; using Reviews.Features; diff --git a/src/Modules/ProductCatalog/Errors/DomainErrors.cs b/src/Modules/ProductCatalog/Common/Errors/DomainErrors.cs similarity index 96% rename from src/Modules/ProductCatalog/Errors/DomainErrors.cs rename to src/Modules/ProductCatalog/Common/Errors/DomainErrors.cs index 9ea458a0..13ed219a 100644 --- a/src/Modules/ProductCatalog/Errors/DomainErrors.cs +++ b/src/Modules/ProductCatalog/Common/Errors/DomainErrors.cs @@ -1,6 +1,6 @@ using ErrorOr; -namespace ProductCatalog.Errors; +namespace ProductCatalog.Common.Errors; public static class DomainErrors { @@ -40,4 +40,3 @@ public static Error NotFound(Guid id) => ); } } - diff --git a/src/Modules/ProductCatalog/Errors/ErrorCatalog.cs b/src/Modules/ProductCatalog/Common/Errors/ErrorCatalog.cs similarity index 97% rename from src/Modules/ProductCatalog/Errors/ErrorCatalog.cs rename to src/Modules/ProductCatalog/Common/Errors/ErrorCatalog.cs index bf56d7e0..b91a3aa4 100644 --- a/src/Modules/ProductCatalog/Errors/ErrorCatalog.cs +++ b/src/Modules/ProductCatalog/Common/Errors/ErrorCatalog.cs @@ -1,4 +1,4 @@ -namespace ProductCatalog.Errors; +namespace ProductCatalog.Common.Errors; public static class ErrorCatalog { @@ -36,4 +36,3 @@ public static class Categories "Duplicate category ID '{0}' appears multiple times in the request."; } } - diff --git a/src/Modules/ProductCatalog/Errors/ProductCatalogDomainErrors.cs b/src/Modules/ProductCatalog/Common/Errors/ProductCatalogDomainErrors.cs similarity index 85% rename from src/Modules/ProductCatalog/Errors/ProductCatalogDomainErrors.cs rename to src/Modules/ProductCatalog/Common/Errors/ProductCatalogDomainErrors.cs index 355ecd3c..456a31a8 100644 --- a/src/Modules/ProductCatalog/Errors/ProductCatalogDomainErrors.cs +++ b/src/Modules/ProductCatalog/Common/Errors/ProductCatalogDomainErrors.cs @@ -1,6 +1,6 @@ using ErrorOr; -namespace ProductCatalog.Errors; +namespace ProductCatalog.Common.Errors; internal static class ProductCatalogDomainErrors { @@ -10,4 +10,3 @@ internal static Error NegativePrice() => Error.Validation("PC-0400", "Price cannot be negative."); } } - diff --git a/src/Modules/ProductCatalog/Events/CacheTags.cs b/src/Modules/ProductCatalog/Common/Events/CacheTags.cs similarity index 89% rename from src/Modules/ProductCatalog/Events/CacheTags.cs rename to src/Modules/ProductCatalog/Common/Events/CacheTags.cs index f2c67896..0a8ea50c 100644 --- a/src/Modules/ProductCatalog/Events/CacheTags.cs +++ b/src/Modules/ProductCatalog/Common/Events/CacheTags.cs @@ -1,4 +1,4 @@ -namespace ProductCatalog.Events; +namespace ProductCatalog.Common.Events; public static class CacheTags { @@ -8,4 +8,3 @@ public static class CacheTags /// Reviews cache is also invalidated when products are deleted (orphaned reviews). public const string Reviews = "Reviews"; } - diff --git a/src/Modules/ProductCatalog/Controllers/V1/CategoriesController.cs b/src/Modules/ProductCatalog/Controllers/V1/CategoriesController.cs deleted file mode 100644 index 0cc50c7f..00000000 --- a/src/Modules/ProductCatalog/Controllers/V1/CategoriesController.cs +++ /dev/null @@ -1,111 +0,0 @@ -using Asp.Versioning; -using ErrorOr; -using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.OutputCaching; -using SharedKernel.Contracts.Api; -using SharedKernel.Contracts.Security; -using Wolverine; - -namespace ProductCatalog.Controllers.V1; - -[ApiVersion(1.0)] -/// -/// Presentation-layer controller that exposes CRUD endpoints for product categories, -/// including a stored-procedure-backed statistics query. -/// -public sealed class CategoriesController(IMessageBus bus) : ApiControllerBase -{ - /// - /// Returns a paginated, filterable list of categories from the output cache. - /// - [HttpGet] - [RequirePermission(Permission.Categories.Read)] - [OutputCache(PolicyName = CacheTags.Categories)] - public async Task>> GetAll( - [FromQuery] CategoryFilter filter, - CancellationToken ct - ) - { - ErrorOr> result = await bus.InvokeAsync< - ErrorOr> - >(new GetCategoriesQuery(filter), ct); - return result.ToActionResult(this); - } - - /// Returns a single category by its identifier, or 404 if not found. - [HttpGet("{id:guid}")] - [RequirePermission(Permission.Categories.Read)] - [OutputCache(PolicyName = CacheTags.Categories)] - public async Task> GetById(Guid id, CancellationToken ct) - { - ErrorOr result = await bus.InvokeAsync>( - new GetCategoryByIdQuery(id), - ct - ); - return result.ToActionResult(this); - } - - /// Creates multiple categories in a single batch operation. - [HttpPost] - [RequirePermission(Permission.Categories.Create)] - public async Task> Create( - CreateCategoriesRequest request, - CancellationToken ct - ) - { - ErrorOr result = await bus.InvokeAsync>( - new CreateCategoriesCommand(request), - ct - ); - return result.ToBatchResult(this); - } - - /// Updates multiple categories in a single batch operation. - [HttpPut] - [RequirePermission(Permission.Categories.Update)] - public async Task> Update( - UpdateCategoriesRequest request, - CancellationToken ct - ) - { - ErrorOr result = await bus.InvokeAsync>( - new UpdateCategoriesCommand(request), - ct - ); - return result.ToBatchResult(this); - } - - /// Soft-deletes multiple categories in a single batch operation. - [HttpDelete] - [RequirePermission(Permission.Categories.Delete)] - public async Task> Delete( - BatchDeleteRequest request, - CancellationToken ct - ) - { - ErrorOr result = await bus.InvokeAsync>( - new DeleteCategoriesCommand(request), - ct - ); - return result.ToBatchResult(this); - } - - /// - /// Returns aggregated statistics for a category by calling the - /// get_product_category_stats(p_category_id) stored procedure via EF Core FromSql. - /// - [HttpGet("{id:guid}/stats")] - [RequirePermission(Permission.Categories.Read)] - [OutputCache(PolicyName = CacheTags.Categories)] - public async Task> GetStats( - Guid id, - CancellationToken ct - ) - { - ErrorOr result = await bus.InvokeAsync< - ErrorOr - >(new GetCategoryStatsQuery(id), ct); - return result.ToActionResult(this); - } -} - diff --git a/src/Modules/ProductCatalog/Controllers/V1/ProductDataController.cs b/src/Modules/ProductCatalog/Controllers/V1/ProductDataController.cs deleted file mode 100644 index 1dbc44c6..00000000 --- a/src/Modules/ProductCatalog/Controllers/V1/ProductDataController.cs +++ /dev/null @@ -1,89 +0,0 @@ -using Asp.Versioning; -using ErrorOr; -using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.OutputCaching; -using SharedKernel.Contracts.Api; -using SharedKernel.Contracts.Security; -using Wolverine; - -namespace ProductCatalog.Controllers.V1; - -[ApiVersion(1.0)] -[Route("api/v{version:apiVersion}/product-data")] -/// -/// Presentation-layer controller that manages product supplementary data (images and videos) -/// stored in MongoDB, with output-cache integration for read endpoints. -/// -public sealed class ProductDataController(IMessageBus bus) : ApiControllerBase -{ - /// Returns all product data documents, optionally filtered by type. - [HttpGet] - [RequirePermission(Permission.ProductData.Read)] - [OutputCache(PolicyName = CacheTags.ProductData)] - public async Task>> GetAll( - [FromQuery] string? type, - CancellationToken ct - ) - { - ErrorOr> result = await bus.InvokeAsync< - ErrorOr> - >(new GetProductDataQuery(type), ct); - return result.ToActionResult(this); - } - - /// Returns a single product data document by its identifier, or 404 if not found. - [HttpGet("{id:guid}")] - [RequirePermission(Permission.ProductData.Read)] - [OutputCache(PolicyName = CacheTags.ProductData)] - public async Task> GetById(Guid id, CancellationToken ct) - { - ErrorOr result = await bus.InvokeAsync>( - new GetProductDataByIdQuery(id), - ct - ); - return result.ToActionResult(this); - } - - /// Creates a new image product-data document and returns it with a 201 Location header. - [HttpPost("image")] - [RequirePermission(Permission.ProductData.Create)] - public async Task> CreateImage( - CreateImageProductDataRequest request, - CancellationToken ct - ) - { - ErrorOr result = await bus.InvokeAsync>( - new CreateImageProductDataCommand(request), - ct - ); - return result.ToCreatedResult(this, v => new { id = v.Id, version = this.GetApiVersion() }); - } - - /// Creates a new video product-data document and returns it with a 201 Location header. - [HttpPost("video")] - [RequirePermission(Permission.ProductData.Create)] - public async Task> CreateVideo( - CreateVideoProductDataRequest request, - CancellationToken ct - ) - { - ErrorOr result = await bus.InvokeAsync>( - new CreateVideoProductDataCommand(request), - ct - ); - return result.ToCreatedResult(this, v => new { id = v.Id, version = this.GetApiVersion() }); - } - - /// Deletes a product data document by its identifier. - [HttpDelete("{id:guid}")] - [RequirePermission(Permission.ProductData.Delete)] - public async Task Delete(Guid id, CancellationToken ct) - { - ErrorOr result = await bus.InvokeAsync>( - new DeleteProductDataCommand(id), - ct - ); - return result.ToNoContentResult(this); - } -} - diff --git a/src/Modules/ProductCatalog/Controllers/V1/ProductsController.cs b/src/Modules/ProductCatalog/Controllers/V1/ProductsController.cs deleted file mode 100644 index 1c6c3045..00000000 --- a/src/Modules/ProductCatalog/Controllers/V1/ProductsController.cs +++ /dev/null @@ -1,92 +0,0 @@ -using Asp.Versioning; -using ErrorOr; -using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.OutputCaching; -using SharedKernel.Contracts.Api; -using SharedKernel.Contracts.Security; -using Wolverine; - -namespace ProductCatalog.Controllers.V1; - -/// -/// Presentation-layer controller that exposes full CRUD endpoints for the product catalog, -/// with permission-based authorization and tenant-aware output caching. -/// -[ApiVersion(1.0)] -public sealed class ProductsController(IMessageBus bus) : ApiControllerBase -{ - /// Returns a filtered, paginated product list including search facets. - [HttpGet] - [RequirePermission(Permission.Products.Read)] - [OutputCache(PolicyName = CacheTags.Products)] - public async Task> GetAll( - [FromQuery] ProductFilter filter, - CancellationToken ct - ) - { - ErrorOr result = await bus.InvokeAsync>( - new GetProductsQuery(filter), - ct - ); - return result.ToActionResult(this); - } - - /// Returns a single product by its identifier, or 404 if not found. - [HttpGet("{id:guid}")] - [RequirePermission(Permission.Products.Read)] - [OutputCache(PolicyName = CacheTags.Products)] - public async Task> GetById(Guid id, CancellationToken ct) - { - ErrorOr result = await bus.InvokeAsync>( - new GetProductByIdQuery(id), - ct - ); - return result.ToActionResult(this); - } - - /// Creates multiple products in a single batch operation. - [HttpPost] - [RequirePermission(Permission.Products.Create)] - public async Task> Create( - CreateProductsRequest request, - CancellationToken ct - ) - { - ErrorOr result = await bus.InvokeAsync>( - new CreateProductsCommand(request), - ct - ); - return result.ToBatchResult(this); - } - - /// Updates multiple products in a single batch operation. - [HttpPut] - [RequirePermission(Permission.Products.Update)] - public async Task> Update( - UpdateProductsRequest request, - CancellationToken ct - ) - { - ErrorOr result = await bus.InvokeAsync>( - new UpdateProductsCommand(request), - ct - ); - return result.ToBatchResult(this); - } - - /// Soft-deletes multiple products in a single batch operation. - [HttpDelete] - [RequirePermission(Permission.Products.Delete)] - public async Task> Delete( - BatchDeleteRequest request, - CancellationToken ct - ) - { - ErrorOr result = await bus.InvokeAsync>( - new DeleteProductsCommand(request), - ct - ); - return result.ToBatchResult(this); - } -} - diff --git a/src/Modules/ProductCatalog/Features/Category/Commands/CreateCategoriesCommand.cs b/src/Modules/ProductCatalog/Features/CreateCategories/CreateCategoriesCommand.cs similarity index 97% rename from src/Modules/ProductCatalog/Features/Category/Commands/CreateCategoriesCommand.cs rename to src/Modules/ProductCatalog/Features/CreateCategories/CreateCategoriesCommand.cs index 55302604..08c4b237 100644 --- a/src/Modules/ProductCatalog/Features/Category/Commands/CreateCategoriesCommand.cs +++ b/src/Modules/ProductCatalog/Features/CreateCategories/CreateCategoriesCommand.cs @@ -5,7 +5,7 @@ using Wolverine; using CategoryEntity = ProductCatalog.Entities.Category; -namespace ProductCatalog.Features.Category; +namespace ProductCatalog.Features.CreateCategories; /// Creates multiple categories in a single batch operation. public sealed record CreateCategoriesCommand(CreateCategoriesRequest Request); @@ -68,4 +68,3 @@ await unitOfWork.ExecuteInTransactionAsync( return (new BatchResponse([], command.Request.Items.Count, 0), messages); } } - diff --git a/src/Modules/ProductCatalog/Features/CreateCategories/CreateCategoriesController.cs b/src/Modules/ProductCatalog/Features/CreateCategories/CreateCategoriesController.cs new file mode 100644 index 00000000..2cd7ad15 --- /dev/null +++ b/src/Modules/ProductCatalog/Features/CreateCategories/CreateCategoriesController.cs @@ -0,0 +1,27 @@ +using Asp.Versioning; +using ErrorOr; +using Microsoft.AspNetCore.Mvc; +using SharedKernel.Contracts.Api; +using SharedKernel.Contracts.Security; +using Wolverine; + +namespace ProductCatalog.Features.CreateCategories; + +[ApiVersion(1.0)] +public sealed class CreateCategoriesController(IMessageBus bus) : ApiControllerBase +{ + /// Creates multiple categories in a single batch operation. + [HttpPost] + [RequirePermission(Permission.Categories.Create)] + public async Task> Create( + CreateCategoriesRequest request, + CancellationToken ct + ) + { + ErrorOr result = await bus.InvokeAsync>( + new CreateCategoriesCommand(request), + ct + ); + return result.ToBatchResult(this); + } +} diff --git a/src/Modules/ProductCatalog/Features/Category/DTOs/CreateCategoriesRequest.cs b/src/Modules/ProductCatalog/Features/CreateCategories/CreateCategoriesRequest.cs similarity index 89% rename from src/Modules/ProductCatalog/Features/Category/DTOs/CreateCategoriesRequest.cs rename to src/Modules/ProductCatalog/Features/CreateCategories/CreateCategoriesRequest.cs index 253cac1a..36e380b9 100644 --- a/src/Modules/ProductCatalog/Features/Category/DTOs/CreateCategoriesRequest.cs +++ b/src/Modules/ProductCatalog/Features/CreateCategories/CreateCategoriesRequest.cs @@ -1,6 +1,6 @@ using System.ComponentModel.DataAnnotations; -namespace ProductCatalog.Features.Category.DTOs; +namespace ProductCatalog.Features.CreateCategories; /// /// Carries a list of category items to be created in a single batch operation; accepts between 1 and 100 items. @@ -10,6 +10,3 @@ public sealed record CreateCategoriesRequest( [MaxLength(100, ErrorMessage = "Maximum 100 items per batch.")] IReadOnlyList Items ); - - - diff --git a/src/Modules/ProductCatalog/Features/Category/DTOs/CreateCategoryRequest.cs b/src/Modules/ProductCatalog/Features/CreateCategories/CreateCategoryRequest.cs similarity index 89% rename from src/Modules/ProductCatalog/Features/Category/DTOs/CreateCategoryRequest.cs rename to src/Modules/ProductCatalog/Features/CreateCategories/CreateCategoryRequest.cs index ed4c7ac2..d6c7634f 100644 --- a/src/Modules/ProductCatalog/Features/Category/DTOs/CreateCategoryRequest.cs +++ b/src/Modules/ProductCatalog/Features/CreateCategories/CreateCategoryRequest.cs @@ -1,7 +1,7 @@ using System.ComponentModel.DataAnnotations; using SharedKernel.Application.Validation; -namespace ProductCatalog.Features.Category.DTOs; +namespace ProductCatalog.Features.CreateCategories; /// /// Payload for creating a new category, carrying the name and optional description. @@ -12,6 +12,3 @@ public sealed record CreateCategoryRequest( string Name, string? Description ); - - - diff --git a/src/Modules/ProductCatalog/Features/Category/Validation/CreateCategoryRequestValidator.cs b/src/Modules/ProductCatalog/Features/CreateCategories/CreateCategoryRequestValidator.cs similarity index 83% rename from src/Modules/ProductCatalog/Features/Category/Validation/CreateCategoryRequestValidator.cs rename to src/Modules/ProductCatalog/Features/CreateCategories/CreateCategoryRequestValidator.cs index edb79a09..749b42b9 100644 --- a/src/Modules/ProductCatalog/Features/Category/Validation/CreateCategoryRequestValidator.cs +++ b/src/Modules/ProductCatalog/Features/CreateCategories/CreateCategoryRequestValidator.cs @@ -1,12 +1,9 @@ using SharedKernel.Application.Validation; -namespace ProductCatalog.Features.Category.Validation; +namespace ProductCatalog.Features.CreateCategories; /// /// FluentValidation validator for that enforces data-annotation constraints. /// public sealed class CreateCategoryRequestValidator : DataAnnotationsValidator; - - - diff --git a/src/Modules/ProductCatalog/Features/ProductData/Commands/CreateImageProductDataCommand.cs b/src/Modules/ProductCatalog/Features/CreateImageProductData/CreateImageProductDataCommand.cs similarity index 93% rename from src/Modules/ProductCatalog/Features/ProductData/Commands/CreateImageProductDataCommand.cs rename to src/Modules/ProductCatalog/Features/CreateImageProductData/CreateImageProductDataCommand.cs index d13ca25a..20341c9c 100644 --- a/src/Modules/ProductCatalog/Features/ProductData/Commands/CreateImageProductDataCommand.cs +++ b/src/Modules/ProductCatalog/Features/CreateImageProductData/CreateImageProductDataCommand.cs @@ -1,12 +1,11 @@ using ErrorOr; using ProductCatalog.Entities; -using ProductCatalog.Features.ProductData.Mappings; using ProductCatalog.Interfaces; using SharedKernel.Application.Context; using SharedKernel.Contracts.Events; using Wolverine; -namespace ProductCatalog.Features.ProductData; +namespace ProductCatalog.Features.CreateImageProductData; public sealed record CreateImageProductDataCommand(CreateImageProductDataRequest Request); diff --git a/src/Modules/ProductCatalog/Features/CreateImageProductData/CreateImageProductDataController.cs b/src/Modules/ProductCatalog/Features/CreateImageProductData/CreateImageProductDataController.cs new file mode 100644 index 00000000..35e68933 --- /dev/null +++ b/src/Modules/ProductCatalog/Features/CreateImageProductData/CreateImageProductDataController.cs @@ -0,0 +1,28 @@ +using Asp.Versioning; +using ErrorOr; +using Microsoft.AspNetCore.Mvc; +using SharedKernel.Contracts.Api; +using SharedKernel.Contracts.Security; +using Wolverine; + +namespace ProductCatalog.Features.CreateImageProductData; + +[ApiVersion(1.0)] +[Route("api/v{version:apiVersion}/product-data")] +public sealed class CreateImageProductDataController(IMessageBus bus) : ApiControllerBase +{ + /// Creates a new image product-data document and returns it with a 201 Location header. + [HttpPost("image")] + [RequirePermission(Permission.ProductData.Create)] + public async Task> CreateImage( + CreateImageProductDataRequest request, + CancellationToken ct + ) + { + ErrorOr result = await bus.InvokeAsync>( + new CreateImageProductDataCommand(request), + ct + ); + return result.ToCreatedResult(this, v => new { id = v.Id, version = this.GetApiVersion() }); + } +} diff --git a/src/Modules/ProductCatalog/Features/ProductData/DTOs/CreateImageProductDataRequest.cs b/src/Modules/ProductCatalog/Features/CreateImageProductData/CreateImageProductDataRequest.cs similarity index 95% rename from src/Modules/ProductCatalog/Features/ProductData/DTOs/CreateImageProductDataRequest.cs rename to src/Modules/ProductCatalog/Features/CreateImageProductData/CreateImageProductDataRequest.cs index 5b37479b..935eb631 100644 --- a/src/Modules/ProductCatalog/Features/ProductData/DTOs/CreateImageProductDataRequest.cs +++ b/src/Modules/ProductCatalog/Features/CreateImageProductData/CreateImageProductDataRequest.cs @@ -1,7 +1,7 @@ using System.ComponentModel.DataAnnotations; using SharedKernel.Application.Validation; -namespace ProductCatalog.Features.ProductData.DTOs; +namespace ProductCatalog.Features.CreateImageProductData; /// /// Payload for uploading image product data, including dimensions, format, and file size. @@ -27,6 +27,3 @@ public sealed record CreateImageProductDataRequest( [Range(1, long.MaxValue, ErrorMessage = "FileSizeBytes must be greater than zero.")] long FileSizeBytes ); - - - diff --git a/src/Modules/ProductCatalog/Features/ProductData/Validation/CreateImageProductDataRequestValidator.cs b/src/Modules/ProductCatalog/Features/CreateImageProductData/CreateImageProductDataRequestValidator.cs similarity index 84% rename from src/Modules/ProductCatalog/Features/ProductData/Validation/CreateImageProductDataRequestValidator.cs rename to src/Modules/ProductCatalog/Features/CreateImageProductData/CreateImageProductDataRequestValidator.cs index 9633652a..76e8139f 100644 --- a/src/Modules/ProductCatalog/Features/ProductData/Validation/CreateImageProductDataRequestValidator.cs +++ b/src/Modules/ProductCatalog/Features/CreateImageProductData/CreateImageProductDataRequestValidator.cs @@ -1,12 +1,9 @@ using SharedKernel.Application.Validation; -namespace ProductCatalog.Features.ProductData.Validation; +namespace ProductCatalog.Features.CreateImageProductData; /// /// FluentValidation validator for , delegating to data-annotation-based validation rules. /// public sealed class CreateImageProductDataRequestValidator : DataAnnotationsValidator; - - - diff --git a/src/Modules/ProductCatalog/Features/Product/DTOs/CreateProductRequest.cs b/src/Modules/ProductCatalog/Features/CreateProducts/CreateProductRequest.cs similarity index 92% rename from src/Modules/ProductCatalog/Features/Product/DTOs/CreateProductRequest.cs rename to src/Modules/ProductCatalog/Features/CreateProducts/CreateProductRequest.cs index 6e8628ec..70772563 100644 --- a/src/Modules/ProductCatalog/Features/Product/DTOs/CreateProductRequest.cs +++ b/src/Modules/ProductCatalog/Features/CreateProducts/CreateProductRequest.cs @@ -1,6 +1,6 @@ using System.ComponentModel.DataAnnotations; -namespace ProductCatalog.Features.Product.DTOs; +namespace ProductCatalog.Features.CreateProducts; /// /// Carries the data required to create a new product, including validation constraints enforced via data annotations. @@ -14,4 +14,3 @@ public sealed record CreateProductRequest( Guid? CategoryId = null, IReadOnlyCollection? ProductDataIds = null ) : IProductRequest; - diff --git a/src/Modules/ProductCatalog/Features/Product/Validation/CreateProductRequestValidator.cs b/src/Modules/ProductCatalog/Features/CreateProducts/CreateProductRequestValidator.cs similarity index 83% rename from src/Modules/ProductCatalog/Features/Product/Validation/CreateProductRequestValidator.cs rename to src/Modules/ProductCatalog/Features/CreateProducts/CreateProductRequestValidator.cs index 0102d90e..2a22a678 100644 --- a/src/Modules/ProductCatalog/Features/Product/Validation/CreateProductRequestValidator.cs +++ b/src/Modules/ProductCatalog/Features/CreateProducts/CreateProductRequestValidator.cs @@ -1,10 +1,7 @@ -namespace ProductCatalog.Features.Product.Validation; +namespace ProductCatalog.Features.CreateProducts; /// /// FluentValidation validator for , inheriting all rules from . /// public sealed class CreateProductRequestValidator : ProductRequestValidatorBase; - - - diff --git a/src/Modules/ProductCatalog/Features/Product/Commands/CreateProductsCommand.cs b/src/Modules/ProductCatalog/Features/CreateProducts/CreateProductsCommand.cs similarity index 96% rename from src/Modules/ProductCatalog/Features/Product/Commands/CreateProductsCommand.cs rename to src/Modules/ProductCatalog/Features/CreateProducts/CreateProductsCommand.cs index 49d3b881..8d88083d 100644 --- a/src/Modules/ProductCatalog/Features/Product/Commands/CreateProductsCommand.cs +++ b/src/Modules/ProductCatalog/Features/CreateProducts/CreateProductsCommand.cs @@ -6,9 +6,9 @@ using SharedKernel.Contracts.Events; using Wolverine; using ProductEntity = ProductCatalog.Entities.Product; -using ProductRepositoryContract = ProductCatalog.Features.Product.Repositories.IProductRepository; +using ProductRepositoryContract = ProductCatalog.Interfaces.IProductRepository; -namespace ProductCatalog.Features.Product; +namespace ProductCatalog.Features.CreateProducts; /// Creates multiple products in a single batch operation. public sealed record CreateProductsCommand(CreateProductsRequest Request); @@ -101,4 +101,3 @@ await unitOfWork.ExecuteInTransactionAsync( return (new BatchResponse([], command.Request.Items.Count, 0), messages); } } - diff --git a/src/Modules/ProductCatalog/Features/CreateProducts/CreateProductsController.cs b/src/Modules/ProductCatalog/Features/CreateProducts/CreateProductsController.cs new file mode 100644 index 00000000..1a6fa330 --- /dev/null +++ b/src/Modules/ProductCatalog/Features/CreateProducts/CreateProductsController.cs @@ -0,0 +1,27 @@ +using Asp.Versioning; +using ErrorOr; +using Microsoft.AspNetCore.Mvc; +using SharedKernel.Contracts.Api; +using SharedKernel.Contracts.Security; +using Wolverine; + +namespace ProductCatalog.Features.CreateProducts; + +[ApiVersion(1.0)] +public sealed class CreateProductsController(IMessageBus bus) : ApiControllerBase +{ + /// Creates multiple products in a single batch operation. + [HttpPost] + [RequirePermission(Permission.Products.Create)] + public async Task> Create( + CreateProductsRequest request, + CancellationToken ct + ) + { + ErrorOr result = await bus.InvokeAsync>( + new CreateProductsCommand(request), + ct + ); + return result.ToBatchResult(this); + } +} diff --git a/src/Modules/ProductCatalog/Features/Product/DTOs/CreateProductsRequest.cs b/src/Modules/ProductCatalog/Features/CreateProducts/CreateProductsRequest.cs similarity index 89% rename from src/Modules/ProductCatalog/Features/Product/DTOs/CreateProductsRequest.cs rename to src/Modules/ProductCatalog/Features/CreateProducts/CreateProductsRequest.cs index 7116c5ba..f0677c9d 100644 --- a/src/Modules/ProductCatalog/Features/Product/DTOs/CreateProductsRequest.cs +++ b/src/Modules/ProductCatalog/Features/CreateProducts/CreateProductsRequest.cs @@ -1,6 +1,6 @@ using System.ComponentModel.DataAnnotations; -namespace ProductCatalog.Features.Product.DTOs; +namespace ProductCatalog.Features.CreateProducts; /// /// Carries a list of product items to be created in a single batch operation; accepts between 1 and 100 items. @@ -10,6 +10,3 @@ public sealed record CreateProductsRequest( [MaxLength(100, ErrorMessage = "Maximum 100 items per batch.")] IReadOnlyList Items ); - - - diff --git a/src/Modules/ProductCatalog/Features/ProductData/Commands/CreateVideoProductDataCommand.cs b/src/Modules/ProductCatalog/Features/CreateVideoProductData/CreateVideoProductDataCommand.cs similarity index 93% rename from src/Modules/ProductCatalog/Features/ProductData/Commands/CreateVideoProductDataCommand.cs rename to src/Modules/ProductCatalog/Features/CreateVideoProductData/CreateVideoProductDataCommand.cs index 1185c696..45938112 100644 --- a/src/Modules/ProductCatalog/Features/ProductData/Commands/CreateVideoProductDataCommand.cs +++ b/src/Modules/ProductCatalog/Features/CreateVideoProductData/CreateVideoProductDataCommand.cs @@ -1,12 +1,11 @@ using ErrorOr; using ProductCatalog.Entities; -using ProductCatalog.Features.ProductData.Mappings; using ProductCatalog.Interfaces; using SharedKernel.Application.Context; using SharedKernel.Contracts.Events; using Wolverine; -namespace ProductCatalog.Features.ProductData; +namespace ProductCatalog.Features.CreateVideoProductData; public sealed record CreateVideoProductDataCommand(CreateVideoProductDataRequest Request); diff --git a/src/Modules/ProductCatalog/Features/CreateVideoProductData/CreateVideoProductDataController.cs b/src/Modules/ProductCatalog/Features/CreateVideoProductData/CreateVideoProductDataController.cs new file mode 100644 index 00000000..aaaea6d6 --- /dev/null +++ b/src/Modules/ProductCatalog/Features/CreateVideoProductData/CreateVideoProductDataController.cs @@ -0,0 +1,28 @@ +using Asp.Versioning; +using ErrorOr; +using Microsoft.AspNetCore.Mvc; +using SharedKernel.Contracts.Api; +using SharedKernel.Contracts.Security; +using Wolverine; + +namespace ProductCatalog.Features.CreateVideoProductData; + +[ApiVersion(1.0)] +[Route("api/v{version:apiVersion}/product-data")] +public sealed class CreateVideoProductDataController(IMessageBus bus) : ApiControllerBase +{ + /// Creates a new video product-data document and returns it with a 201 Location header. + [HttpPost("video")] + [RequirePermission(Permission.ProductData.Create)] + public async Task> CreateVideo( + CreateVideoProductDataRequest request, + CancellationToken ct + ) + { + ErrorOr result = await bus.InvokeAsync>( + new CreateVideoProductDataCommand(request), + ct + ); + return result.ToCreatedResult(this, v => new { id = v.Id, version = this.GetApiVersion() }); + } +} diff --git a/src/Modules/ProductCatalog/Features/ProductData/DTOs/CreateVideoProductDataRequest.cs b/src/Modules/ProductCatalog/Features/CreateVideoProductData/CreateVideoProductDataRequest.cs similarity index 95% rename from src/Modules/ProductCatalog/Features/ProductData/DTOs/CreateVideoProductDataRequest.cs rename to src/Modules/ProductCatalog/Features/CreateVideoProductData/CreateVideoProductDataRequest.cs index 1af3fe94..c5b94654 100644 --- a/src/Modules/ProductCatalog/Features/ProductData/DTOs/CreateVideoProductDataRequest.cs +++ b/src/Modules/ProductCatalog/Features/CreateVideoProductData/CreateVideoProductDataRequest.cs @@ -1,7 +1,7 @@ using System.ComponentModel.DataAnnotations; using SharedKernel.Application.Validation; -namespace ProductCatalog.Features.ProductData.DTOs; +namespace ProductCatalog.Features.CreateVideoProductData; /// /// Payload for uploading video product data, including duration, resolution, format, and file size. @@ -29,6 +29,3 @@ public sealed record CreateVideoProductDataRequest( [Range(1, long.MaxValue, ErrorMessage = "FileSizeBytes must be greater than zero.")] long FileSizeBytes ); - - - diff --git a/src/Modules/ProductCatalog/Features/ProductData/Validation/CreateVideoProductDataRequestValidator.cs b/src/Modules/ProductCatalog/Features/CreateVideoProductData/CreateVideoProductDataRequestValidator.cs similarity index 84% rename from src/Modules/ProductCatalog/Features/ProductData/Validation/CreateVideoProductDataRequestValidator.cs rename to src/Modules/ProductCatalog/Features/CreateVideoProductData/CreateVideoProductDataRequestValidator.cs index 49d853cd..bd8140f2 100644 --- a/src/Modules/ProductCatalog/Features/ProductData/Validation/CreateVideoProductDataRequestValidator.cs +++ b/src/Modules/ProductCatalog/Features/CreateVideoProductData/CreateVideoProductDataRequestValidator.cs @@ -1,12 +1,9 @@ using SharedKernel.Application.Validation; -namespace ProductCatalog.Features.ProductData.Validation; +namespace ProductCatalog.Features.CreateVideoProductData; /// /// FluentValidation validator for , delegating to data-annotation-based validation rules. /// public sealed class CreateVideoProductDataRequestValidator : DataAnnotationsValidator; - - - diff --git a/src/Modules/ProductCatalog/Features/Category/Commands/DeleteCategoriesCommand.cs b/src/Modules/ProductCatalog/Features/DeleteCategories/DeleteCategoriesCommand.cs similarity index 96% rename from src/Modules/ProductCatalog/Features/Category/Commands/DeleteCategoriesCommand.cs rename to src/Modules/ProductCatalog/Features/DeleteCategories/DeleteCategoriesCommand.cs index 873ce6db..cecbdb4e 100644 --- a/src/Modules/ProductCatalog/Features/Category/Commands/DeleteCategoriesCommand.cs +++ b/src/Modules/ProductCatalog/Features/DeleteCategories/DeleteCategoriesCommand.cs @@ -1,9 +1,8 @@ using ErrorOr; -using ProductCatalog.Features.Category.Specifications; using ProductCatalog; using Wolverine; -namespace ProductCatalog.Features.Category; +namespace ProductCatalog.Features.DeleteCategories; /// Soft-deletes multiple categories in a single batch operation. public sealed record DeleteCategoriesCommand(BatchDeleteRequest Request); @@ -75,4 +74,3 @@ await productRepository.ClearCategoryAsync( return (new BatchResponse([], command.Request.Ids.Count, 0), messages); } } - diff --git a/src/Modules/ProductCatalog/Features/DeleteCategories/DeleteCategoriesController.cs b/src/Modules/ProductCatalog/Features/DeleteCategories/DeleteCategoriesController.cs new file mode 100644 index 00000000..2fe0dcde --- /dev/null +++ b/src/Modules/ProductCatalog/Features/DeleteCategories/DeleteCategoriesController.cs @@ -0,0 +1,27 @@ +using Asp.Versioning; +using ErrorOr; +using Microsoft.AspNetCore.Mvc; +using SharedKernel.Contracts.Api; +using SharedKernel.Contracts.Security; +using Wolverine; + +namespace ProductCatalog.Features.DeleteCategories; + +[ApiVersion(1.0)] +public sealed class DeleteCategoriesController(IMessageBus bus) : ApiControllerBase +{ + /// Soft-deletes multiple categories in a single batch operation. + [HttpDelete] + [RequirePermission(Permission.Categories.Delete)] + public async Task> Delete( + BatchDeleteRequest request, + CancellationToken ct + ) + { + ErrorOr result = await bus.InvokeAsync>( + new DeleteCategoriesCommand(request), + ct + ); + return result.ToBatchResult(this); + } +} diff --git a/src/Modules/ProductCatalog/Features/ProductData/Commands/DeleteProductDataCommand.cs b/src/Modules/ProductCatalog/Features/DeleteProductData/DeleteProductDataCommand.cs similarity index 98% rename from src/Modules/ProductCatalog/Features/ProductData/Commands/DeleteProductDataCommand.cs rename to src/Modules/ProductCatalog/Features/DeleteProductData/DeleteProductDataCommand.cs index b299c374..109be42a 100644 --- a/src/Modules/ProductCatalog/Features/ProductData/Commands/DeleteProductDataCommand.cs +++ b/src/Modules/ProductCatalog/Features/DeleteProductData/DeleteProductDataCommand.cs @@ -6,7 +6,7 @@ using ProductCatalog; using Wolverine; -namespace ProductCatalog.Features.ProductData; +namespace ProductCatalog.Features.DeleteProductData; public sealed record DeleteProductDataCommand(Guid Id) : IHasId; @@ -107,4 +107,3 @@ await repository.SoftDeleteAsync( return (Result.Success, messages); } } - diff --git a/src/Modules/ProductCatalog/Features/DeleteProductData/DeleteProductDataController.cs b/src/Modules/ProductCatalog/Features/DeleteProductData/DeleteProductDataController.cs new file mode 100644 index 00000000..409bebcc --- /dev/null +++ b/src/Modules/ProductCatalog/Features/DeleteProductData/DeleteProductDataController.cs @@ -0,0 +1,25 @@ +using Asp.Versioning; +using ErrorOr; +using Microsoft.AspNetCore.Mvc; +using SharedKernel.Contracts.Api; +using SharedKernel.Contracts.Security; +using Wolverine; + +namespace ProductCatalog.Features.DeleteProductData; + +[ApiVersion(1.0)] +[Route("api/v{version:apiVersion}/product-data")] +public sealed class DeleteProductDataController(IMessageBus bus) : ApiControllerBase +{ + /// Deletes a product data document by its identifier. + [HttpDelete("{id:guid}")] + [RequirePermission(Permission.ProductData.Delete)] + public async Task Delete(Guid id, CancellationToken ct) + { + ErrorOr result = await bus.InvokeAsync>( + new DeleteProductDataCommand(id), + ct + ); + return result.ToNoContentResult(this); + } +} diff --git a/src/Modules/ProductCatalog/Features/ProductData/Handlers/ProductDataCascadeDeleteHandler.cs b/src/Modules/ProductCatalog/Features/DeleteProductData/ProductDataCascadeDeleteHandler.cs similarity index 95% rename from src/Modules/ProductCatalog/Features/ProductData/Handlers/ProductDataCascadeDeleteHandler.cs rename to src/Modules/ProductCatalog/Features/DeleteProductData/ProductDataCascadeDeleteHandler.cs index 68f77260..1d3f2896 100644 --- a/src/Modules/ProductCatalog/Features/ProductData/Handlers/ProductDataCascadeDeleteHandler.cs +++ b/src/Modules/ProductCatalog/Features/DeleteProductData/ProductDataCascadeDeleteHandler.cs @@ -3,7 +3,7 @@ using Polly.Registry; using ProductCatalog.Logging; -namespace ProductCatalog.Features.ProductData.Handlers; +namespace ProductCatalog.Features.DeleteProductData; public sealed class ProductDataCascadeDeleteHandler { @@ -40,4 +40,3 @@ await productDataRepository.SoftDeleteByTenantAsync( } } } - diff --git a/src/Modules/ProductCatalog/Features/Product/Commands/DeleteProductsCommand.cs b/src/Modules/ProductCatalog/Features/DeleteProducts/DeleteProductsCommand.cs similarity index 94% rename from src/Modules/ProductCatalog/Features/Product/Commands/DeleteProductsCommand.cs rename to src/Modules/ProductCatalog/Features/DeleteProducts/DeleteProductsCommand.cs index f83b12ad..5ce0c4a4 100644 --- a/src/Modules/ProductCatalog/Features/Product/Commands/DeleteProductsCommand.cs +++ b/src/Modules/ProductCatalog/Features/DeleteProducts/DeleteProductsCommand.cs @@ -1,10 +1,9 @@ using ErrorOr; -using ProductCatalog.Features.Product.Specifications; using ProductCatalog; using Wolverine; -using ProductRepositoryContract = ProductCatalog.Features.Product.Repositories.IProductRepository; +using ProductRepositoryContract = ProductCatalog.Interfaces.IProductRepository; -namespace ProductCatalog.Features.Product; +namespace ProductCatalog.Features.DeleteProducts; /// Soft-deletes multiple products and their associated data links in a single batch operation. public sealed record DeleteProductsCommand(BatchDeleteRequest Request); @@ -95,4 +94,3 @@ await unitOfWork.ExecuteInTransactionAsync( return (new BatchResponse([], command.Request.Ids.Count, 0), messages); } } - diff --git a/src/Modules/ProductCatalog/Features/DeleteProducts/DeleteProductsController.cs b/src/Modules/ProductCatalog/Features/DeleteProducts/DeleteProductsController.cs new file mode 100644 index 00000000..eb76845a --- /dev/null +++ b/src/Modules/ProductCatalog/Features/DeleteProducts/DeleteProductsController.cs @@ -0,0 +1,27 @@ +using Asp.Versioning; +using ErrorOr; +using Microsoft.AspNetCore.Mvc; +using SharedKernel.Contracts.Api; +using SharedKernel.Contracts.Security; +using Wolverine; + +namespace ProductCatalog.Features.DeleteProducts; + +[ApiVersion(1.0)] +public sealed class DeleteProductsController(IMessageBus bus) : ApiControllerBase +{ + /// Soft-deletes multiple products in a single batch operation. + [HttpDelete] + [RequirePermission(Permission.Products.Delete)] + public async Task> Delete( + BatchDeleteRequest request, + CancellationToken ct + ) + { + ErrorOr result = await bus.InvokeAsync>( + new DeleteProductsCommand(request), + ct + ); + return result.ToBatchResult(this); + } +} diff --git a/src/Modules/ProductCatalog/Features/Category/DTOs/CategoryFilter.cs b/src/Modules/ProductCatalog/Features/GetCategories/CategoryFilter.cs similarity index 90% rename from src/Modules/ProductCatalog/Features/Category/DTOs/CategoryFilter.cs rename to src/Modules/ProductCatalog/Features/GetCategories/CategoryFilter.cs index 47202bde..9c3f6d4d 100644 --- a/src/Modules/ProductCatalog/Features/Category/DTOs/CategoryFilter.cs +++ b/src/Modules/ProductCatalog/Features/GetCategories/CategoryFilter.cs @@ -1,7 +1,7 @@ using SharedKernel.Application.Contracts; using SharedKernel.Application.DTOs; -namespace ProductCatalog.Features.Category.DTOs; +namespace ProductCatalog.Features.GetCategories; /// /// Filter parameters for querying categories, supporting full-text search, sorting, and pagination. @@ -13,6 +13,3 @@ public sealed record CategoryFilter( int PageNumber = 1, int PageSize = PaginationFilter.DefaultPageSize ) : PaginationFilter(PageNumber, PageSize), ISortableFilter; - - - diff --git a/src/Modules/ProductCatalog/Features/Category/Specifications/CategoryFilterCriteria.cs b/src/Modules/ProductCatalog/Features/GetCategories/CategoryFilterCriteria.cs similarity index 95% rename from src/Modules/ProductCatalog/Features/Category/Specifications/CategoryFilterCriteria.cs rename to src/Modules/ProductCatalog/Features/GetCategories/CategoryFilterCriteria.cs index b4848b04..0e5cdadd 100644 --- a/src/Modules/ProductCatalog/Features/Category/Specifications/CategoryFilterCriteria.cs +++ b/src/Modules/ProductCatalog/Features/GetCategories/CategoryFilterCriteria.cs @@ -3,7 +3,7 @@ using Microsoft.EntityFrameworkCore; using CategoryEntity = ProductCatalog.Entities.Category; -namespace ProductCatalog.Features.Category.Specifications; +namespace ProductCatalog.Features.GetCategories; /// /// Extension methods that apply search criteria to an Ardalis specification builder. @@ -37,6 +37,3 @@ CategoryFilter filter ); } } - - - diff --git a/src/Modules/ProductCatalog/Features/Category/Validation/CategoryFilterValidator.cs b/src/Modules/ProductCatalog/Features/GetCategories/CategoryFilterValidator.cs similarity index 89% rename from src/Modules/ProductCatalog/Features/Category/Validation/CategoryFilterValidator.cs rename to src/Modules/ProductCatalog/Features/GetCategories/CategoryFilterValidator.cs index 8f7a624b..e84c4323 100644 --- a/src/Modules/ProductCatalog/Features/Category/Validation/CategoryFilterValidator.cs +++ b/src/Modules/ProductCatalog/Features/GetCategories/CategoryFilterValidator.cs @@ -1,7 +1,7 @@ using SharedKernel.Application.Validation; using FluentValidation; -namespace ProductCatalog.Features.Category.Validation; +namespace ProductCatalog.Features.GetCategories; /// /// FluentValidation validator for . @@ -15,6 +15,3 @@ public CategoryFilterValidator() Include(new SortableFilterValidator(CategorySortFields.Map.AllowedNames)); } } - - - diff --git a/src/Modules/ProductCatalog/Features/Category/Specifications/CategorySpecification.cs b/src/Modules/ProductCatalog/Features/GetCategories/CategorySpecification.cs similarity index 86% rename from src/Modules/ProductCatalog/Features/Category/Specifications/CategorySpecification.cs rename to src/Modules/ProductCatalog/Features/GetCategories/CategorySpecification.cs index 27b62d70..c059feac 100644 --- a/src/Modules/ProductCatalog/Features/Category/Specifications/CategorySpecification.cs +++ b/src/Modules/ProductCatalog/Features/GetCategories/CategorySpecification.cs @@ -1,8 +1,7 @@ -using ProductCatalog.Features.Category.Mappings; using Ardalis.Specification; using CategoryEntity = ProductCatalog.Entities.Category; -namespace ProductCatalog.Features.Category.Specifications; +namespace ProductCatalog.Features.GetCategories; /// /// Ardalis specification for querying a filtered and sorted list of categories projected to . @@ -18,6 +17,3 @@ public CategorySpecification(CategoryFilter filter) Query.Select(CategoryMappings.Projection); } } - - - diff --git a/src/Modules/ProductCatalog/Features/GetCategories/GetCategoriesController.cs b/src/Modules/ProductCatalog/Features/GetCategories/GetCategoriesController.cs new file mode 100644 index 00000000..31a260c9 --- /dev/null +++ b/src/Modules/ProductCatalog/Features/GetCategories/GetCategoriesController.cs @@ -0,0 +1,28 @@ +using Asp.Versioning; +using ErrorOr; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.OutputCaching; +using SharedKernel.Contracts.Api; +using SharedKernel.Contracts.Security; +using Wolverine; + +namespace ProductCatalog.Features.GetCategories; + +[ApiVersion(1.0)] +public sealed class GetCategoriesController(IMessageBus bus) : ApiControllerBase +{ + /// Returns a paginated, filterable list of categories from the output cache. + [HttpGet] + [RequirePermission(Permission.Categories.Read)] + [OutputCache(PolicyName = CacheTags.Categories)] + public async Task>> GetAll( + [FromQuery] CategoryFilter filter, + CancellationToken ct + ) + { + ErrorOr> result = await bus.InvokeAsync< + ErrorOr> + >(new GetCategoriesQuery(filter), ct); + return result.ToActionResult(this); + } +} diff --git a/src/Modules/ProductCatalog/Features/Category/Queries/GetCategoriesQuery.cs b/src/Modules/ProductCatalog/Features/GetCategories/GetCategoriesQuery.cs similarity index 87% rename from src/Modules/ProductCatalog/Features/Category/Queries/GetCategoriesQuery.cs rename to src/Modules/ProductCatalog/Features/GetCategories/GetCategoriesQuery.cs index d47c2660..391c7d80 100644 --- a/src/Modules/ProductCatalog/Features/Category/Queries/GetCategoriesQuery.cs +++ b/src/Modules/ProductCatalog/Features/GetCategories/GetCategoriesQuery.cs @@ -1,7 +1,6 @@ -using ProductCatalog.Features.Category.Specifications; using ErrorOr; -namespace ProductCatalog.Features.Category; +namespace ProductCatalog.Features.GetCategories; /// Returns a paginated, filtered, and sorted list of categories. public sealed record GetCategoriesQuery(CategoryFilter Filter); @@ -23,6 +22,3 @@ CancellationToken ct ); } } - - - diff --git a/src/Modules/ProductCatalog/Features/Category/Specifications/CategoryByIdSpecification.cs b/src/Modules/ProductCatalog/Features/GetCategoryById/CategoryByIdSpecification.cs similarity index 85% rename from src/Modules/ProductCatalog/Features/Category/Specifications/CategoryByIdSpecification.cs rename to src/Modules/ProductCatalog/Features/GetCategoryById/CategoryByIdSpecification.cs index ab11c314..56882605 100644 --- a/src/Modules/ProductCatalog/Features/Category/Specifications/CategoryByIdSpecification.cs +++ b/src/Modules/ProductCatalog/Features/GetCategoryById/CategoryByIdSpecification.cs @@ -1,8 +1,7 @@ -using ProductCatalog.Features.Category.Mappings; using Ardalis.Specification; using CategoryEntity = ProductCatalog.Entities.Category; -namespace ProductCatalog.Features.Category.Specifications; +namespace ProductCatalog.Features.GetCategoryById; /// /// Ardalis specification that fetches a single category by its identifier, projected directly to . @@ -18,6 +17,3 @@ public CategoryByIdSpecification(Guid id) .Select(CategoryMappings.Projection); } } - - - diff --git a/src/Modules/ProductCatalog/Features/GetCategoryById/GetCategoryByIdController.cs b/src/Modules/ProductCatalog/Features/GetCategoryById/GetCategoryByIdController.cs new file mode 100644 index 00000000..6d4aabbd --- /dev/null +++ b/src/Modules/ProductCatalog/Features/GetCategoryById/GetCategoryByIdController.cs @@ -0,0 +1,26 @@ +using Asp.Versioning; +using ErrorOr; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.OutputCaching; +using SharedKernel.Contracts.Api; +using SharedKernel.Contracts.Security; +using Wolverine; + +namespace ProductCatalog.Features.GetCategoryById; + +[ApiVersion(1.0)] +public sealed class GetCategoryByIdController(IMessageBus bus) : ApiControllerBase +{ + /// Returns a single category by its identifier, or 404 if not found. + [HttpGet("{id:guid}")] + [RequirePermission(Permission.Categories.Read)] + [OutputCache(PolicyName = CacheTags.Categories)] + public async Task> GetById(Guid id, CancellationToken ct) + { + ErrorOr result = await bus.InvokeAsync>( + new GetCategoryByIdQuery(id), + ct + ); + return result.ToActionResult(this); + } +} diff --git a/src/Modules/ProductCatalog/Features/Category/Queries/GetCategoryByIdQuery.cs b/src/Modules/ProductCatalog/Features/GetCategoryById/GetCategoryByIdQuery.cs similarity index 89% rename from src/Modules/ProductCatalog/Features/Category/Queries/GetCategoryByIdQuery.cs rename to src/Modules/ProductCatalog/Features/GetCategoryById/GetCategoryByIdQuery.cs index 12ad730b..548c7c95 100644 --- a/src/Modules/ProductCatalog/Features/Category/Queries/GetCategoryByIdQuery.cs +++ b/src/Modules/ProductCatalog/Features/GetCategoryById/GetCategoryByIdQuery.cs @@ -1,8 +1,7 @@ using ErrorOr; -using ProductCatalog.Features.Category.Specifications; using ProductCatalog.Interfaces; -namespace ProductCatalog.Features.Category; +namespace ProductCatalog.Features.GetCategoryById; /// Returns a single category by its unique identifier, or if not found. public sealed record GetCategoryByIdQuery(Guid Id) : IHasId; @@ -27,4 +26,3 @@ CancellationToken ct return result; } } - diff --git a/src/Modules/ProductCatalog/Features/GetCategoryStats/GetCategoryStatsController.cs b/src/Modules/ProductCatalog/Features/GetCategoryStats/GetCategoryStatsController.cs new file mode 100644 index 00000000..f25824d3 --- /dev/null +++ b/src/Modules/ProductCatalog/Features/GetCategoryStats/GetCategoryStatsController.cs @@ -0,0 +1,31 @@ +using Asp.Versioning; +using ErrorOr; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.OutputCaching; +using SharedKernel.Contracts.Api; +using SharedKernel.Contracts.Security; +using Wolverine; + +namespace ProductCatalog.Features.GetCategoryStats; + +[ApiVersion(1.0)] +public sealed class GetCategoryStatsController(IMessageBus bus) : ApiControllerBase +{ + /// + /// Returns aggregated statistics for a category by calling the + /// get_product_category_stats(p_category_id) stored procedure via EF Core FromSql. + /// + [HttpGet("{id:guid}/stats")] + [RequirePermission(Permission.Categories.Read)] + [OutputCache(PolicyName = CacheTags.Categories)] + public async Task> GetStats( + Guid id, + CancellationToken ct + ) + { + ErrorOr result = await bus.InvokeAsync< + ErrorOr + >(new GetCategoryStatsQuery(id), ct); + return result.ToActionResult(this); + } +} diff --git a/src/Modules/ProductCatalog/Features/Category/Queries/GetCategoryStatsQuery.cs b/src/Modules/ProductCatalog/Features/GetCategoryStats/GetCategoryStatsQuery.cs similarity index 59% rename from src/Modules/ProductCatalog/Features/Category/Queries/GetCategoryStatsQuery.cs rename to src/Modules/ProductCatalog/Features/GetCategoryStats/GetCategoryStatsQuery.cs index c15421e7..a768f173 100644 --- a/src/Modules/ProductCatalog/Features/Category/Queries/GetCategoryStatsQuery.cs +++ b/src/Modules/ProductCatalog/Features/GetCategoryStats/GetCategoryStatsQuery.cs @@ -1,8 +1,8 @@ using ErrorOr; -using ProductCatalog.Features.Category.Mappings; using ProductCatalog.Interfaces; +using ProductCategoryStatsEntity = ProductCatalog.Entities.ProductCategoryStats; -namespace ProductCatalog.Features.Category; +namespace ProductCatalog.Features.GetCategoryStats; /// Returns aggregated statistics for a category by its identifier, or if not found. public sealed record GetCategoryStatsQuery(Guid Id) : IHasId; @@ -16,12 +16,17 @@ public static async Task> HandleAsync( CancellationToken ct ) { - ProductCategoryStats? stats = await repository.GetStatsByIdAsync(request.Id, ct); + ProductCategoryStatsEntity? stats = await repository.GetStatsByIdAsync(request.Id, ct); if (stats is null) return DomainErrors.Categories.NotFound(request.Id); - return stats.ToResponse(); + return new ProductCategoryStatsResponse( + stats.CategoryId, + stats.CategoryName, + stats.ProductCount, + stats.AveragePrice, + stats.TotalReviews + ); } } - diff --git a/src/Modules/ProductCatalog/Features/Category/DTOs/ProductCategoryStatsResponse.cs b/src/Modules/ProductCatalog/Features/GetCategoryStats/ProductCategoryStatsResponse.cs similarity index 85% rename from src/Modules/ProductCatalog/Features/Category/DTOs/ProductCategoryStatsResponse.cs rename to src/Modules/ProductCatalog/Features/GetCategoryStats/ProductCategoryStatsResponse.cs index 1730e632..1a000353 100644 --- a/src/Modules/ProductCatalog/Features/Category/DTOs/ProductCategoryStatsResponse.cs +++ b/src/Modules/ProductCatalog/Features/GetCategoryStats/ProductCategoryStatsResponse.cs @@ -1,4 +1,4 @@ -namespace ProductCatalog.Features.Category.DTOs; +namespace ProductCatalog.Features.GetCategoryStats; /// /// Aggregated statistics for a single category, including product count, average price, and total review count. @@ -10,6 +10,3 @@ public sealed record ProductCategoryStatsResponse( decimal AveragePrice, long TotalReviews ); - - - diff --git a/src/Modules/ProductCatalog/Features/GetProductById/GetProductByIdController.cs b/src/Modules/ProductCatalog/Features/GetProductById/GetProductByIdController.cs new file mode 100644 index 00000000..0b1524b2 --- /dev/null +++ b/src/Modules/ProductCatalog/Features/GetProductById/GetProductByIdController.cs @@ -0,0 +1,26 @@ +using Asp.Versioning; +using ErrorOr; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.OutputCaching; +using SharedKernel.Contracts.Api; +using SharedKernel.Contracts.Security; +using Wolverine; + +namespace ProductCatalog.Features.GetProductById; + +[ApiVersion(1.0)] +public sealed class GetProductByIdController(IMessageBus bus) : ApiControllerBase +{ + /// Returns a single product by its identifier, or 404 if not found. + [HttpGet("{id:guid}")] + [RequirePermission(Permission.Products.Read)] + [OutputCache(PolicyName = CacheTags.Products)] + public async Task> GetById(Guid id, CancellationToken ct) + { + ErrorOr result = await bus.InvokeAsync>( + new GetProductByIdQuery(id), + ct + ); + return result.ToActionResult(this); + } +} diff --git a/src/Modules/ProductCatalog/Features/Product/Queries/GetProductByIdQuery.cs b/src/Modules/ProductCatalog/Features/GetProductById/GetProductByIdQuery.cs similarity index 75% rename from src/Modules/ProductCatalog/Features/Product/Queries/GetProductByIdQuery.cs rename to src/Modules/ProductCatalog/Features/GetProductById/GetProductByIdQuery.cs index f6723d26..1cd0037f 100644 --- a/src/Modules/ProductCatalog/Features/Product/Queries/GetProductByIdQuery.cs +++ b/src/Modules/ProductCatalog/Features/GetProductById/GetProductByIdQuery.cs @@ -1,9 +1,7 @@ using ErrorOr; -using ProductCatalog.Features.Product.Repositories; -using ProductCatalog.Features.Product.Specifications; -using ProductRepositoryContract = ProductCatalog.Features.Product.Repositories.IProductRepository; +using ProductRepositoryContract = ProductCatalog.Interfaces.IProductRepository; -namespace ProductCatalog.Features.Product; +namespace ProductCatalog.Features.GetProductById; /// Retrieves a single product by its unique identifier. public sealed record GetProductByIdQuery(Guid Id) : IHasId; @@ -28,4 +26,3 @@ CancellationToken ct return result; } } - diff --git a/src/Modules/ProductCatalog/Features/Product/Specifications/ProductByIdSpecification.cs b/src/Modules/ProductCatalog/Features/GetProductById/ProductByIdSpecification.cs similarity index 81% rename from src/Modules/ProductCatalog/Features/Product/Specifications/ProductByIdSpecification.cs rename to src/Modules/ProductCatalog/Features/GetProductById/ProductByIdSpecification.cs index 64d50636..8cd63658 100644 --- a/src/Modules/ProductCatalog/Features/Product/Specifications/ProductByIdSpecification.cs +++ b/src/Modules/ProductCatalog/Features/GetProductById/ProductByIdSpecification.cs @@ -1,8 +1,7 @@ -using ProductCatalog.Features.Product.Mappings; using Ardalis.Specification; using ProductEntity = ProductCatalog.Entities.Product; -namespace ProductCatalog.Features.Product.Specifications; +namespace ProductCatalog.Features.GetProductById; /// /// Ardalis specification that fetches a single product by its ID and projects it directly to a DTO. @@ -14,6 +13,3 @@ public ProductByIdSpecification(Guid id) Query.Where(product => product.Id == id).Select(ProductMappings.Projection); } } - - - diff --git a/src/Modules/ProductCatalog/Features/Product/Specifications/ProductByIdWithLinksSpecification.cs b/src/Modules/ProductCatalog/Features/GetProductById/ProductByIdWithLinksSpecification.cs similarity index 89% rename from src/Modules/ProductCatalog/Features/Product/Specifications/ProductByIdWithLinksSpecification.cs rename to src/Modules/ProductCatalog/Features/GetProductById/ProductByIdWithLinksSpecification.cs index d87a1c97..53d47975 100644 --- a/src/Modules/ProductCatalog/Features/Product/Specifications/ProductByIdWithLinksSpecification.cs +++ b/src/Modules/ProductCatalog/Features/GetProductById/ProductByIdWithLinksSpecification.cs @@ -1,7 +1,7 @@ using Ardalis.Specification; using ProductEntity = ProductCatalog.Entities.Product; -namespace ProductCatalog.Features.Product.Specifications; +namespace ProductCatalog.Features.GetProductById; /// /// Ardalis specification that loads a product by ID and eagerly includes its ProductDataLinks collection, used when link synchronisation or deletion is required. @@ -13,6 +13,3 @@ public ProductByIdWithLinksSpecification(Guid id) Query.Where(product => product.Id == id).Include(product => product.ProductDataLinks); } } - - - diff --git a/src/Modules/ProductCatalog/Features/GetProductData/GetProductDataController.cs b/src/Modules/ProductCatalog/Features/GetProductData/GetProductDataController.cs new file mode 100644 index 00000000..c0a199b6 --- /dev/null +++ b/src/Modules/ProductCatalog/Features/GetProductData/GetProductDataController.cs @@ -0,0 +1,29 @@ +using Asp.Versioning; +using ErrorOr; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.OutputCaching; +using SharedKernel.Contracts.Api; +using SharedKernel.Contracts.Security; +using Wolverine; + +namespace ProductCatalog.Features.GetProductData; + +[ApiVersion(1.0)] +[Route("api/v{version:apiVersion}/product-data")] +public sealed class GetProductDataController(IMessageBus bus) : ApiControllerBase +{ + /// Returns all product data documents, optionally filtered by type. + [HttpGet] + [RequirePermission(Permission.ProductData.Read)] + [OutputCache(PolicyName = CacheTags.ProductData)] + public async Task>> GetAll( + [FromQuery] string? type, + CancellationToken ct + ) + { + ErrorOr> result = await bus.InvokeAsync< + ErrorOr> + >(new GetProductDataQuery(type), ct); + return result.ToActionResult(this); + } +} diff --git a/src/Modules/ProductCatalog/Features/ProductData/Queries/GetProductDataQuery.cs b/src/Modules/ProductCatalog/Features/GetProductData/GetProductDataQuery.cs similarity index 80% rename from src/Modules/ProductCatalog/Features/ProductData/Queries/GetProductDataQuery.cs rename to src/Modules/ProductCatalog/Features/GetProductData/GetProductDataQuery.cs index 1635396e..515f3046 100644 --- a/src/Modules/ProductCatalog/Features/ProductData/Queries/GetProductDataQuery.cs +++ b/src/Modules/ProductCatalog/Features/GetProductData/GetProductDataQuery.cs @@ -1,8 +1,6 @@ using ErrorOr; -using ProductCatalog.Features.ProductData.Mappings; -using ProductCatalog.Interfaces; -namespace ProductCatalog.Features.ProductData; +namespace ProductCatalog.Features.GetProductData; public sealed record GetProductDataQuery(string? Type); diff --git a/src/Modules/ProductCatalog/Features/GetProductDataById/GetProductDataByIdController.cs b/src/Modules/ProductCatalog/Features/GetProductDataById/GetProductDataByIdController.cs new file mode 100644 index 00000000..fef62c90 --- /dev/null +++ b/src/Modules/ProductCatalog/Features/GetProductDataById/GetProductDataByIdController.cs @@ -0,0 +1,27 @@ +using Asp.Versioning; +using ErrorOr; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.OutputCaching; +using SharedKernel.Contracts.Api; +using SharedKernel.Contracts.Security; +using Wolverine; + +namespace ProductCatalog.Features.GetProductDataById; + +[ApiVersion(1.0)] +[Route("api/v{version:apiVersion}/product-data")] +public sealed class GetProductDataByIdController(IMessageBus bus) : ApiControllerBase +{ + /// Returns a single product data document by its identifier, or 404 if not found. + [HttpGet("{id:guid}")] + [RequirePermission(Permission.ProductData.Read)] + [OutputCache(PolicyName = CacheTags.ProductData)] + public async Task> GetById(Guid id, CancellationToken ct) + { + ErrorOr result = await bus.InvokeAsync>( + new GetProductDataByIdQuery(id), + ct + ); + return result.ToActionResult(this); + } +} diff --git a/src/Modules/ProductCatalog/Features/ProductData/Queries/GetProductDataByIdQuery.cs b/src/Modules/ProductCatalog/Features/GetProductDataById/GetProductDataByIdQuery.cs similarity index 84% rename from src/Modules/ProductCatalog/Features/ProductData/Queries/GetProductDataByIdQuery.cs rename to src/Modules/ProductCatalog/Features/GetProductDataById/GetProductDataByIdQuery.cs index 5be7ff8e..0c94174a 100644 --- a/src/Modules/ProductCatalog/Features/ProductData/Queries/GetProductDataByIdQuery.cs +++ b/src/Modules/ProductCatalog/Features/GetProductDataById/GetProductDataByIdQuery.cs @@ -1,9 +1,7 @@ using ErrorOr; -using ProductCatalog.Features.ProductData.Mappings; -using ProductCatalog.Interfaces; using SharedKernel.Application.Context; -namespace ProductCatalog.Features.ProductData; +namespace ProductCatalog.Features.GetProductDataById; public sealed record GetProductDataByIdQuery(Guid Id) : IHasId; diff --git a/src/Modules/ProductCatalog/Features/GetProducts/GetProductsController.cs b/src/Modules/ProductCatalog/Features/GetProducts/GetProductsController.cs new file mode 100644 index 00000000..4ce3f977 --- /dev/null +++ b/src/Modules/ProductCatalog/Features/GetProducts/GetProductsController.cs @@ -0,0 +1,29 @@ +using Asp.Versioning; +using ErrorOr; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.OutputCaching; +using SharedKernel.Contracts.Api; +using SharedKernel.Contracts.Security; +using Wolverine; + +namespace ProductCatalog.Features.GetProducts; + +[ApiVersion(1.0)] +public sealed class GetProductsController(IMessageBus bus) : ApiControllerBase +{ + /// Returns a filtered, paginated product list including search facets. + [HttpGet] + [RequirePermission(Permission.Products.Read)] + [OutputCache(PolicyName = CacheTags.Products)] + public async Task> GetAll( + [FromQuery] ProductFilter filter, + CancellationToken ct + ) + { + ErrorOr result = await bus.InvokeAsync>( + new GetProductsQuery(filter), + ct + ); + return result.ToActionResult(this); + } +} diff --git a/src/Modules/ProductCatalog/Features/Product/Queries/GetProductsQuery.cs b/src/Modules/ProductCatalog/Features/GetProducts/GetProductsQuery.cs similarity index 89% rename from src/Modules/ProductCatalog/Features/Product/Queries/GetProductsQuery.cs rename to src/Modules/ProductCatalog/Features/GetProducts/GetProductsQuery.cs index 0d10e3a2..7fb3bb13 100644 --- a/src/Modules/ProductCatalog/Features/Product/Queries/GetProductsQuery.cs +++ b/src/Modules/ProductCatalog/Features/GetProducts/GetProductsQuery.cs @@ -1,7 +1,7 @@ using ErrorOr; -using ProductRepositoryContract = ProductCatalog.Features.Product.Repositories.IProductRepository; +using ProductRepositoryContract = ProductCatalog.Interfaces.IProductRepository; -namespace ProductCatalog.Features.Product; +namespace ProductCatalog.Features.GetProducts; /// Retrieves a filtered, sorted, and paged list of products together with search facets. public sealed record GetProductsQuery(ProductFilter Filter); @@ -33,4 +33,3 @@ CancellationToken ct ); } } - diff --git a/src/Modules/ProductCatalog/Features/Product/DTOs/ProductFilter.cs b/src/Modules/ProductCatalog/Features/GetProducts/ProductFilter.cs similarity index 93% rename from src/Modules/ProductCatalog/Features/Product/DTOs/ProductFilter.cs rename to src/Modules/ProductCatalog/Features/GetProducts/ProductFilter.cs index 0b39c4e0..d578fdc6 100644 --- a/src/Modules/ProductCatalog/Features/Product/DTOs/ProductFilter.cs +++ b/src/Modules/ProductCatalog/Features/GetProducts/ProductFilter.cs @@ -1,7 +1,7 @@ using SharedKernel.Application.Contracts; using SharedKernel.Application.DTOs; -namespace ProductCatalog.Features.Product.DTOs; +namespace ProductCatalog.Features.GetProducts; /// /// Encapsulates all criteria available for querying and paging the product list, including text search, price range, date range, category filtering, and sorting. @@ -20,6 +20,3 @@ public sealed record ProductFilter( string? Query = null, IReadOnlyCollection? CategoryIds = null ) : PaginationFilter(PageNumber, PageSize), IDateRangeFilter, ISortableFilter; - - - diff --git a/src/Modules/ProductCatalog/Features/Product/Specifications/ProductFilterCriteria.cs b/src/Modules/ProductCatalog/Features/GetProducts/ProductFilterCriteria.cs similarity index 97% rename from src/Modules/ProductCatalog/Features/Product/Specifications/ProductFilterCriteria.cs rename to src/Modules/ProductCatalog/Features/GetProducts/ProductFilterCriteria.cs index ba8cbe9c..aae9d801 100644 --- a/src/Modules/ProductCatalog/Features/Product/Specifications/ProductFilterCriteria.cs +++ b/src/Modules/ProductCatalog/Features/GetProducts/ProductFilterCriteria.cs @@ -3,7 +3,7 @@ using Microsoft.EntityFrameworkCore; using ProductEntity = ProductCatalog.Entities.Product; -namespace ProductCatalog.Features.Product.Specifications; +namespace ProductCatalog.Features.GetProducts; /// /// Internal helper that extends with product-specific filter predicates, centralising all WHERE-clause logic for reuse across multiple specifications. @@ -72,6 +72,3 @@ internal sealed record ProductFilterCriteriaOptions( { internal static ProductFilterCriteriaOptions Default => new(); } - - - diff --git a/src/Modules/ProductCatalog/Features/Product/Validation/ProductFilterValidator.cs b/src/Modules/ProductCatalog/Features/GetProducts/ProductFilterValidator.cs similarity index 96% rename from src/Modules/ProductCatalog/Features/Product/Validation/ProductFilterValidator.cs rename to src/Modules/ProductCatalog/Features/GetProducts/ProductFilterValidator.cs index 9befe46c..8870d9ca 100644 --- a/src/Modules/ProductCatalog/Features/Product/Validation/ProductFilterValidator.cs +++ b/src/Modules/ProductCatalog/Features/GetProducts/ProductFilterValidator.cs @@ -1,7 +1,7 @@ using SharedKernel.Application.Validation; using FluentValidation; -namespace ProductCatalog.Features.Product.Validation; +namespace ProductCatalog.Features.GetProducts; /// /// FluentValidation validator for ; composes pagination, date-range, sortable-field, and price-range rules including cross-field MinPrice/MaxPrice consistency. @@ -34,6 +34,3 @@ public ProductFilterValidator() .WithMessage("CategoryIds cannot contain an empty value."); } } - - - diff --git a/src/Modules/ProductCatalog/Features/Product/Specifications/ProductSpecification.cs b/src/Modules/ProductCatalog/Features/GetProducts/ProductSpecification.cs similarity index 84% rename from src/Modules/ProductCatalog/Features/Product/Specifications/ProductSpecification.cs rename to src/Modules/ProductCatalog/Features/GetProducts/ProductSpecification.cs index dc04089a..202e4824 100644 --- a/src/Modules/ProductCatalog/Features/Product/Specifications/ProductSpecification.cs +++ b/src/Modules/ProductCatalog/Features/GetProducts/ProductSpecification.cs @@ -1,8 +1,7 @@ -using ProductCatalog.Features.Product.Mappings; using Ardalis.Specification; using ProductEntity = ProductCatalog.Entities.Product; -namespace ProductCatalog.Features.Product.Specifications; +namespace ProductCatalog.Features.GetProducts; /// /// Ardalis specification that applies the full product filter, sorting, and projection to produce a list. @@ -19,6 +18,3 @@ public ProductSpecification(ProductFilter filter) Query.Select(ProductMappings.Projection); } } - - - diff --git a/src/Modules/ProductCatalog/Features/Product/Commands/IdempotentCreateCommand.cs b/src/Modules/ProductCatalog/Features/IdempotentCreate/IdempotentCreateCommand.cs similarity index 88% rename from src/Modules/ProductCatalog/Features/Product/Commands/IdempotentCreateCommand.cs rename to src/Modules/ProductCatalog/Features/IdempotentCreate/IdempotentCreateCommand.cs index be287f74..7f7c0009 100644 --- a/src/Modules/ProductCatalog/Features/Product/Commands/IdempotentCreateCommand.cs +++ b/src/Modules/ProductCatalog/Features/IdempotentCreate/IdempotentCreateCommand.cs @@ -1,10 +1,10 @@ using ErrorOr; using ProductCatalog; using ProductCatalog.ValueObjects; -using IProductRepository = ProductCatalog.Features.Product.Repositories.IProductRepository; +using IProductRepository = ProductCatalog.Interfaces.IProductRepository; using ProductEntity = ProductCatalog.Entities.Product; -namespace ProductCatalog.Features.Product.Commands; +namespace ProductCatalog.Features.IdempotentCreate; public sealed record IdempotentCreateCommand(IdempotentCreateRequest Request); @@ -41,4 +41,3 @@ await unitOfWork.ExecuteInTransactionAsync( ); } } - diff --git a/src/Modules/ProductCatalog/Controllers/V1/IdempotentController.cs b/src/Modules/ProductCatalog/Features/IdempotentCreate/IdempotentCreateController.cs similarity index 82% rename from src/Modules/ProductCatalog/Controllers/V1/IdempotentController.cs rename to src/Modules/ProductCatalog/Features/IdempotentCreate/IdempotentCreateController.cs index 3df96086..ff4916c2 100644 --- a/src/Modules/ProductCatalog/Controllers/V1/IdempotentController.cs +++ b/src/Modules/ProductCatalog/Features/IdempotentCreate/IdempotentCreateController.cs @@ -1,21 +1,19 @@ using Asp.Versioning; using ErrorOr; using Microsoft.AspNetCore.Mvc; -using ProductCatalog.Features.Product.Commands; -using ProductCatalog.Features.Product.DTOs; using SharedKernel.Contracts.Api; using SharedKernel.Contracts.Api.Filters.Idempotency; using SharedKernel.Contracts.Security; using Wolverine; -namespace ProductCatalog.Controllers.V1; +namespace ProductCatalog.Features.IdempotentCreate; [ApiVersion(1.0)] /// /// Presentation-layer controller that demonstrates idempotent POST semantics using the /// action filter to detect and short-circuit duplicate requests. /// -public sealed class IdempotentController(IMessageBus bus) : ApiControllerBase +public sealed class IdempotentCreateController(IMessageBus bus) : ApiControllerBase { [HttpPost] [Idempotent] @@ -34,4 +32,3 @@ CancellationToken ct return Ok(result.Value); } } - diff --git a/src/Modules/ProductCatalog/Features/Product/DTOs/IdempotentCreateRequest.cs b/src/Modules/ProductCatalog/Features/IdempotentCreate/IdempotentCreateRequest.cs similarity index 87% rename from src/Modules/ProductCatalog/Features/Product/DTOs/IdempotentCreateRequest.cs rename to src/Modules/ProductCatalog/Features/IdempotentCreate/IdempotentCreateRequest.cs index d73eaf7c..e537ea82 100644 --- a/src/Modules/ProductCatalog/Features/Product/DTOs/IdempotentCreateRequest.cs +++ b/src/Modules/ProductCatalog/Features/IdempotentCreate/IdempotentCreateRequest.cs @@ -1,7 +1,7 @@ using System.ComponentModel.DataAnnotations; using SharedKernel.Application.Validation; -namespace ProductCatalog.Features.Product.DTOs; +namespace ProductCatalog.Features.IdempotentCreate; /// /// Carries the data for an idempotent resource creation request; demonstrates safe-retry semantics at the API layer. @@ -10,4 +10,3 @@ public sealed record IdempotentCreateRequest( [NotEmpty] [MaxLength(200)] string Name, string? Description ); - diff --git a/src/Modules/ProductCatalog/Features/Product/Validation/IdempotentCreateRequestValidator.cs b/src/Modules/ProductCatalog/Features/IdempotentCreate/IdempotentCreateRequestValidator.cs similarity index 77% rename from src/Modules/ProductCatalog/Features/Product/Validation/IdempotentCreateRequestValidator.cs rename to src/Modules/ProductCatalog/Features/IdempotentCreate/IdempotentCreateRequestValidator.cs index 04a0f207..77e97967 100644 --- a/src/Modules/ProductCatalog/Features/Product/Validation/IdempotentCreateRequestValidator.cs +++ b/src/Modules/ProductCatalog/Features/IdempotentCreate/IdempotentCreateRequestValidator.cs @@ -1,7 +1,6 @@ -using ProductCatalog.Features.Product.DTOs; using SharedKernel.Application.Validation; -namespace ProductCatalog.Features.Product.Validation; +namespace ProductCatalog.Features.IdempotentCreate; /// /// FluentValidation validator for that enforces @@ -9,4 +8,3 @@ namespace ProductCatalog.Features.Product.Validation; /// public sealed class IdempotentCreateRequestValidator : DataAnnotationsValidator; - diff --git a/src/Modules/ProductCatalog/Features/Product/DTOs/IdempotentCreateResponse.cs b/src/Modules/ProductCatalog/Features/IdempotentCreate/IdempotentCreateResponse.cs similarity index 85% rename from src/Modules/ProductCatalog/Features/Product/DTOs/IdempotentCreateResponse.cs rename to src/Modules/ProductCatalog/Features/IdempotentCreate/IdempotentCreateResponse.cs index e2e45192..2b477d51 100644 --- a/src/Modules/ProductCatalog/Features/Product/DTOs/IdempotentCreateResponse.cs +++ b/src/Modules/ProductCatalog/Features/IdempotentCreate/IdempotentCreateResponse.cs @@ -1,6 +1,6 @@ using SharedKernel.Domain.Entities.Contracts; -namespace ProductCatalog.Features.Product.DTOs; +namespace ProductCatalog.Features.IdempotentCreate; /// /// Represents the persisted resource returned after a successful idempotent create operation. @@ -11,4 +11,3 @@ public sealed record IdempotentCreateResponse( string? Description, DateTime CreatedAtUtc ) : IHasId; - diff --git a/src/Modules/ProductCatalog/Features/Product/Commands/PatchProductCommand.cs b/src/Modules/ProductCatalog/Features/PatchProduct/PatchProductCommand.cs similarity index 92% rename from src/Modules/ProductCatalog/Features/Product/Commands/PatchProductCommand.cs rename to src/Modules/ProductCatalog/Features/PatchProduct/PatchProductCommand.cs index bd37bd3c..04bd84e9 100644 --- a/src/Modules/ProductCatalog/Features/Product/Commands/PatchProductCommand.cs +++ b/src/Modules/ProductCatalog/Features/PatchProduct/PatchProductCommand.cs @@ -1,13 +1,12 @@ using ErrorOr; using FluentValidation; using ProductCatalog; -using ProductCatalog.Features.Product.Mappings; using ProductCatalog.ValueObjects; using SystemTextJsonPatch; using Wolverine; -using IProductRepository = ProductCatalog.Features.Product.Repositories.IProductRepository; +using IProductRepository = ProductCatalog.Interfaces.IProductRepository; -namespace ProductCatalog.Features.Product.Commands; +namespace ProductCatalog.Features.PatchProduct; public sealed record PatchProductCommand( Guid Id, diff --git a/src/Modules/ProductCatalog/Controllers/V1/PatchController.cs b/src/Modules/ProductCatalog/Features/PatchProduct/PatchProductController.cs similarity index 65% rename from src/Modules/ProductCatalog/Controllers/V1/PatchController.cs rename to src/Modules/ProductCatalog/Features/PatchProduct/PatchProductController.cs index 4844b097..8480808e 100644 --- a/src/Modules/ProductCatalog/Controllers/V1/PatchController.cs +++ b/src/Modules/ProductCatalog/Features/PatchProduct/PatchProductController.cs @@ -1,21 +1,15 @@ using Asp.Versioning; using ErrorOr; using Microsoft.AspNetCore.Mvc; -using ProductCatalog.Features.Product.Commands; -using ProductCatalog.Features.Product.DTOs; using SharedKernel.Contracts.Api; using SharedKernel.Contracts.Security; using SystemTextJsonPatch; using Wolverine; -namespace ProductCatalog.Controllers.V1; +namespace ProductCatalog.Features.PatchProduct; [ApiVersion(1.0)] -/// -/// Presentation-layer controller that demonstrates JSON Patch (RFC 6902) support -/// for partial product updates using SystemTextJsonPatch. -/// -public sealed class PatchController(IMessageBus bus) : ApiControllerBase +public sealed class PatchProductController(IMessageBus bus) : ApiControllerBase { [HttpPatch("products/{id:guid}")] [RequirePermission(Permission.Examples.Update)] @@ -32,4 +26,3 @@ CancellationToken ct return result.ToActionResult(this); } } - diff --git a/src/Modules/ProductCatalog/Features/Product/DTOs/PatchableProductDto.cs b/src/Modules/ProductCatalog/Features/PatchProduct/PatchableProductDto.cs similarity index 92% rename from src/Modules/ProductCatalog/Features/Product/DTOs/PatchableProductDto.cs rename to src/Modules/ProductCatalog/Features/PatchProduct/PatchableProductDto.cs index cc92eba2..f1fd4bca 100644 --- a/src/Modules/ProductCatalog/Features/Product/DTOs/PatchableProductDto.cs +++ b/src/Modules/ProductCatalog/Features/PatchProduct/PatchableProductDto.cs @@ -1,6 +1,6 @@ using System.ComponentModel.DataAnnotations; -namespace ProductCatalog.Features.Product.DTOs; +namespace ProductCatalog.Features.PatchProduct; /// /// Mutable DTO used as the patch target for JSON Patch operations on a product; declared as a class @@ -20,4 +20,3 @@ public sealed class PatchableProductDto public Guid? CategoryId { get; set; } } - diff --git a/src/Modules/ProductCatalog/Features/Product/Validation/PatchableProductDtoValidator.cs b/src/Modules/ProductCatalog/Features/PatchProduct/PatchableProductDtoValidator.cs similarity index 77% rename from src/Modules/ProductCatalog/Features/Product/Validation/PatchableProductDtoValidator.cs rename to src/Modules/ProductCatalog/Features/PatchProduct/PatchableProductDtoValidator.cs index fe4ceeb9..ca5ea96a 100644 --- a/src/Modules/ProductCatalog/Features/Product/Validation/PatchableProductDtoValidator.cs +++ b/src/Modules/ProductCatalog/Features/PatchProduct/PatchableProductDtoValidator.cs @@ -1,9 +1,7 @@ using FluentValidation; -using ProductCatalog.Features.Product.DTOs; -using ProductCatalog.Features.Product.Validation; using SharedKernel.Application.Validation; -namespace ProductCatalog.Features.Product.Validation; +namespace ProductCatalog.Features.PatchProduct; /// /// FluentValidation validator for the post-patch state; @@ -16,4 +14,3 @@ public PatchableProductDtoValidator() RuleFor(x => x.Description).RequiredAbovePriceThreshold(x => x.Price); } } - diff --git a/src/Modules/ProductCatalog/Features/Category/Specifications/CategoriesByIdsSpecification.cs b/src/Modules/ProductCatalog/Features/Shared/CategoriesByIdsSpecification.cs similarity index 88% rename from src/Modules/ProductCatalog/Features/Category/Specifications/CategoriesByIdsSpecification.cs rename to src/Modules/ProductCatalog/Features/Shared/CategoriesByIdsSpecification.cs index 35325ae8..2aa5466e 100644 --- a/src/Modules/ProductCatalog/Features/Category/Specifications/CategoriesByIdsSpecification.cs +++ b/src/Modules/ProductCatalog/Features/Shared/CategoriesByIdsSpecification.cs @@ -1,7 +1,7 @@ using Ardalis.Specification; using CategoryEntity = ProductCatalog.Entities.Category; -namespace ProductCatalog.Features.Category.Specifications; +namespace ProductCatalog.Features.Shared; /// /// Ardalis specification that loads multiple categories by their IDs, used for batch update and delete operations. @@ -13,6 +13,3 @@ public CategoriesByIdsSpecification(IReadOnlyCollection ids) Query.Where(category => ids.Contains(category.Id)); } } - - - diff --git a/src/Modules/ProductCatalog/Features/Category/Mappings/CategoryMappings.cs b/src/Modules/ProductCatalog/Features/Shared/CategoryMappings.cs similarity index 69% rename from src/Modules/ProductCatalog/Features/Category/Mappings/CategoryMappings.cs rename to src/Modules/ProductCatalog/Features/Shared/CategoryMappings.cs index 08910b9c..136986d7 100644 --- a/src/Modules/ProductCatalog/Features/Category/Mappings/CategoryMappings.cs +++ b/src/Modules/ProductCatalog/Features/Shared/CategoryMappings.cs @@ -1,8 +1,7 @@ using System.Linq.Expressions; using CategoryEntity = ProductCatalog.Entities.Category; -using ProductCategoryStatsEntity = ProductCatalog.Entities.ProductCategoryStats; -namespace ProductCatalog.Features.Category.Mappings; +namespace ProductCatalog.Features.Shared; /// /// Provides mapping utilities between category domain entities and their response DTOs. @@ -28,17 +27,4 @@ public static class CategoryMappings /// Maps a to a using the compiled projection. public static CategoryResponse ToResponse(this CategoryEntity category) => CompiledProjection(category); - - /// Maps a to a . - public static ProductCategoryStatsResponse ToResponse(this ProductCategoryStatsEntity stats) => - new( - stats.CategoryId, - stats.CategoryName, - stats.ProductCount, - stats.AveragePrice, - stats.TotalReviews - ); } - - - diff --git a/src/Modules/ProductCatalog/Features/Category/DTOs/CategoryResponse.cs b/src/Modules/ProductCatalog/Features/Shared/CategoryResponse.cs similarity index 83% rename from src/Modules/ProductCatalog/Features/Category/DTOs/CategoryResponse.cs rename to src/Modules/ProductCatalog/Features/Shared/CategoryResponse.cs index 54408c4e..f9e9b4c1 100644 --- a/src/Modules/ProductCatalog/Features/Category/DTOs/CategoryResponse.cs +++ b/src/Modules/ProductCatalog/Features/Shared/CategoryResponse.cs @@ -1,4 +1,4 @@ -namespace ProductCatalog.Features.Category.DTOs; +namespace ProductCatalog.Features.Shared; /// /// Read model returned by category queries, containing the public-facing representation of a category. @@ -9,6 +9,3 @@ public sealed record CategoryResponse( string? Description, DateTime CreatedAtUtc ) : IHasId; - - - diff --git a/src/Modules/ProductCatalog/Features/Category/CategorySortFields.cs b/src/Modules/ProductCatalog/Features/Shared/CategorySortFields.cs similarity index 95% rename from src/Modules/ProductCatalog/Features/Category/CategorySortFields.cs rename to src/Modules/ProductCatalog/Features/Shared/CategorySortFields.cs index b1ad2dae..78b05347 100644 --- a/src/Modules/ProductCatalog/Features/Category/CategorySortFields.cs +++ b/src/Modules/ProductCatalog/Features/Shared/CategorySortFields.cs @@ -1,7 +1,7 @@ using SharedKernel.Application.Sorting; using CategoryEntity = ProductCatalog.Entities.Category; -namespace ProductCatalog.Features.Category; +namespace ProductCatalog.Features.Shared; /// /// Defines the allowed sort fields for category queries and maps them to entity expressions. @@ -23,6 +23,3 @@ public static class CategorySortFields .Add(CreatedAt, c => c.Audit.CreatedAtUtc) .Default(c => c.Audit.CreatedAtUtc); } - - - diff --git a/src/Modules/ProductCatalog/Features/Product/DTOs/IProductRequest.cs b/src/Modules/ProductCatalog/Features/Shared/IProductRequest.cs similarity index 89% rename from src/Modules/ProductCatalog/Features/Product/DTOs/IProductRequest.cs rename to src/Modules/ProductCatalog/Features/Shared/IProductRequest.cs index a44a260c..5e039fca 100644 --- a/src/Modules/ProductCatalog/Features/Product/DTOs/IProductRequest.cs +++ b/src/Modules/ProductCatalog/Features/Shared/IProductRequest.cs @@ -1,4 +1,4 @@ -namespace ProductCatalog.Features.Product.DTOs; +namespace ProductCatalog.Features.Shared; /// /// Shared contract for create and update product command requests, enabling reuse of @@ -12,4 +12,3 @@ public interface IProductRequest Guid? CategoryId { get; } IReadOnlyCollection? ProductDataIds { get; } } - diff --git a/src/Modules/ProductCatalog/Features/Product/Specifications/ProductCategoryFacetSpecification.cs b/src/Modules/ProductCatalog/Features/Shared/ProductCategoryFacetSpecification.cs similarity index 87% rename from src/Modules/ProductCatalog/Features/Product/Specifications/ProductCategoryFacetSpecification.cs rename to src/Modules/ProductCatalog/Features/Shared/ProductCategoryFacetSpecification.cs index 12334665..a702e707 100644 --- a/src/Modules/ProductCatalog/Features/Product/Specifications/ProductCategoryFacetSpecification.cs +++ b/src/Modules/ProductCatalog/Features/Shared/ProductCategoryFacetSpecification.cs @@ -1,7 +1,8 @@ using Ardalis.Specification; +using ProductCatalog.Features.GetProducts; using ProductEntity = ProductCatalog.Entities.Product; -namespace ProductCatalog.Features.Product.Specifications; +namespace ProductCatalog.Features.Shared; /// /// Ardalis specification used for the category facet query; applies all filter criteria except category-ID filtering so that counts reflect the full category distribution. @@ -15,6 +16,3 @@ public ProductCategoryFacetSpecification(ProductFilter filter) Query.AsNoTracking(); } } - - - diff --git a/src/Modules/ProductCatalog/Features/Product/DTOs/ProductCategoryFacetValue.cs b/src/Modules/ProductCatalog/Features/Shared/ProductCategoryFacetValue.cs similarity index 84% rename from src/Modules/ProductCatalog/Features/Product/DTOs/ProductCategoryFacetValue.cs rename to src/Modules/ProductCatalog/Features/Shared/ProductCategoryFacetValue.cs index 6c2ea2a0..3193416e 100644 --- a/src/Modules/ProductCatalog/Features/Product/DTOs/ProductCategoryFacetValue.cs +++ b/src/Modules/ProductCatalog/Features/Shared/ProductCategoryFacetValue.cs @@ -1,9 +1,6 @@ -namespace ProductCatalog.Features.Product.DTOs; +namespace ProductCatalog.Features.Shared; /// /// Represents a single category bucket in the product search facets, containing the category identity and the number of matching products. /// public sealed record ProductCategoryFacetValue(Guid? CategoryId, string CategoryName, int Count); - - - diff --git a/src/Modules/ProductCatalog/Features/ProductData/Mappings/ProductDataMappings.cs b/src/Modules/ProductCatalog/Features/Shared/ProductDataMappings.cs similarity index 97% rename from src/Modules/ProductCatalog/Features/ProductData/Mappings/ProductDataMappings.cs rename to src/Modules/ProductCatalog/Features/Shared/ProductDataMappings.cs index 848ba003..ceef7536 100644 --- a/src/Modules/ProductCatalog/Features/ProductData/Mappings/ProductDataMappings.cs +++ b/src/Modules/ProductCatalog/Features/Shared/ProductDataMappings.cs @@ -2,7 +2,7 @@ using ProductDataEntity = ProductCatalog.Entities.ProductData.ProductData; using VideoProductDataEntity = ProductCatalog.Entities.ProductData.VideoProductData; -namespace ProductCatalog.Features.ProductData.Mappings; +namespace ProductCatalog.Features.Shared; /// /// Provides mapping utilities from product data domain entities to their polymorphic response DTOs. @@ -62,6 +62,3 @@ private static VideoProductDataResponse ToVideoResponse(this VideoProductDataEnt "video" ); } - - - diff --git a/src/Modules/ProductCatalog/Features/ProductData/DTOs/ProductDataResponse.cs b/src/Modules/ProductCatalog/Features/Shared/ProductDataResponse.cs similarity index 96% rename from src/Modules/ProductCatalog/Features/ProductData/DTOs/ProductDataResponse.cs rename to src/Modules/ProductCatalog/Features/Shared/ProductDataResponse.cs index ba016956..9a813234 100644 --- a/src/Modules/ProductCatalog/Features/ProductData/DTOs/ProductDataResponse.cs +++ b/src/Modules/ProductCatalog/Features/Shared/ProductDataResponse.cs @@ -1,6 +1,6 @@ using System.Text.Json.Serialization; -namespace ProductCatalog.Features.ProductData.DTOs; +namespace ProductCatalog.Features.Shared; /// /// Abstract base read model for product data, serialised as a polymorphic type using the type discriminator. @@ -36,6 +36,3 @@ public sealed record VideoProductDataResponse : ProductDataResponse public int DurationSeconds { get; init; } public string? Resolution { get; init; } } - - - diff --git a/src/Modules/ProductCatalog/Features/Product/Mappings/ProductMappings.cs b/src/Modules/ProductCatalog/Features/Shared/ProductMappings.cs similarity index 95% rename from src/Modules/ProductCatalog/Features/Product/Mappings/ProductMappings.cs rename to src/Modules/ProductCatalog/Features/Shared/ProductMappings.cs index d0c7923c..a23cd538 100644 --- a/src/Modules/ProductCatalog/Features/Product/Mappings/ProductMappings.cs +++ b/src/Modules/ProductCatalog/Features/Shared/ProductMappings.cs @@ -1,7 +1,7 @@ using System.Linq.Expressions; using ProductEntity = ProductCatalog.Entities.Product; -namespace ProductCatalog.Features.Product.Mappings; +namespace ProductCatalog.Features.Shared; /// /// Provides EF Core-compatible projection expressions and in-memory mapping helpers for converting Product domain entities to DTOs. @@ -29,6 +29,3 @@ public static class ProductMappings public static ProductResponse ToResponse(this ProductEntity product) => CompiledProjection(product); } - - - diff --git a/src/Modules/ProductCatalog/Features/Product/DTOs/ProductPriceFacetBucketResponse.cs b/src/Modules/ProductCatalog/Features/Shared/ProductPriceFacetBucketResponse.cs similarity index 85% rename from src/Modules/ProductCatalog/Features/Product/DTOs/ProductPriceFacetBucketResponse.cs rename to src/Modules/ProductCatalog/Features/Shared/ProductPriceFacetBucketResponse.cs index ff330753..eff961fd 100644 --- a/src/Modules/ProductCatalog/Features/Product/DTOs/ProductPriceFacetBucketResponse.cs +++ b/src/Modules/ProductCatalog/Features/Shared/ProductPriceFacetBucketResponse.cs @@ -1,4 +1,4 @@ -namespace ProductCatalog.Features.Product.DTOs; +namespace ProductCatalog.Features.Shared; /// /// Represents a single price-range bucket in the product search facets, with a human-readable label and the count of matching products. @@ -9,6 +9,3 @@ public sealed record ProductPriceFacetBucketResponse( decimal? MaxPrice, int Count ); - - - diff --git a/src/Modules/ProductCatalog/Features/Product/Specifications/ProductPriceFacetSpecification.cs b/src/Modules/ProductCatalog/Features/Shared/ProductPriceFacetSpecification.cs similarity index 87% rename from src/Modules/ProductCatalog/Features/Product/Specifications/ProductPriceFacetSpecification.cs rename to src/Modules/ProductCatalog/Features/Shared/ProductPriceFacetSpecification.cs index 112e2092..02e185e2 100644 --- a/src/Modules/ProductCatalog/Features/Product/Specifications/ProductPriceFacetSpecification.cs +++ b/src/Modules/ProductCatalog/Features/Shared/ProductPriceFacetSpecification.cs @@ -1,7 +1,8 @@ using Ardalis.Specification; +using ProductCatalog.Features.GetProducts; using ProductEntity = ProductCatalog.Entities.Product; -namespace ProductCatalog.Features.Product.Specifications; +namespace ProductCatalog.Features.Shared; /// /// Ardalis specification used for the price facet query; applies all filter criteria except the price range so that all price buckets remain visible regardless of the selected price filter. @@ -15,6 +16,3 @@ public ProductPriceFacetSpecification(ProductFilter filter) Query.AsNoTracking(); } } - - - diff --git a/src/Modules/ProductCatalog/Features/Product/Validation/ProductRequestValidatorBase.cs b/src/Modules/ProductCatalog/Features/Shared/ProductRequestValidatorBase.cs similarity index 96% rename from src/Modules/ProductCatalog/Features/Product/Validation/ProductRequestValidatorBase.cs rename to src/Modules/ProductCatalog/Features/Shared/ProductRequestValidatorBase.cs index eae45db6..9247833a 100644 --- a/src/Modules/ProductCatalog/Features/Product/Validation/ProductRequestValidatorBase.cs +++ b/src/Modules/ProductCatalog/Features/Shared/ProductRequestValidatorBase.cs @@ -1,7 +1,7 @@ using SharedKernel.Application.Validation; using FluentValidation; -namespace ProductCatalog.Features.Product.Validation; +namespace ProductCatalog.Features.Shared; /// /// Shared FluentValidation extension methods and constants for product-related validation rules. @@ -36,6 +36,3 @@ protected ProductRequestValidatorBase() RuleFor(x => x.Description).RequiredAbovePriceThreshold(x => x.Price); } } - - - diff --git a/src/Modules/ProductCatalog/Features/Product/DTOs/ProductResponse.cs b/src/Modules/ProductCatalog/Features/Shared/ProductResponse.cs similarity index 87% rename from src/Modules/ProductCatalog/Features/Product/DTOs/ProductResponse.cs rename to src/Modules/ProductCatalog/Features/Shared/ProductResponse.cs index 021c0f36..3e9e9589 100644 --- a/src/Modules/ProductCatalog/Features/Product/DTOs/ProductResponse.cs +++ b/src/Modules/ProductCatalog/Features/Shared/ProductResponse.cs @@ -1,4 +1,4 @@ -namespace ProductCatalog.Features.Product.DTOs; +namespace ProductCatalog.Features.Shared; /// /// Represents a product as returned by the Application layer to API consumers, projected from the domain entity. @@ -12,6 +12,3 @@ public sealed record ProductResponse( DateTime CreatedAtUtc, IReadOnlyCollection ProductDataIds ) : IHasId; - - - diff --git a/src/Modules/ProductCatalog/Features/Product/DTOs/ProductSearchFacetsResponse.cs b/src/Modules/ProductCatalog/Features/Shared/ProductSearchFacetsResponse.cs similarity index 86% rename from src/Modules/ProductCatalog/Features/Product/DTOs/ProductSearchFacetsResponse.cs rename to src/Modules/ProductCatalog/Features/Shared/ProductSearchFacetsResponse.cs index 5a071166..da521e1f 100644 --- a/src/Modules/ProductCatalog/Features/Product/DTOs/ProductSearchFacetsResponse.cs +++ b/src/Modules/ProductCatalog/Features/Shared/ProductSearchFacetsResponse.cs @@ -1,4 +1,4 @@ -namespace ProductCatalog.Features.Product.DTOs; +namespace ProductCatalog.Features.Shared; /// /// Aggregates all facet data returned alongside a product search result, enabling client-side filter refinement. @@ -7,6 +7,3 @@ public sealed record ProductSearchFacetsResponse( IReadOnlyCollection Categories, IReadOnlyCollection PriceBuckets ); - - - diff --git a/src/Modules/ProductCatalog/Features/Product/ProductSortFields.cs b/src/Modules/ProductCatalog/Features/Shared/ProductSortFields.cs similarity index 94% rename from src/Modules/ProductCatalog/Features/Product/ProductSortFields.cs rename to src/Modules/ProductCatalog/Features/Shared/ProductSortFields.cs index 86f772c1..dec9e07a 100644 --- a/src/Modules/ProductCatalog/Features/Product/ProductSortFields.cs +++ b/src/Modules/ProductCatalog/Features/Shared/ProductSortFields.cs @@ -1,7 +1,7 @@ using SharedKernel.Application.Sorting; using ProductEntity = ProductCatalog.Entities.Product; -namespace ProductCatalog.Features.Product; +namespace ProductCatalog.Features.Shared; /// /// Defines the allowed sort fields for product queries and provides the used by specifications to apply ordering. @@ -18,6 +18,3 @@ public static class ProductSortFields .Add(CreatedAt, p => p.Audit.CreatedAtUtc) .Default(p => p.Audit.CreatedAtUtc); } - - - diff --git a/src/Modules/ProductCatalog/Features/Product/ProductValidationHelper.cs b/src/Modules/ProductCatalog/Features/Shared/ProductValidationHelper.cs similarity index 97% rename from src/Modules/ProductCatalog/Features/Product/ProductValidationHelper.cs rename to src/Modules/ProductCatalog/Features/Shared/ProductValidationHelper.cs index 4554bb17..d7d9ee49 100644 --- a/src/Modules/ProductCatalog/Features/Product/ProductValidationHelper.cs +++ b/src/Modules/ProductCatalog/Features/Shared/ProductValidationHelper.cs @@ -1,6 +1,6 @@ using SharedKernel.Application.Batch; -namespace ProductCatalog.Features.Product; +namespace ProductCatalog.Features.Shared; /// Shared validation methods for product commands. internal static class ProductValidationHelper @@ -59,7 +59,7 @@ CancellationToken ct return []; List existing = await categoryRepository.ListAsync( - new Category.Specifications.CategoriesByIdsSpecification(allCategoryIds), + new CategoriesByIdsSpecification(allCategoryIds), ct ); allCategoryIds.ExceptWith(existing.Select(c => c.Id)); diff --git a/src/Modules/ProductCatalog/Features/Product/Specifications/ProductsByIdsWithLinksSpecification.cs b/src/Modules/ProductCatalog/Features/Shared/ProductsByIdsWithLinksSpecification.cs similarity index 90% rename from src/Modules/ProductCatalog/Features/Product/Specifications/ProductsByIdsWithLinksSpecification.cs rename to src/Modules/ProductCatalog/Features/Shared/ProductsByIdsWithLinksSpecification.cs index 5c547b53..18c82233 100644 --- a/src/Modules/ProductCatalog/Features/Product/Specifications/ProductsByIdsWithLinksSpecification.cs +++ b/src/Modules/ProductCatalog/Features/Shared/ProductsByIdsWithLinksSpecification.cs @@ -1,7 +1,7 @@ using Ardalis.Specification; using ProductEntity = ProductCatalog.Entities.Product; -namespace ProductCatalog.Features.Product.Specifications; +namespace ProductCatalog.Features.Shared; /// /// Ardalis specification that loads multiple products by their IDs and eagerly includes @@ -16,6 +16,3 @@ public ProductsByIdsWithLinksSpecification(IReadOnlyCollection ids) .Include(product => product.ProductDataLinks); } } - - - diff --git a/src/Modules/ProductCatalog/Features/Product/DTOs/ProductsResponse.cs b/src/Modules/ProductCatalog/Features/Shared/ProductsResponse.cs similarity index 86% rename from src/Modules/ProductCatalog/Features/Product/DTOs/ProductsResponse.cs rename to src/Modules/ProductCatalog/Features/Shared/ProductsResponse.cs index 2037f183..9d457e51 100644 --- a/src/Modules/ProductCatalog/Features/Product/DTOs/ProductsResponse.cs +++ b/src/Modules/ProductCatalog/Features/Shared/ProductsResponse.cs @@ -1,4 +1,4 @@ -namespace ProductCatalog.Features.Product.DTOs; +namespace ProductCatalog.Features.Shared; /// /// Combines a paged list of products with their associated search facets in a single response envelope. @@ -7,6 +7,3 @@ public sealed record ProductsResponse( PagedResponse Page, ProductSearchFacetsResponse Facets ) : IPagedItems, IHasFacets; - - - diff --git a/src/Modules/ProductCatalog/Features/Tenant/Specifications/CategoriesForTenantSoftDeleteSpecification.cs b/src/Modules/ProductCatalog/Features/TenantCascadeDelete/CategoriesForTenantSoftDeleteSpecification.cs similarity index 91% rename from src/Modules/ProductCatalog/Features/Tenant/Specifications/CategoriesForTenantSoftDeleteSpecification.cs rename to src/Modules/ProductCatalog/Features/TenantCascadeDelete/CategoriesForTenantSoftDeleteSpecification.cs index 04a522c2..525dc0cb 100644 --- a/src/Modules/ProductCatalog/Features/Tenant/Specifications/CategoriesForTenantSoftDeleteSpecification.cs +++ b/src/Modules/ProductCatalog/Features/TenantCascadeDelete/CategoriesForTenantSoftDeleteSpecification.cs @@ -1,7 +1,7 @@ using Ardalis.Specification; using CategoryEntity = ProductCatalog.Entities.Category; -namespace ProductCatalog.Features.Tenant.Specifications; +namespace ProductCatalog.Features.TenantCascadeDelete; /// /// Loads all non-deleted categories for a specific tenant, bypassing global query filters @@ -16,4 +16,3 @@ public CategoriesForTenantSoftDeleteSpecification(Guid tenantId) .IgnoreQueryFilters(); } } - diff --git a/src/Modules/ProductCatalog/Features/Tenant/Specifications/ProductsForTenantSoftDeleteSpecification.cs b/src/Modules/ProductCatalog/Features/TenantCascadeDelete/ProductsForTenantSoftDeleteSpecification.cs similarity index 93% rename from src/Modules/ProductCatalog/Features/Tenant/Specifications/ProductsForTenantSoftDeleteSpecification.cs rename to src/Modules/ProductCatalog/Features/TenantCascadeDelete/ProductsForTenantSoftDeleteSpecification.cs index ef9070cc..66350956 100644 --- a/src/Modules/ProductCatalog/Features/Tenant/Specifications/ProductsForTenantSoftDeleteSpecification.cs +++ b/src/Modules/ProductCatalog/Features/TenantCascadeDelete/ProductsForTenantSoftDeleteSpecification.cs @@ -1,7 +1,7 @@ using Ardalis.Specification; using ProductEntity = ProductCatalog.Entities.Product; -namespace ProductCatalog.Features.Tenant.Specifications; +namespace ProductCatalog.Features.TenantCascadeDelete; /// /// Loads all non-deleted products (with their data links) for a specific tenant, bypassing @@ -19,4 +19,3 @@ public ProductsForTenantSoftDeleteSpecification(Guid tenantId) .IgnoreQueryFilters(); } } - diff --git a/src/Modules/ProductCatalog/Features/Tenant/Handlers/TenantCascadeDeleteHandler.cs b/src/Modules/ProductCatalog/Features/TenantCascadeDelete/TenantCascadeDeleteHandler.cs similarity index 95% rename from src/Modules/ProductCatalog/Features/Tenant/Handlers/TenantCascadeDeleteHandler.cs rename to src/Modules/ProductCatalog/Features/TenantCascadeDelete/TenantCascadeDeleteHandler.cs index c0f62d6b..2ac49daf 100644 --- a/src/Modules/ProductCatalog/Features/Tenant/Handlers/TenantCascadeDeleteHandler.cs +++ b/src/Modules/ProductCatalog/Features/TenantCascadeDelete/TenantCascadeDeleteHandler.cs @@ -1,10 +1,9 @@ -using ProductCatalog.Features.Tenant.Specifications; using ProductCatalog; using Wolverine; using CategoryEntity = ProductCatalog.Entities.Category; using ProductEntity = ProductCatalog.Entities.Product; -namespace ProductCatalog.Features.Tenant.Handlers; +namespace ProductCatalog.Features.TenantCascadeDelete; /// /// Handles by cascading the soft-delete to all @@ -65,4 +64,3 @@ await unitOfWork.ExecuteInTransactionAsync( return messages; } } - diff --git a/src/Modules/ProductCatalog/Features/Category/Commands/UpdateCategoriesCommand.cs b/src/Modules/ProductCatalog/Features/UpdateCategories/UpdateCategoriesCommand.cs similarity index 97% rename from src/Modules/ProductCatalog/Features/Category/Commands/UpdateCategoriesCommand.cs rename to src/Modules/ProductCatalog/Features/UpdateCategories/UpdateCategoriesCommand.cs index 71b20795..7d2655fb 100644 --- a/src/Modules/ProductCatalog/Features/Category/Commands/UpdateCategoriesCommand.cs +++ b/src/Modules/ProductCatalog/Features/UpdateCategories/UpdateCategoriesCommand.cs @@ -1,12 +1,11 @@ using ErrorOr; -using ProductCatalog.Features.Category.Specifications; using ProductCatalog; using SharedKernel.Application.Batch; using SharedKernel.Application.Batch.Rules; using SharedKernel.Contracts.Events; using Wolverine; -namespace ProductCatalog.Features.Category; +namespace ProductCatalog.Features.UpdateCategories; /// Updates multiple categories in a single batch operation. public sealed record UpdateCategoriesCommand(UpdateCategoriesRequest Request); @@ -98,4 +97,3 @@ await unitOfWork.ExecuteInTransactionAsync( return (new BatchResponse([], command.Request.Items.Count, 0), messages); } } - diff --git a/src/Modules/ProductCatalog/Features/UpdateCategories/UpdateCategoriesController.cs b/src/Modules/ProductCatalog/Features/UpdateCategories/UpdateCategoriesController.cs new file mode 100644 index 00000000..05fd0c72 --- /dev/null +++ b/src/Modules/ProductCatalog/Features/UpdateCategories/UpdateCategoriesController.cs @@ -0,0 +1,27 @@ +using Asp.Versioning; +using ErrorOr; +using Microsoft.AspNetCore.Mvc; +using SharedKernel.Contracts.Api; +using SharedKernel.Contracts.Security; +using Wolverine; + +namespace ProductCatalog.Features.UpdateCategories; + +[ApiVersion(1.0)] +public sealed class UpdateCategoriesController(IMessageBus bus) : ApiControllerBase +{ + /// Updates multiple categories in a single batch operation. + [HttpPut] + [RequirePermission(Permission.Categories.Update)] + public async Task> Update( + UpdateCategoriesRequest request, + CancellationToken ct + ) + { + ErrorOr result = await bus.InvokeAsync>( + new UpdateCategoriesCommand(request), + ct + ); + return result.ToBatchResult(this); + } +} diff --git a/src/Modules/ProductCatalog/Features/Category/DTOs/UpdateCategoriesRequest.cs b/src/Modules/ProductCatalog/Features/UpdateCategories/UpdateCategoriesRequest.cs similarity index 94% rename from src/Modules/ProductCatalog/Features/Category/DTOs/UpdateCategoriesRequest.cs rename to src/Modules/ProductCatalog/Features/UpdateCategories/UpdateCategoriesRequest.cs index 1b2e8f2f..69f708e8 100644 --- a/src/Modules/ProductCatalog/Features/Category/DTOs/UpdateCategoriesRequest.cs +++ b/src/Modules/ProductCatalog/Features/UpdateCategories/UpdateCategoriesRequest.cs @@ -1,7 +1,7 @@ using System.ComponentModel.DataAnnotations; using SharedKernel.Application.Validation; -namespace ProductCatalog.Features.Category.DTOs; +namespace ProductCatalog.Features.UpdateCategories; /// /// Carries a list of category items to be updated in a single batch operation; accepts between 1 and 100 items. @@ -22,6 +22,3 @@ public sealed record UpdateCategoryItem( string Name, string? Description ) : IHasId; - - - diff --git a/src/Modules/ProductCatalog/Features/Category/Validation/UpdateCategoryItemValidator.cs b/src/Modules/ProductCatalog/Features/UpdateCategories/UpdateCategoryItemValidator.cs similarity index 82% rename from src/Modules/ProductCatalog/Features/Category/Validation/UpdateCategoryItemValidator.cs rename to src/Modules/ProductCatalog/Features/UpdateCategories/UpdateCategoryItemValidator.cs index 8c7d7924..6a3fe4bc 100644 --- a/src/Modules/ProductCatalog/Features/Category/Validation/UpdateCategoryItemValidator.cs +++ b/src/Modules/ProductCatalog/Features/UpdateCategories/UpdateCategoryItemValidator.cs @@ -1,11 +1,8 @@ using SharedKernel.Application.Validation; -namespace ProductCatalog.Features.Category.Validation; +namespace ProductCatalog.Features.UpdateCategories; /// /// FluentValidation validator for that enforces data-annotation constraints. /// public sealed class UpdateCategoryItemValidator : DataAnnotationsValidator; - - - diff --git a/src/Modules/ProductCatalog/Features/Category/DTOs/UpdateCategoryRequest.cs b/src/Modules/ProductCatalog/Features/UpdateCategories/UpdateCategoryRequest.cs similarity index 78% rename from src/Modules/ProductCatalog/Features/Category/DTOs/UpdateCategoryRequest.cs rename to src/Modules/ProductCatalog/Features/UpdateCategories/UpdateCategoryRequest.cs index b065d4f8..896b4647 100644 --- a/src/Modules/ProductCatalog/Features/Category/DTOs/UpdateCategoryRequest.cs +++ b/src/Modules/ProductCatalog/Features/UpdateCategories/UpdateCategoryRequest.cs @@ -1,9 +1,6 @@ -namespace ProductCatalog.Features.Category.DTOs; +namespace ProductCatalog.Features.UpdateCategories; /// /// Payload for updating an existing category's name and optional description. /// public sealed record UpdateCategoryRequest(string Name, string? Description); - - - diff --git a/src/Modules/ProductCatalog/Features/Product/Validation/UpdateProductItemValidator.cs b/src/Modules/ProductCatalog/Features/UpdateProducts/UpdateProductItemValidator.cs similarity index 84% rename from src/Modules/ProductCatalog/Features/Product/Validation/UpdateProductItemValidator.cs rename to src/Modules/ProductCatalog/Features/UpdateProducts/UpdateProductItemValidator.cs index eb145830..0b82dd7a 100644 --- a/src/Modules/ProductCatalog/Features/Product/Validation/UpdateProductItemValidator.cs +++ b/src/Modules/ProductCatalog/Features/UpdateProducts/UpdateProductItemValidator.cs @@ -1,10 +1,7 @@ -namespace ProductCatalog.Features.Product.Validation; +namespace ProductCatalog.Features.UpdateProducts; /// /// FluentValidation validator for , reusing the shared /// product validation rules including the description-required-above-price-threshold rule. /// public sealed class UpdateProductItemValidator : ProductRequestValidatorBase; - - - diff --git a/src/Modules/ProductCatalog/Features/Product/DTOs/UpdateProductRequest.cs b/src/Modules/ProductCatalog/Features/UpdateProducts/UpdateProductRequest.cs similarity index 92% rename from src/Modules/ProductCatalog/Features/Product/DTOs/UpdateProductRequest.cs rename to src/Modules/ProductCatalog/Features/UpdateProducts/UpdateProductRequest.cs index 005d8958..b2985fd0 100644 --- a/src/Modules/ProductCatalog/Features/Product/DTOs/UpdateProductRequest.cs +++ b/src/Modules/ProductCatalog/Features/UpdateProducts/UpdateProductRequest.cs @@ -1,6 +1,6 @@ using System.ComponentModel.DataAnnotations; -namespace ProductCatalog.Features.Product.DTOs; +namespace ProductCatalog.Features.UpdateProducts; /// /// Carries the replacement data for an existing product, subject to the same validation constraints as . @@ -14,4 +14,3 @@ public sealed record UpdateProductRequest( Guid? CategoryId = null, IReadOnlyCollection? ProductDataIds = null ) : IProductRequest; - diff --git a/src/Modules/ProductCatalog/Features/Product/Validation/UpdateProductRequestValidator.cs b/src/Modules/ProductCatalog/Features/UpdateProducts/UpdateProductRequestValidator.cs similarity index 83% rename from src/Modules/ProductCatalog/Features/Product/Validation/UpdateProductRequestValidator.cs rename to src/Modules/ProductCatalog/Features/UpdateProducts/UpdateProductRequestValidator.cs index 594e4aaa..d8c9f619 100644 --- a/src/Modules/ProductCatalog/Features/Product/Validation/UpdateProductRequestValidator.cs +++ b/src/Modules/ProductCatalog/Features/UpdateProducts/UpdateProductRequestValidator.cs @@ -1,10 +1,7 @@ -namespace ProductCatalog.Features.Product.Validation; +namespace ProductCatalog.Features.UpdateProducts; /// /// FluentValidation validator for , inheriting all rules from . /// public sealed class UpdateProductRequestValidator : ProductRequestValidatorBase; - - - diff --git a/src/Modules/ProductCatalog/Features/Product/Commands/UpdateProductsCommand.cs b/src/Modules/ProductCatalog/Features/UpdateProducts/UpdateProductsCommand.cs similarity index 95% rename from src/Modules/ProductCatalog/Features/Product/Commands/UpdateProductsCommand.cs rename to src/Modules/ProductCatalog/Features/UpdateProducts/UpdateProductsCommand.cs index 38233c14..b1b50a79 100644 --- a/src/Modules/ProductCatalog/Features/Product/Commands/UpdateProductsCommand.cs +++ b/src/Modules/ProductCatalog/Features/UpdateProducts/UpdateProductsCommand.cs @@ -1,5 +1,4 @@ using ErrorOr; -using ProductCatalog.Features.Product.Specifications; using ProductCatalog; using ProductCatalog.Entities; using ProductCatalog.ValueObjects; @@ -8,9 +7,9 @@ using SharedKernel.Contracts.Events; using Wolverine; using ProductEntity = ProductCatalog.Entities.Product; -using ProductRepositoryContract = ProductCatalog.Features.Product.Repositories.IProductRepository; +using ProductRepositoryContract = ProductCatalog.Interfaces.IProductRepository; -namespace ProductCatalog.Features.Product; +namespace ProductCatalog.Features.UpdateProducts; /// Updates multiple products in a single batch operation. public sealed record UpdateProductsCommand(UpdateProductsRequest Request); @@ -130,4 +129,3 @@ await unitOfWork.ExecuteInTransactionAsync( return (new BatchResponse([], items.Count, 0), messages); } } - diff --git a/src/Modules/ProductCatalog/Features/UpdateProducts/UpdateProductsController.cs b/src/Modules/ProductCatalog/Features/UpdateProducts/UpdateProductsController.cs new file mode 100644 index 00000000..aefbe6d5 --- /dev/null +++ b/src/Modules/ProductCatalog/Features/UpdateProducts/UpdateProductsController.cs @@ -0,0 +1,27 @@ +using Asp.Versioning; +using ErrorOr; +using Microsoft.AspNetCore.Mvc; +using SharedKernel.Contracts.Api; +using SharedKernel.Contracts.Security; +using Wolverine; + +namespace ProductCatalog.Features.UpdateProducts; + +[ApiVersion(1.0)] +public sealed class UpdateProductsController(IMessageBus bus) : ApiControllerBase +{ + /// Updates multiple products in a single batch operation. + [HttpPut] + [RequirePermission(Permission.Products.Update)] + public async Task> Update( + UpdateProductsRequest request, + CancellationToken ct + ) + { + ErrorOr result = await bus.InvokeAsync>( + new UpdateProductsCommand(request), + ct + ); + return result.ToBatchResult(this); + } +} diff --git a/src/Modules/ProductCatalog/Features/Product/DTOs/UpdateProductsRequest.cs b/src/Modules/ProductCatalog/Features/UpdateProducts/UpdateProductsRequest.cs similarity index 95% rename from src/Modules/ProductCatalog/Features/Product/DTOs/UpdateProductsRequest.cs rename to src/Modules/ProductCatalog/Features/UpdateProducts/UpdateProductsRequest.cs index 994808bd..6c8a25fe 100644 --- a/src/Modules/ProductCatalog/Features/Product/DTOs/UpdateProductsRequest.cs +++ b/src/Modules/ProductCatalog/Features/UpdateProducts/UpdateProductsRequest.cs @@ -1,6 +1,6 @@ using System.ComponentModel.DataAnnotations; -namespace ProductCatalog.Features.Product.DTOs; +namespace ProductCatalog.Features.UpdateProducts; /// /// Carries a list of product items to be updated in a single batch operation; accepts between 1 and 100 items. @@ -24,4 +24,3 @@ public sealed record UpdateProductItem( Guid? CategoryId = null, IReadOnlyCollection? ProductDataIds = null ) : IProductRequest, IHasId; - diff --git a/src/Modules/ProductCatalog/Features/Product/Queries/ValidateProductExistsQueryHandler.cs b/src/Modules/ProductCatalog/Features/ValidateProductExists/ValidateProductExistsQueryHandler.cs similarity index 77% rename from src/Modules/ProductCatalog/Features/Product/Queries/ValidateProductExistsQueryHandler.cs rename to src/Modules/ProductCatalog/Features/ValidateProductExists/ValidateProductExistsQueryHandler.cs index 882aab47..75da8aa1 100644 --- a/src/Modules/ProductCatalog/Features/Product/Queries/ValidateProductExistsQueryHandler.cs +++ b/src/Modules/ProductCatalog/Features/ValidateProductExists/ValidateProductExistsQueryHandler.cs @@ -1,15 +1,14 @@ using ErrorOr; -using ProductCatalog.Features.Product.Repositories; using SharedKernel.Contracts.Queries.ProductCatalog; using ProductEntity = ProductCatalog.Entities.Product; -namespace ProductCatalog.Features.Product.Queries; +namespace ProductCatalog.Features.ValidateProductExists; public sealed class ValidateProductExistsQueryHandler { public static async Task> HandleAsync( ValidateProductExistsQuery query, - Repositories.IProductRepository repository, + IProductRepository repository, CancellationToken ct ) { @@ -21,4 +20,3 @@ CancellationToken ct return Result.Success; } } - diff --git a/src/Modules/ProductCatalog/GlobalUsings.cs b/src/Modules/ProductCatalog/GlobalUsings.cs new file mode 100644 index 00000000..6191a45c --- /dev/null +++ b/src/Modules/ProductCatalog/GlobalUsings.cs @@ -0,0 +1,42 @@ +// ── Domain ──────────────────────────────────────────────────────────────────── +global using SharedKernel.Domain.Common; +global using SharedKernel.Domain.Entities; +global using SharedKernel.Domain.Entities.Contracts; +global using SharedKernel.Domain.Interfaces; + +// ── Application ─────────────────────────────────────────────────────────────── +global using ProductCatalog.Common.Errors; +global using ProductCatalog.Common.Events; +global using ProductCatalog.Features.Shared; +global using ProductCatalog.Entities; +global using ProductCatalog.Entities.ProductData; +global using ProductCatalog.Interfaces; +global using SharedKernel.Application.Batch; +global using SharedKernel.Application.Batch.Rules; +global using SharedKernel.Application.Context; +global using SharedKernel.Application.Contracts; +global using SharedKernel.Application.DTOs; +global using SharedKernel.Application.Events; +global using SharedKernel.Application.Extensions; +global using SharedKernel.Application.Resilience; +global using SharedKernel.Application.Search; +global using SharedKernel.Application.Sorting; +global using SharedKernel.Application.Validation; +global using SharedKernel.Contracts.Events; + +// ── API / Presentation ──────────────────────────────────────────────────────── +global using ProductCatalog.GraphQL.Models; +global using Reviews.Domain; +global using Reviews.Features; +global using SharedKernel.Contracts.Api; +global using SharedKernel.Contracts.Security; +global using Error = ErrorOr.Error; + +// ── Infrastructure ──────────────────────────────────────────────────────────── +global using SharedKernel.Application.Options.Infrastructure; +global using SharedKernel.Infrastructure.Configurations; +global using SharedKernel.Infrastructure.Persistence; +global using SharedKernel.Infrastructure.Repositories; +global using SharedKernel.Infrastructure.SoftDelete; +global using SharedKernel.Infrastructure.StoredProcedures; +global using SharedKernel.Infrastructure.UnitOfWork; diff --git a/src/Modules/ProductCatalog/GraphQL/Mutations/ProductMutations.cs b/src/Modules/ProductCatalog/GraphQL/Mutations/ProductMutations.cs index 18a36f80..1d13f0d3 100644 --- a/src/Modules/ProductCatalog/GraphQL/Mutations/ProductMutations.cs +++ b/src/Modules/ProductCatalog/GraphQL/Mutations/ProductMutations.cs @@ -1,5 +1,7 @@ using ErrorOr; using HotChocolate.Authorization; +using ProductCatalog.Features.CreateProducts; +using ProductCatalog.Features.DeleteProducts; using SharedKernel.Contracts.Security; using Wolverine; diff --git a/src/Modules/ProductCatalog/GraphQL/Queries/CategoryQueries.cs b/src/Modules/ProductCatalog/GraphQL/Queries/CategoryQueries.cs index 0b363ed9..979dfe19 100644 --- a/src/Modules/ProductCatalog/GraphQL/Queries/CategoryQueries.cs +++ b/src/Modules/ProductCatalog/GraphQL/Queries/CategoryQueries.cs @@ -1,5 +1,7 @@ using ErrorOr; using HotChocolate.Authorization; +using ProductCatalog.Features.GetCategories; +using ProductCatalog.Features.GetCategoryById; using ProductCatalog.GraphQL.Models; using Wolverine; diff --git a/src/Modules/ProductCatalog/GraphQL/Queries/ProductQueries.cs b/src/Modules/ProductCatalog/GraphQL/Queries/ProductQueries.cs index e2e5f67b..968b6195 100644 --- a/src/Modules/ProductCatalog/GraphQL/Queries/ProductQueries.cs +++ b/src/Modules/ProductCatalog/GraphQL/Queries/ProductQueries.cs @@ -1,5 +1,7 @@ using ErrorOr; using HotChocolate.Authorization; +using ProductCatalog.Features.GetProductById; +using ProductCatalog.Features.GetProducts; using ProductCatalog.GraphQL.Models; using Wolverine; diff --git a/src/Modules/ProductCatalog/Features/Product/Repositories/IProductRepository.cs b/src/Modules/ProductCatalog/Interfaces/IProductRepository.cs similarity index 94% rename from src/Modules/ProductCatalog/Features/Product/Repositories/IProductRepository.cs rename to src/Modules/ProductCatalog/Interfaces/IProductRepository.cs index f58f24ff..8105ad48 100644 --- a/src/Modules/ProductCatalog/Features/Product/Repositories/IProductRepository.cs +++ b/src/Modules/ProductCatalog/Interfaces/IProductRepository.cs @@ -1,7 +1,8 @@ using ErrorOr; +using ProductCatalog.Features.GetProducts; using ProductEntity = ProductCatalog.Entities.Product; -namespace ProductCatalog.Features.Product.Repositories; +namespace ProductCatalog.Interfaces; /// /// Domain-facing repository contract for products, extending the generic repository with product-specific filtered queries and facet aggregations. @@ -29,4 +30,3 @@ Task> GetPriceFacetsAsync( /// Sets CategoryId to null on all products whose CategoryId is in . Task ClearCategoryAsync(IReadOnlyCollection categoryIds, CancellationToken ct = default); } - diff --git a/src/Modules/ProductCatalog/ProductCatalog.Api.GlobalUsings.cs b/src/Modules/ProductCatalog/ProductCatalog.Api.GlobalUsings.cs deleted file mode 100644 index 31b3c083..00000000 --- a/src/Modules/ProductCatalog/ProductCatalog.Api.GlobalUsings.cs +++ /dev/null @@ -1,17 +0,0 @@ -global using ProductCatalog.Events; -global using ProductCatalog.Features.Category; -global using ProductCatalog.Features.Category.DTOs; -global using ProductCatalog.Features.Product; -global using ProductCatalog.Features.Product.DTOs; -global using ProductCatalog.Features.ProductData; -global using ProductCatalog.Features.ProductData.DTOs; -global using ProductCatalog.GraphQL.Models; -global using Reviews.Domain; -global using Reviews.Features; -global using SharedKernel.Application.DTOs; -global using SharedKernel.Application.Events; -global using SharedKernel.Contracts.Api; -global using SharedKernel.Contracts.Events; -global using SharedKernel.Contracts.Security; -global using SharedKernel.Domain.Common; -global using Error = ErrorOr.Error; diff --git a/src/Modules/ProductCatalog/ProductCatalog.Application.GlobalUsings.cs b/src/Modules/ProductCatalog/ProductCatalog.Application.GlobalUsings.cs deleted file mode 100644 index d0d3e187..00000000 --- a/src/Modules/ProductCatalog/ProductCatalog.Application.GlobalUsings.cs +++ /dev/null @@ -1,25 +0,0 @@ -global using ProductCatalog.Errors; -global using ProductCatalog.Events; -global using ProductCatalog.Features.Category.DTOs; -global using ProductCatalog.Features.Product.DTOs; -global using ProductCatalog.Features.Product.Repositories; -global using ProductCatalog.Features.ProductData.DTOs; -global using ProductCatalog.Entities; -global using ProductCatalog.Entities.ProductData; -global using ProductCatalog.Interfaces; -global using SharedKernel.Application.Batch; -global using SharedKernel.Application.Batch.Rules; -global using SharedKernel.Application.Context; -global using SharedKernel.Application.Contracts; -global using SharedKernel.Application.DTOs; -global using SharedKernel.Application.Events; -global using SharedKernel.Application.Extensions; -global using SharedKernel.Application.Resilience; -global using SharedKernel.Application.Search; -global using SharedKernel.Application.Sorting; -global using SharedKernel.Application.Validation; -global using SharedKernel.Contracts.Events; -global using SharedKernel.Domain.Common; -global using SharedKernel.Domain.Entities.Contracts; -global using SharedKernel.Domain.Interfaces; - diff --git a/src/Modules/ProductCatalog/ProductCatalog.Domain.GlobalUsings.cs b/src/Modules/ProductCatalog/ProductCatalog.Domain.GlobalUsings.cs deleted file mode 100644 index 58741b9a..00000000 --- a/src/Modules/ProductCatalog/ProductCatalog.Domain.GlobalUsings.cs +++ /dev/null @@ -1,5 +0,0 @@ -global using SharedKernel.Domain.Common; -global using SharedKernel.Domain.Entities; -global using SharedKernel.Domain.Entities.Contracts; -global using SharedKernel.Domain.Interfaces; - diff --git a/src/Modules/ProductCatalog/ProductCatalog.Infrastructure.GlobalUsings.cs b/src/Modules/ProductCatalog/ProductCatalog.Infrastructure.GlobalUsings.cs deleted file mode 100644 index 5360232a..00000000 --- a/src/Modules/ProductCatalog/ProductCatalog.Infrastructure.GlobalUsings.cs +++ /dev/null @@ -1,21 +0,0 @@ -global using ProductCatalog.Features.Category.DTOs; -global using ProductCatalog.Features.Product.DTOs; -global using ProductCatalog.Features.Product.Repositories; -global using ProductCatalog.Features.Product.Specifications; -global using ProductCatalog.Entities; -global using ProductCatalog.Entities.ProductData; -global using ProductCatalog.Interfaces; -global using SharedKernel.Application.Context; -global using SharedKernel.Application.Events; -global using SharedKernel.Application.Options.Infrastructure; -global using SharedKernel.Contracts.Events; -global using SharedKernel.Domain.Common; -global using SharedKernel.Domain.Entities.Contracts; -global using SharedKernel.Domain.Interfaces; -global using SharedKernel.Infrastructure.Configurations; -global using SharedKernel.Infrastructure.Persistence; -global using SharedKernel.Infrastructure.Repositories; -global using SharedKernel.Infrastructure.SoftDelete; -global using SharedKernel.Infrastructure.StoredProcedures; -global using SharedKernel.Infrastructure.UnitOfWork; - diff --git a/src/Modules/ProductCatalog/ProductCatalogModule.cs b/src/Modules/ProductCatalog/ProductCatalogModule.cs index 7fdc2bf7..74f19983 100644 --- a/src/Modules/ProductCatalog/ProductCatalogModule.cs +++ b/src/Modules/ProductCatalog/ProductCatalogModule.cs @@ -8,13 +8,10 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Resilience; using Polly; -using ProductCatalog.Controllers.V1; using ProductCatalog.GraphQL.DataLoaders; using ProductCatalog.GraphQL.Mutations; using ProductCatalog.GraphQL.Queries; using ProductCatalog.GraphQL.Types; -using ProductCatalog.Features.Product.Repositories; -using ProductCatalog.Features.Product.Validation; using ProductCatalog.Interfaces; using ProductCatalog.Persistence; using ProductCatalog.Repositories; @@ -26,7 +23,7 @@ using SharedKernel.Infrastructure.Configuration; using SharedKernel.Infrastructure.Health; using SharedKernel.Infrastructure.Registration; -using ProductApplicationRepository = ProductCatalog.Features.Product.Repositories.IProductRepository; +using ProductApplicationRepository = ProductCatalog.Interfaces.IProductRepository; namespace ProductCatalog; @@ -53,7 +50,7 @@ IConfiguration configuration .AddRepository() .AddCascadeRule(); - services.AddValidatorsFromAssemblyContaining( + services.AddValidatorsFromAssemblyContaining( filter: registration => !registration.ValidatorType.IsGenericTypeDefinition ); services.AddScoped(typeof(IBatchRule<>), typeof(FluentValidationBatchRule<>)); @@ -94,7 +91,7 @@ IConfiguration configuration } ); - services.AddControllers().AddApplicationPart(typeof(ProductsController).Assembly); + services.AddControllers().AddApplicationPart(typeof(ProductCatalogModule).Assembly); services .AddGraphQLServer() diff --git a/src/Modules/ProductCatalog/Repositories/ProductRepository.cs b/src/Modules/ProductCatalog/Repositories/ProductRepository.cs index b0a6fc44..5c059e12 100644 --- a/src/Modules/ProductCatalog/Repositories/ProductRepository.cs +++ b/src/Modules/ProductCatalog/Repositories/ProductRepository.cs @@ -1,7 +1,8 @@ using ErrorOr; using Microsoft.EntityFrameworkCore; +using ProductCatalog.Features.GetProducts; using ProductCatalog.Persistence; -using ProductApplicationRepository = ProductCatalog.Features.Product.Repositories.IProductRepository; +using ProductApplicationRepository = ProductCatalog.Interfaces.IProductRepository; namespace ProductCatalog.Repositories; diff --git a/src/Modules/ProductCatalog/ValueObjects/Price.cs b/src/Modules/ProductCatalog/ValueObjects/Price.cs index af53dab6..5b3b81de 100644 --- a/src/Modules/ProductCatalog/ValueObjects/Price.cs +++ b/src/Modules/ProductCatalog/ValueObjects/Price.cs @@ -1,5 +1,5 @@ using ErrorOr; -using ProductCatalog.Errors; +using ProductCatalog.Common.Errors; namespace ProductCatalog.ValueObjects; diff --git a/tests/APITemplate.Tests/Integration/Features/IdempotentControllerTests.cs b/tests/APITemplate.Tests/Integration/Features/IdempotentControllerTests.cs index 9baa30e0..c655e1d2 100644 --- a/tests/APITemplate.Tests/Integration/Features/IdempotentControllerTests.cs +++ b/tests/APITemplate.Tests/Integration/Features/IdempotentControllerTests.cs @@ -2,7 +2,7 @@ using System.Net.Http.Json; using System.Text.Json; using APITemplate.Tests.Integration.Helpers; -using ProductCatalog.Features.Product.DTOs; +using ProductCatalog.Features.IdempotentCreate; using Shouldly; using Xunit; diff --git a/tests/APITemplate.Tests/Unit/ProductCatalog/PatchProductCommandHandlerTests.cs b/tests/APITemplate.Tests/Unit/ProductCatalog/PatchProductCommandHandlerTests.cs index 200b358d..451660f8 100644 --- a/tests/APITemplate.Tests/Unit/ProductCatalog/PatchProductCommandHandlerTests.cs +++ b/tests/APITemplate.Tests/Unit/ProductCatalog/PatchProductCommandHandlerTests.cs @@ -5,10 +5,10 @@ using Moq; using ProductCatalog; using ProductCatalog.Entities; -using ProductCatalog.Events; -using ProductCatalog.Features.Product.Commands; -using ProductCatalog.Features.Product.DTOs; -using ProductCatalog.Features.Product.Repositories; +using ProductCatalog.Common.Events; +using ProductCatalog.Features.PatchProduct; +using ProductCatalog.Features.Shared; +using ProductCatalog.Interfaces; using ProductCatalog.ValueObjects; using SharedKernel.Contracts.Events; using SharedKernel.Domain.Interfaces; diff --git a/tests/APITemplate.Tests/Unit/ProductCatalog/ProductCatalogModuleTests.cs b/tests/APITemplate.Tests/Unit/ProductCatalog/ProductCatalogModuleTests.cs index ebc43dd3..00fa8905 100644 --- a/tests/APITemplate.Tests/Unit/ProductCatalog/ProductCatalogModuleTests.cs +++ b/tests/APITemplate.Tests/Unit/ProductCatalog/ProductCatalogModuleTests.cs @@ -1,8 +1,10 @@ using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using ProductCatalog; -using ProductCatalog.Features.Category.DTOs; -using ProductCatalog.Features.Product.DTOs; +using ProductCatalog.Features.CreateCategories; +using ProductCatalog.Features.CreateProducts; +using ProductCatalog.Features.UpdateCategories; +using ProductCatalog.Features.UpdateProducts; using SharedKernel.Application.Batch; using SharedKernel.Application.Batch.Rules; using Shouldly; diff --git a/tests/APITemplate.Tests/Unit/Validators/CreateProductRequestValidatorTests.cs b/tests/APITemplate.Tests/Unit/Validators/CreateProductRequestValidatorTests.cs index 24c0536d..b0e7af89 100644 --- a/tests/APITemplate.Tests/Unit/Validators/CreateProductRequestValidatorTests.cs +++ b/tests/APITemplate.Tests/Unit/Validators/CreateProductRequestValidatorTests.cs @@ -1,5 +1,4 @@ -using ProductCatalog.Features.Product.DTOs; -using ProductCatalog.Features.Product.Validation; +using ProductCatalog.Features.CreateProducts; using Shouldly; using Xunit; diff --git a/tests/APITemplate.Tests/Unit/Validators/UpdateProductRequestValidatorTests.cs b/tests/APITemplate.Tests/Unit/Validators/UpdateProductRequestValidatorTests.cs index d4eeeead..174b3543 100644 --- a/tests/APITemplate.Tests/Unit/Validators/UpdateProductRequestValidatorTests.cs +++ b/tests/APITemplate.Tests/Unit/Validators/UpdateProductRequestValidatorTests.cs @@ -1,5 +1,4 @@ -using ProductCatalog.Features.Product.DTOs; -using ProductCatalog.Features.Product.Validation; +using ProductCatalog.Features.UpdateProducts; using Shouldly; using Xunit;