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;