completed invetory updates
This commit is contained in:
@@ -0,0 +1,67 @@
|
||||
using ERPCore.Domain.Enums;
|
||||
using ERPCore.Dtos.Brands;
|
||||
using ERPCore.Dtos.Common;
|
||||
using ERPCore.Services.Interfaces;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace ERPCore.Controllers;
|
||||
|
||||
/// <summary>Brand master endpoints (docs/11-BACKEND-PHASE1.md §2.6).</summary>
|
||||
[Route("api/v1/brands")]
|
||||
public sealed class BrandsController : ApiControllerBase
|
||||
{
|
||||
private readonly IBrandService _brands;
|
||||
|
||||
public BrandsController(IBrandService brands) => _brands = brands;
|
||||
|
||||
[HttpGet]
|
||||
[ProducesResponseType(typeof(PagedResponse<BrandDto>), StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<PagedResponse<BrandDto>>> List(
|
||||
[FromQuery] PageQuery query, [FromQuery] EntityStatus? status, CancellationToken ct)
|
||||
=> Ok(await _brands.ListAsync(query, status, ct));
|
||||
|
||||
[HttpGet("{brandId:int}")]
|
||||
[ProducesResponseType(typeof(BrandDto), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public async Task<ActionResult<BrandDto>> GetById(int brandId, CancellationToken ct)
|
||||
{
|
||||
var result = await _brands.GetAsync(brandId, ct);
|
||||
if (result is null) return NotFound();
|
||||
|
||||
SetETag(result.RowVersion);
|
||||
return Ok(result.Value);
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[ProducesResponseType(typeof(BrandDto), StatusCodes.Status201Created)]
|
||||
[ProducesResponseType(StatusCodes.Status409Conflict)]
|
||||
public async Task<ActionResult<BrandDto>> Create([FromBody] CreateBrandRequest request, CancellationToken ct)
|
||||
{
|
||||
var result = await _brands.CreateAsync(request, ct);
|
||||
SetETag(result.RowVersion);
|
||||
return Created($"/api/v1/brands/{result.Value.BrandId}", result.Value);
|
||||
}
|
||||
|
||||
[HttpPut("{brandId:int}")]
|
||||
[ProducesResponseType(typeof(BrandDto), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
[ProducesResponseType(StatusCodes.Status409Conflict)]
|
||||
[ProducesResponseType(StatusCodes.Status412PreconditionFailed)]
|
||||
public async Task<ActionResult<BrandDto>> Update(int brandId, [FromBody] UpdateBrandRequest request, CancellationToken ct)
|
||||
{
|
||||
var expected = RequireIfMatch();
|
||||
var result = await _brands.UpdateAsync(brandId, request, expected, ct);
|
||||
SetETag(result.RowVersion);
|
||||
return Ok(result.Value);
|
||||
}
|
||||
|
||||
/// <summary>Deactivate/reactivate. Masters are never hard-deleted (FR-MD-08).</summary>
|
||||
[HttpPatch("{brandId:int}/status")]
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public async Task<IActionResult> SetStatus(int brandId, [FromBody] UpdateBrandStatusRequest request, CancellationToken ct)
|
||||
{
|
||||
await _brands.SetStatusAsync(brandId, request.Status, ct);
|
||||
return NoContent();
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
using ERPCore.Domain.Enums;
|
||||
using ERPCore.Dtos.Categories;
|
||||
using ERPCore.Dtos.Common;
|
||||
using ERPCore.Services.Interfaces;
|
||||
@@ -5,7 +6,11 @@ using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace ERPCore.Controllers;
|
||||
|
||||
/// <summary>Category endpoints (docs/11-BACKEND-PHASE1.md §2.3).</summary>
|
||||
/// <summary>
|
||||
/// Category endpoints (docs/11-BACKEND-PHASE1.md §2.3), including the subcategories
|
||||
/// nested beneath each category. The hierarchy is exactly two levels deep — the old
|
||||
/// <c>?tree=true</c> parameter is gone along with the self-nesting model.
|
||||
/// </summary>
|
||||
[Route("api/v1/categories")]
|
||||
public sealed class CategoriesController : ApiControllerBase
|
||||
{
|
||||
@@ -13,19 +18,79 @@ public sealed class CategoriesController : ApiControllerBase
|
||||
|
||||
public CategoriesController(ICategoryService categories) => _categories = categories;
|
||||
|
||||
/// <summary>Flat paged list, or a nested tree when <c>tree=true</c>.</summary>
|
||||
[HttpGet]
|
||||
[ProducesResponseType(typeof(PagedResponse<CategoryDto>), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(IReadOnlyList<CategoryTreeDto>), StatusCodes.Status200OK)]
|
||||
public async Task<IActionResult> List([FromQuery] PageQuery query, [FromQuery] bool tree, CancellationToken ct)
|
||||
=> tree ? Ok(await _categories.GetTreeAsync(ct)) : Ok(await _categories.ListAsync(query, ct));
|
||||
public async Task<ActionResult<PagedResponse<CategoryDto>>> List(
|
||||
[FromQuery] PageQuery query, [FromQuery] EntityStatus? status, CancellationToken ct)
|
||||
=> Ok(await _categories.ListAsync(query, status, ct));
|
||||
|
||||
[HttpGet("{categoryId:int}")]
|
||||
[ProducesResponseType(typeof(CategoryDto), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public async Task<ActionResult<CategoryDto>> GetById(int categoryId, CancellationToken ct)
|
||||
{
|
||||
var result = await _categories.GetAsync(categoryId, ct);
|
||||
if (result is null) return NotFound();
|
||||
|
||||
SetETag(result.RowVersion);
|
||||
return Ok(result.Value);
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[ProducesResponseType(typeof(CategoryDto), StatusCodes.Status201Created)]
|
||||
[ProducesResponseType(StatusCodes.Status422UnprocessableEntity)]
|
||||
[ProducesResponseType(StatusCodes.Status409Conflict)]
|
||||
public async Task<ActionResult<CategoryDto>> Create([FromBody] CreateCategoryRequest request, CancellationToken ct)
|
||||
{
|
||||
var dto = await _categories.CreateAsync(request, ct);
|
||||
return Created($"/api/v1/categories/{dto.CategoryId}", dto);
|
||||
var result = await _categories.CreateAsync(request, ct);
|
||||
SetETag(result.RowVersion);
|
||||
return Created($"/api/v1/categories/{result.Value.CategoryId}", result.Value);
|
||||
}
|
||||
|
||||
[HttpPut("{categoryId:int}")]
|
||||
[ProducesResponseType(typeof(CategoryDto), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
[ProducesResponseType(StatusCodes.Status409Conflict)]
|
||||
[ProducesResponseType(StatusCodes.Status412PreconditionFailed)]
|
||||
public async Task<ActionResult<CategoryDto>> Update(
|
||||
int categoryId, [FromBody] UpdateCategoryRequest request, CancellationToken ct)
|
||||
{
|
||||
var expected = RequireIfMatch();
|
||||
var result = await _categories.UpdateAsync(categoryId, request, expected, ct);
|
||||
SetETag(result.RowVersion);
|
||||
return Ok(result.Value);
|
||||
}
|
||||
|
||||
/// <summary>Deactivate/reactivate. Masters are never hard-deleted (FR-MD-08).</summary>
|
||||
[HttpPatch("{categoryId:int}/status")]
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public async Task<IActionResult> SetStatus(
|
||||
int categoryId, [FromBody] UpdateCategoryStatusRequest request, CancellationToken ct)
|
||||
{
|
||||
await _categories.SetStatusAsync(categoryId, request.Status, ct);
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
// Subcategories — nested under their parent category (docs/11 §2.3).
|
||||
// Updates live on SubCategoriesController at /api/v1/subcategories/{id}.
|
||||
|
||||
[HttpGet("{categoryId:int}/subcategories")]
|
||||
[ProducesResponseType(typeof(PagedResponse<SubCategoryDto>), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public async Task<ActionResult<PagedResponse<SubCategoryDto>>> ListSubCategories(
|
||||
int categoryId, [FromQuery] PageQuery query, [FromQuery] EntityStatus? status, CancellationToken ct)
|
||||
=> Ok(await _categories.ListSubCategoriesAsync(categoryId, query, status, ct));
|
||||
|
||||
[HttpPost("{categoryId:int}/subcategories")]
|
||||
[ProducesResponseType(typeof(SubCategoryDto), StatusCodes.Status201Created)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
[ProducesResponseType(StatusCodes.Status409Conflict)]
|
||||
[ProducesResponseType(StatusCodes.Status422UnprocessableEntity)]
|
||||
public async Task<ActionResult<SubCategoryDto>> CreateSubCategory(
|
||||
int categoryId, [FromBody] CreateSubCategoryRequest request, CancellationToken ct)
|
||||
{
|
||||
var result = await _categories.CreateSubCategoryAsync(categoryId, request, ct);
|
||||
SetETag(result.RowVersion);
|
||||
return Created($"/api/v1/subcategories/{result.Value.SubCategoryId}", result.Value);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
using ERPCore.Domain.Enums;
|
||||
using ERPCore.Dtos.Common;
|
||||
using ERPCore.Dtos.ItemTypes;
|
||||
using ERPCore.Services.Interfaces;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace ERPCore.Controllers;
|
||||
|
||||
/// <summary>
|
||||
/// Item type master endpoints (docs/11-BACKEND-PHASE1.md §2.7) — the Color/Size/Material
|
||||
/// dimension names. <c>GET</c> is the reason this master exists: it populates the item
|
||||
/// builder's dropdown. Items never reference an item type; the chosen values are encoded
|
||||
/// into the client-generated SKU (docs/10 Part C.9).
|
||||
/// </summary>
|
||||
[Route("api/v1/item-types")]
|
||||
public sealed class ItemTypesController : ApiControllerBase
|
||||
{
|
||||
private readonly IItemTypeService _itemTypes;
|
||||
|
||||
public ItemTypesController(IItemTypeService itemTypes) => _itemTypes = itemTypes;
|
||||
|
||||
/// <summary>Feeds the frontend item-builder dropdown; filter <c>status=Active</c> for selectable rows.</summary>
|
||||
[HttpGet]
|
||||
[ProducesResponseType(typeof(PagedResponse<ItemTypeDto>), StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<PagedResponse<ItemTypeDto>>> List(
|
||||
[FromQuery] PageQuery query, [FromQuery] EntityStatus? status, CancellationToken ct)
|
||||
=> Ok(await _itemTypes.ListAsync(query, status, ct));
|
||||
|
||||
[HttpGet("{itemTypeId:int}")]
|
||||
[ProducesResponseType(typeof(ItemTypeDto), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public async Task<ActionResult<ItemTypeDto>> GetById(int itemTypeId, CancellationToken ct)
|
||||
{
|
||||
var result = await _itemTypes.GetAsync(itemTypeId, ct);
|
||||
if (result is null) return NotFound();
|
||||
|
||||
SetETag(result.RowVersion);
|
||||
return Ok(result.Value);
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[ProducesResponseType(typeof(ItemTypeDto), StatusCodes.Status201Created)]
|
||||
[ProducesResponseType(StatusCodes.Status409Conflict)]
|
||||
public async Task<ActionResult<ItemTypeDto>> Create([FromBody] CreateItemTypeRequest request, CancellationToken ct)
|
||||
{
|
||||
var result = await _itemTypes.CreateAsync(request, ct);
|
||||
SetETag(result.RowVersion);
|
||||
return Created($"/api/v1/item-types/{result.Value.ItemTypeId}", result.Value);
|
||||
}
|
||||
|
||||
[HttpPut("{itemTypeId:int}")]
|
||||
[ProducesResponseType(typeof(ItemTypeDto), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
[ProducesResponseType(StatusCodes.Status409Conflict)]
|
||||
[ProducesResponseType(StatusCodes.Status412PreconditionFailed)]
|
||||
public async Task<ActionResult<ItemTypeDto>> Update(int itemTypeId, [FromBody] UpdateItemTypeRequest request, CancellationToken ct)
|
||||
{
|
||||
var expected = RequireIfMatch();
|
||||
var result = await _itemTypes.UpdateAsync(itemTypeId, request, expected, ct);
|
||||
SetETag(result.RowVersion);
|
||||
return Ok(result.Value);
|
||||
}
|
||||
|
||||
/// <summary>Deactivate/reactivate. Masters are never hard-deleted (FR-MD-08).</summary>
|
||||
[HttpPatch("{itemTypeId:int}/status")]
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public async Task<IActionResult> SetStatus(int itemTypeId, [FromBody] UpdateItemTypeStatusRequest request, CancellationToken ct)
|
||||
{
|
||||
await _itemTypes.SetStatusAsync(itemTypeId, request.Status, ct);
|
||||
return NoContent();
|
||||
}
|
||||
}
|
||||
@@ -21,9 +21,11 @@ public sealed class ItemsController : ApiControllerBase
|
||||
[FromQuery] PageQuery query,
|
||||
[FromQuery] EntityStatus? status,
|
||||
[FromQuery] int? categoryId,
|
||||
[FromQuery] int? subCategoryId,
|
||||
[FromQuery] int? brandId,
|
||||
[FromQuery] TrackingMode? trackingMode,
|
||||
CancellationToken ct)
|
||||
=> Ok(await _items.ListAsync(query, status, categoryId, trackingMode, ct));
|
||||
=> Ok(await _items.ListAsync(query, status, categoryId, subCategoryId, brandId, trackingMode, ct));
|
||||
|
||||
/// <summary>Get a single item; returns an <c>ETag</c> for optimistic concurrency.</summary>
|
||||
[HttpGet("{itemId:int}")]
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
using ERPCore.Dtos.Config;
|
||||
using ERPCore.Services.Interfaces;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace ERPCore.Controllers;
|
||||
|
||||
/// <summary>
|
||||
/// Product configuration endpoints (docs/11-BACKEND-PHASE1.md §2.8) — the singleton
|
||||
/// feature gate for subcategories/brands/item-types.
|
||||
/// <para>
|
||||
/// <b>Authorization:</b> writes are admitted by the inherited ERP door policy only.
|
||||
/// A dedicated <c>CONFIG_MANAGE</c> permission is reserved for when per-endpoint RBAC
|
||||
/// lands (FR-X-01, currently deferred) — at that point this action gets the attribute
|
||||
/// with no other change. Until then any ERP-admitted user can flip these flags; that is
|
||||
/// the accepted Phase-1 posture, consistent with every other endpoint.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
[Route("api/v1/product-config")]
|
||||
public sealed class ProductConfigController : ApiControllerBase
|
||||
{
|
||||
private readonly IProductConfigService _config;
|
||||
|
||||
public ProductConfigController(IProductConfigService config) => _config = config;
|
||||
|
||||
[HttpGet]
|
||||
[ProducesResponseType(typeof(ProductConfigDto), StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<ProductConfigDto>> Get(CancellationToken ct)
|
||||
{
|
||||
var result = await _config.GetAsync(ct);
|
||||
SetETag(result.RowVersion);
|
||||
return Ok(result.Value);
|
||||
}
|
||||
|
||||
[HttpPut]
|
||||
[ProducesResponseType(typeof(ProductConfigDto), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(StatusCodes.Status412PreconditionFailed)]
|
||||
public async Task<ActionResult<ProductConfigDto>> Update(
|
||||
[FromBody] UpdateProductConfigRequest request, CancellationToken ct)
|
||||
{
|
||||
var expected = RequireIfMatch();
|
||||
var result = await _config.UpdateAsync(request, expected, ct);
|
||||
SetETag(result.RowVersion);
|
||||
return Ok(result.Value);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
using ERPCore.Dtos.Categories;
|
||||
using ERPCore.Services.Interfaces;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace ERPCore.Controllers;
|
||||
|
||||
/// <summary>
|
||||
/// Subcategory endpoints addressed by their own id (docs/11-BACKEND-PHASE1.md §2.3).
|
||||
/// Listing and creation live under the parent category on <see cref="CategoriesController"/>,
|
||||
/// since a subcategory only exists in the context of one.
|
||||
/// </summary>
|
||||
[Route("api/v1/subcategories")]
|
||||
public sealed class SubCategoriesController : ApiControllerBase
|
||||
{
|
||||
private readonly ICategoryService _categories;
|
||||
|
||||
public SubCategoriesController(ICategoryService categories) => _categories = categories;
|
||||
|
||||
[HttpGet("{subCategoryId:int}")]
|
||||
[ProducesResponseType(typeof(SubCategoryDto), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public async Task<ActionResult<SubCategoryDto>> GetById(int subCategoryId, CancellationToken ct)
|
||||
{
|
||||
var result = await _categories.GetSubCategoryAsync(subCategoryId, ct);
|
||||
if (result is null) return NotFound();
|
||||
|
||||
SetETag(result.RowVersion);
|
||||
return Ok(result.Value);
|
||||
}
|
||||
|
||||
/// <summary>Renames a subcategory. It cannot be moved to another category — see the request DTO.</summary>
|
||||
[HttpPut("{subCategoryId:int}")]
|
||||
[ProducesResponseType(typeof(SubCategoryDto), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
[ProducesResponseType(StatusCodes.Status409Conflict)]
|
||||
[ProducesResponseType(StatusCodes.Status412PreconditionFailed)]
|
||||
public async Task<ActionResult<SubCategoryDto>> Update(
|
||||
int subCategoryId, [FromBody] UpdateSubCategoryRequest request, CancellationToken ct)
|
||||
{
|
||||
var expected = RequireIfMatch();
|
||||
var result = await _categories.UpdateSubCategoryAsync(subCategoryId, request, expected, ct);
|
||||
SetETag(result.RowVersion);
|
||||
return Ok(result.Value);
|
||||
}
|
||||
|
||||
/// <summary>Deactivate/reactivate. Masters are never hard-deleted (FR-MD-08).</summary>
|
||||
[HttpPatch("{subCategoryId:int}/status")]
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public async Task<IActionResult> SetStatus(
|
||||
int subCategoryId, [FromBody] UpdateSubCategoryStatusRequest request, CancellationToken ct)
|
||||
{
|
||||
await _categories.SetSubCategoryStatusAsync(subCategoryId, request.Status, ct);
|
||||
return NoContent();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user