feat: Implement Category, Item, UOM, Vendor, and Warehouse services with CRUD operations
- Added CategoryService for managing categories with listing, tree structure, and creation functionalities. - Introduced ItemService for item management, including listing, detail retrieval, creation, updating, and status management. - Created UomService for handling unit of measure operations, including listing and creation. - Developed VendorService for vendor management, supporting listing, detail retrieval, creation, updating, and status management. - Implemented WarehouseService for warehouse and bin management, including listing warehouses, creating warehouses, and managing bins within warehouses. - Added interfaces for each service to define the contract for service implementations. - Generated Entity Framework Core model snapshot for database migrations.
This commit is contained in:
@@ -0,0 +1,37 @@
|
|||||||
|
namespace ERPCore.Common.Http;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Encodes the PostgreSQL xmin concurrency token (a <see cref="uint"/>) as an
|
||||||
|
/// opaque, quoted HTTP ETag and parses <c>If-Match</c> values back. Round-trips
|
||||||
|
/// via base64 so the value is stable and content-type agnostic
|
||||||
|
/// (docs/11-BACKEND-PHASE1.md §1.6).
|
||||||
|
/// </summary>
|
||||||
|
public static class ETag
|
||||||
|
{
|
||||||
|
/// <summary>Quoted ETag string for a row-version token, e.g. <c>"0RsAAA=="</c>.</summary>
|
||||||
|
public static string From(uint rowVersion)
|
||||||
|
=> "\"" + Convert.ToBase64String(BitConverter.GetBytes(rowVersion)) + "\"";
|
||||||
|
|
||||||
|
/// <summary>Parse an <c>If-Match</c> header value (quoted, optionally weak) to a token.</summary>
|
||||||
|
public static bool TryParse(string? ifMatch, out uint rowVersion)
|
||||||
|
{
|
||||||
|
rowVersion = 0;
|
||||||
|
if (string.IsNullOrWhiteSpace(ifMatch)) return false;
|
||||||
|
|
||||||
|
var v = ifMatch.Trim();
|
||||||
|
if (v.StartsWith("W/", StringComparison.OrdinalIgnoreCase)) v = v[2..].Trim();
|
||||||
|
v = v.Trim('"');
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var bytes = Convert.FromBase64String(v);
|
||||||
|
if (bytes.Length != sizeof(uint)) return false;
|
||||||
|
rowVersion = BitConverter.ToUInt32(bytes);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch (FormatException)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
namespace ERPCore.Common.Http;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Pairs a response DTO with the aggregate's current row-version so the controller
|
||||||
|
/// can emit an <c>ETag</c> header without the token leaking into the JSON body.
|
||||||
|
/// </summary>
|
||||||
|
public sealed record ETagged<T>(T Value, uint RowVersion);
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
using ERPCore.Common.Http;
|
||||||
|
using ERPCore.System.Errors;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
|
namespace ERPCore.Controllers;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Base for the v1 API controllers. Centralises ETag / If-Match handling
|
||||||
|
/// (docs/11-BACKEND-PHASE1.md §1.6) so concurrency behaviour is uniform.
|
||||||
|
/// Each controller declares its own explicit lowercase <c>[Route]</c> to match
|
||||||
|
/// the API contract paths (docs/11 §1.1).
|
||||||
|
/// </summary>
|
||||||
|
[ApiController]
|
||||||
|
[Produces("application/json")]
|
||||||
|
public abstract class ApiControllerBase : ControllerBase
|
||||||
|
{
|
||||||
|
/// <summary>Parse a mandatory <c>If-Match</c> header, or 428 if absent/malformed.</summary>
|
||||||
|
protected uint RequireIfMatch()
|
||||||
|
{
|
||||||
|
var header = Request.Headers.IfMatch.ToString();
|
||||||
|
if (!ETag.TryParse(header, out var rowVersion))
|
||||||
|
throw new DomainException("PRECONDITION_REQUIRED", "A valid If-Match header is required for this update.", 428);
|
||||||
|
return rowVersion;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Emit the strong <c>ETag</c> response header for a row-version token.</summary>
|
||||||
|
protected void SetETag(uint rowVersion) => Response.Headers.ETag = ETag.From(rowVersion);
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
using ERPCore.Dtos.Categories;
|
||||||
|
using ERPCore.Dtos.Common;
|
||||||
|
using ERPCore.Services.Interfaces;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
|
namespace ERPCore.Controllers;
|
||||||
|
|
||||||
|
/// <summary>Category endpoints (docs/11-BACKEND-PHASE1.md §2.3).</summary>
|
||||||
|
[Route("api/v1/categories")]
|
||||||
|
public sealed class CategoriesController : ApiControllerBase
|
||||||
|
{
|
||||||
|
private readonly ICategoryService _categories;
|
||||||
|
|
||||||
|
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));
|
||||||
|
|
||||||
|
[HttpPost]
|
||||||
|
[ProducesResponseType(typeof(CategoryDto), StatusCodes.Status201Created)]
|
||||||
|
[ProducesResponseType(StatusCodes.Status422UnprocessableEntity)]
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
using ERPCore.Domain.Enums;
|
||||||
|
using ERPCore.Dtos.Common;
|
||||||
|
using ERPCore.Dtos.Items;
|
||||||
|
using ERPCore.Services.Interfaces;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
|
namespace ERPCore.Controllers;
|
||||||
|
|
||||||
|
/// <summary>Item master endpoints (docs/11-BACKEND-PHASE1.md §2.1–2.2).</summary>
|
||||||
|
[Route("api/v1/items")]
|
||||||
|
public sealed class ItemsController : ApiControllerBase
|
||||||
|
{
|
||||||
|
private readonly IItemService _items;
|
||||||
|
|
||||||
|
public ItemsController(IItemService items) => _items = items;
|
||||||
|
|
||||||
|
/// <summary>List items with optional filters and paging.</summary>
|
||||||
|
[HttpGet]
|
||||||
|
[ProducesResponseType(typeof(PagedResponse<ItemListItemDto>), StatusCodes.Status200OK)]
|
||||||
|
public async Task<ActionResult<PagedResponse<ItemListItemDto>>> List(
|
||||||
|
[FromQuery] PageQuery query,
|
||||||
|
[FromQuery] EntityStatus? status,
|
||||||
|
[FromQuery] long? categoryId,
|
||||||
|
[FromQuery] TrackingMode? trackingMode,
|
||||||
|
CancellationToken ct)
|
||||||
|
=> Ok(await _items.ListAsync(query, status, categoryId, trackingMode, ct));
|
||||||
|
|
||||||
|
/// <summary>Get a single item; returns an <c>ETag</c> for optimistic concurrency.</summary>
|
||||||
|
[HttpGet("{itemId:long}")]
|
||||||
|
[ProducesResponseType(typeof(ItemDetailDto), StatusCodes.Status200OK)]
|
||||||
|
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||||
|
public async Task<ActionResult<ItemDetailDto>> GetById(long itemId, CancellationToken ct)
|
||||||
|
{
|
||||||
|
var result = await _items.GetAsync(itemId, ct);
|
||||||
|
if (result is null) return NotFound();
|
||||||
|
|
||||||
|
SetETag(result.RowVersion);
|
||||||
|
return Ok(result.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Create an item (SKU unique). Server sets status and timestamps.</summary>
|
||||||
|
[HttpPost]
|
||||||
|
[ProducesResponseType(typeof(ItemDetailDto), StatusCodes.Status201Created)]
|
||||||
|
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||||
|
[ProducesResponseType(StatusCodes.Status422UnprocessableEntity)]
|
||||||
|
public async Task<ActionResult<ItemDetailDto>> Create([FromBody] CreateItemRequest request, CancellationToken ct)
|
||||||
|
{
|
||||||
|
var result = await _items.CreateAsync(request, ct);
|
||||||
|
SetETag(result.RowVersion);
|
||||||
|
return Created($"/api/v1/items/{result.Value.ItemId}", result.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Full update; requires <c>If-Match</c> (412 on stale ETag).</summary>
|
||||||
|
[HttpPut("{itemId:long}")]
|
||||||
|
[ProducesResponseType(typeof(ItemDetailDto), StatusCodes.Status200OK)]
|
||||||
|
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||||
|
[ProducesResponseType(StatusCodes.Status412PreconditionFailed)]
|
||||||
|
public async Task<ActionResult<ItemDetailDto>> Update(long itemId, [FromBody] UpdateItemRequest request, CancellationToken ct)
|
||||||
|
{
|
||||||
|
var expected = RequireIfMatch();
|
||||||
|
var result = await _items.UpdateAsync(itemId, request, expected, ct);
|
||||||
|
SetETag(result.RowVersion);
|
||||||
|
return Ok(result.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Activate / deactivate the item (FR-MD-08 — deactivate, not delete).</summary>
|
||||||
|
[HttpPatch("{itemId:long}/status")]
|
||||||
|
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||||
|
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||||
|
public async Task<IActionResult> SetStatus(long itemId, [FromBody] UpdateItemStatusRequest request, CancellationToken ct)
|
||||||
|
{
|
||||||
|
await _items.SetStatusAsync(itemId, request.Status, ct);
|
||||||
|
return NoContent();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Replace the item's per-warehouse reorder settings (FR-MD-05).</summary>
|
||||||
|
[HttpPut("{itemId:long}/reorder")]
|
||||||
|
[ProducesResponseType(typeof(ItemReorderSettingsDto), StatusCodes.Status200OK)]
|
||||||
|
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||||
|
public async Task<ActionResult<ItemReorderSettingsDto>> UpdateReorder(long itemId, [FromBody] UpdateReorderRequest request, CancellationToken ct)
|
||||||
|
=> Ok(await _items.UpdateReorderAsync(itemId, request, ct));
|
||||||
|
|
||||||
|
/// <summary>Replace the item's UOM conversions (FR-MD-02).</summary>
|
||||||
|
[HttpPut("{itemId:long}/uom-conversions")]
|
||||||
|
[ProducesResponseType(typeof(ItemUomConversionsDto), StatusCodes.Status200OK)]
|
||||||
|
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||||
|
public async Task<ActionResult<ItemUomConversionsDto>> UpdateUomConversions(long itemId, [FromBody] UpdateUomConversionsRequest request, CancellationToken ct)
|
||||||
|
=> Ok(await _items.UpdateUomConversionsAsync(itemId, request, ct));
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
using ERPCore.Dtos.Common;
|
||||||
|
using ERPCore.Dtos.Uoms;
|
||||||
|
using ERPCore.Services.Interfaces;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
|
namespace ERPCore.Controllers;
|
||||||
|
|
||||||
|
/// <summary>Unit-of-measure endpoints (docs/11-BACKEND-PHASE1.md §2.2).</summary>
|
||||||
|
[Route("api/v1/uoms")]
|
||||||
|
public sealed class UomsController : ApiControllerBase
|
||||||
|
{
|
||||||
|
private readonly IUomService _uoms;
|
||||||
|
|
||||||
|
public UomsController(IUomService uoms) => _uoms = uoms;
|
||||||
|
|
||||||
|
[HttpGet]
|
||||||
|
[ProducesResponseType(typeof(PagedResponse<UomDto>), StatusCodes.Status200OK)]
|
||||||
|
public async Task<ActionResult<PagedResponse<UomDto>>> List([FromQuery] PageQuery query, CancellationToken ct)
|
||||||
|
=> Ok(await _uoms.ListAsync(query, ct));
|
||||||
|
|
||||||
|
[HttpPost]
|
||||||
|
[ProducesResponseType(typeof(UomDto), StatusCodes.Status201Created)]
|
||||||
|
[ProducesResponseType(StatusCodes.Status409Conflict)]
|
||||||
|
public async Task<ActionResult<UomDto>> Create([FromBody] CreateUomRequest request, CancellationToken ct)
|
||||||
|
{
|
||||||
|
var dto = await _uoms.CreateAsync(request, ct);
|
||||||
|
return Created($"/api/v1/uoms/{dto.UomId}", dto);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
using ERPCore.Domain.Enums;
|
||||||
|
using ERPCore.Dtos.Common;
|
||||||
|
using ERPCore.Dtos.Vendors;
|
||||||
|
using ERPCore.Services.Interfaces;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
|
namespace ERPCore.Controllers;
|
||||||
|
|
||||||
|
/// <summary>Vendor master endpoints (docs/11-BACKEND-PHASE1.md §2.4).</summary>
|
||||||
|
[Route("api/v1/vendors")]
|
||||||
|
public sealed class VendorsController : ApiControllerBase
|
||||||
|
{
|
||||||
|
private readonly IVendorService _vendors;
|
||||||
|
|
||||||
|
public VendorsController(IVendorService vendors) => _vendors = vendors;
|
||||||
|
|
||||||
|
[HttpGet]
|
||||||
|
[ProducesResponseType(typeof(PagedResponse<VendorDto>), StatusCodes.Status200OK)]
|
||||||
|
public async Task<ActionResult<PagedResponse<VendorDto>>> List(
|
||||||
|
[FromQuery] PageQuery query, [FromQuery] EntityStatus? status, CancellationToken ct)
|
||||||
|
=> Ok(await _vendors.ListAsync(query, status, ct));
|
||||||
|
|
||||||
|
[HttpGet("{vendorId:long}")]
|
||||||
|
[ProducesResponseType(typeof(VendorDto), StatusCodes.Status200OK)]
|
||||||
|
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||||
|
public async Task<ActionResult<VendorDto>> GetById(long vendorId, CancellationToken ct)
|
||||||
|
{
|
||||||
|
var result = await _vendors.GetAsync(vendorId, ct);
|
||||||
|
if (result is null) return NotFound();
|
||||||
|
|
||||||
|
SetETag(result.RowVersion);
|
||||||
|
return Ok(result.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost]
|
||||||
|
[ProducesResponseType(typeof(VendorDto), StatusCodes.Status201Created)]
|
||||||
|
[ProducesResponseType(StatusCodes.Status409Conflict)]
|
||||||
|
public async Task<ActionResult<VendorDto>> Create([FromBody] CreateVendorRequest request, CancellationToken ct)
|
||||||
|
{
|
||||||
|
var result = await _vendors.CreateAsync(request, ct);
|
||||||
|
SetETag(result.RowVersion);
|
||||||
|
return Created($"/api/v1/vendors/{result.Value.VendorId}", result.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPut("{vendorId:long}")]
|
||||||
|
[ProducesResponseType(typeof(VendorDto), StatusCodes.Status200OK)]
|
||||||
|
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||||
|
[ProducesResponseType(StatusCodes.Status412PreconditionFailed)]
|
||||||
|
public async Task<ActionResult<VendorDto>> Update(long vendorId, [FromBody] UpdateVendorRequest request, CancellationToken ct)
|
||||||
|
{
|
||||||
|
var expected = RequireIfMatch();
|
||||||
|
var result = await _vendors.UpdateAsync(vendorId, request, expected, ct);
|
||||||
|
SetETag(result.RowVersion);
|
||||||
|
return Ok(result.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPatch("{vendorId:long}/status")]
|
||||||
|
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||||
|
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||||
|
public async Task<IActionResult> SetStatus(long vendorId, [FromBody] UpdateVendorStatusRequest request, CancellationToken ct)
|
||||||
|
{
|
||||||
|
await _vendors.SetStatusAsync(vendorId, request.Status, ct);
|
||||||
|
return NoContent();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
using ERPCore.Dtos.Common;
|
||||||
|
using ERPCore.Dtos.Warehouses;
|
||||||
|
using ERPCore.Services.Interfaces;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
|
namespace ERPCore.Controllers;
|
||||||
|
|
||||||
|
/// <summary>Warehouse & bin endpoints (docs/11-BACKEND-PHASE1.md §2.5).</summary>
|
||||||
|
[Route("api/v1/warehouses")]
|
||||||
|
public sealed class WarehousesController : ApiControllerBase
|
||||||
|
{
|
||||||
|
private readonly IWarehouseService _warehouses;
|
||||||
|
|
||||||
|
public WarehousesController(IWarehouseService warehouses) => _warehouses = warehouses;
|
||||||
|
|
||||||
|
[HttpGet]
|
||||||
|
[ProducesResponseType(typeof(PagedResponse<WarehouseDto>), StatusCodes.Status200OK)]
|
||||||
|
public async Task<ActionResult<PagedResponse<WarehouseDto>>> List([FromQuery] PageQuery query, CancellationToken ct)
|
||||||
|
=> Ok(await _warehouses.ListAsync(query, ct));
|
||||||
|
|
||||||
|
[HttpGet("{warehouseId:long}")]
|
||||||
|
[ProducesResponseType(typeof(WarehouseDto), StatusCodes.Status200OK)]
|
||||||
|
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||||
|
public async Task<ActionResult<WarehouseDto>> GetById(long warehouseId, CancellationToken ct)
|
||||||
|
{
|
||||||
|
var dto = await _warehouses.GetAsync(warehouseId, ct);
|
||||||
|
return dto is null ? NotFound() : Ok(dto);
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost]
|
||||||
|
[ProducesResponseType(typeof(WarehouseDto), StatusCodes.Status201Created)]
|
||||||
|
[ProducesResponseType(StatusCodes.Status409Conflict)]
|
||||||
|
public async Task<ActionResult<WarehouseDto>> Create([FromBody] CreateWarehouseRequest request, CancellationToken ct)
|
||||||
|
{
|
||||||
|
var dto = await _warehouses.CreateAsync(request, ct);
|
||||||
|
return Created($"/api/v1/warehouses/{dto.WarehouseId}", dto);
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet("{warehouseId:long}/bins")]
|
||||||
|
[ProducesResponseType(typeof(IReadOnlyList<BinDto>), StatusCodes.Status200OK)]
|
||||||
|
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||||
|
public async Task<ActionResult<IReadOnlyList<BinDto>>> ListBins(long warehouseId, CancellationToken ct)
|
||||||
|
=> Ok(await _warehouses.ListBinsAsync(warehouseId, ct));
|
||||||
|
|
||||||
|
[HttpPost("{warehouseId:long}/bins")]
|
||||||
|
[ProducesResponseType(typeof(BinDto), StatusCodes.Status201Created)]
|
||||||
|
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||||
|
[ProducesResponseType(StatusCodes.Status409Conflict)]
|
||||||
|
public async Task<ActionResult<BinDto>> CreateBin(long warehouseId, [FromBody] CreateBinRequest request, CancellationToken ct)
|
||||||
|
{
|
||||||
|
var dto = await _warehouses.CreateBinAsync(warehouseId, request, ct);
|
||||||
|
return Created($"/api/v1/warehouses/{warehouseId}/bins/{dto.BinId}", dto);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
namespace ERPCore.Domain.Entities;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Bin / storage location within a warehouse (FR-MD-07, FR-WH-02). Stock is
|
||||||
|
/// tracked to bin level. Model: docs/10-BACKEND-PHASE1.md Part C.1.
|
||||||
|
/// </summary>
|
||||||
|
public class Bin
|
||||||
|
{
|
||||||
|
public long BinId { get; set; }
|
||||||
|
|
||||||
|
public long WarehouseId { get; set; }
|
||||||
|
public Warehouse? Warehouse { get; set; }
|
||||||
|
|
||||||
|
public string Code { get; set; } = string.Empty;
|
||||||
|
public string? BinType { get; set; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
namespace ERPCore.Domain.Entities;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Hierarchical item category (FR-MD-04). A null <see cref="ParentId"/> denotes a
|
||||||
|
/// root category. Model: docs/10-BACKEND-PHASE1.md Part C.1.
|
||||||
|
/// </summary>
|
||||||
|
public class Category
|
||||||
|
{
|
||||||
|
public long CategoryId { get; set; }
|
||||||
|
public string Name { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
public long? ParentId { get; set; }
|
||||||
|
public Category? Parent { get; set; }
|
||||||
|
public ICollection<Category> Children { get; set; } = new List<Category>();
|
||||||
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
using ERPCore.Domain.Enums;
|
||||||
|
|
||||||
|
namespace ERPCore.Domain.Entities;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Item master (FR-MD-01). Mutable aggregate: carries a <see cref="RowVersion"/>
|
||||||
|
/// concurrency token surfaced as an ETag (docs/10 Part C.10). SKU is unique.
|
||||||
|
/// Model: docs/10-BACKEND-PHASE1.md Part C.1.
|
||||||
|
/// </summary>
|
||||||
|
public class Item
|
||||||
|
{
|
||||||
|
public long ItemId { get; set; }
|
||||||
|
public string Sku { get; set; } = string.Empty;
|
||||||
|
public string Name { get; set; } = string.Empty;
|
||||||
|
public string? Description { get; set; }
|
||||||
|
|
||||||
|
public long CategoryId { get; set; }
|
||||||
|
public Category? Category { get; set; }
|
||||||
|
|
||||||
|
public long BaseUomId { get; set; }
|
||||||
|
public Uom? BaseUom { get; set; }
|
||||||
|
|
||||||
|
public long? DefaultVendorId { get; set; }
|
||||||
|
public Vendor? DefaultVendor { get; set; }
|
||||||
|
|
||||||
|
public ItemType ItemType { get; set; }
|
||||||
|
public TrackingMode TrackingMode { get; set; }
|
||||||
|
public string? TaxClass { get; set; }
|
||||||
|
public EntityStatus Status { get; set; } = EntityStatus.Active;
|
||||||
|
|
||||||
|
public DateTime CreatedAt { get; set; }
|
||||||
|
public DateTime? UpdatedAt { get; set; }
|
||||||
|
|
||||||
|
/// <summary>PostgreSQL xmin-backed optimistic concurrency token (ETag source).</summary>
|
||||||
|
public uint RowVersion { get; set; }
|
||||||
|
|
||||||
|
public ICollection<ItemReorder> ReorderSettings { get; set; } = new List<ItemReorder>();
|
||||||
|
public ICollection<UomConversion> UomConversions { get; set; } = new List<UomConversion>();
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
namespace ERPCore.Domain.Entities;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Reorder policy for an item, optionally per warehouse (FR-MD-05). Reorder alerts
|
||||||
|
/// are computed from these versus available stock (FR-STK-10) — not stored.
|
||||||
|
/// Model: docs/10-BACKEND-PHASE1.md Part C.1.
|
||||||
|
/// </summary>
|
||||||
|
public class ItemReorder
|
||||||
|
{
|
||||||
|
public long ReorderId { get; set; }
|
||||||
|
|
||||||
|
public long ItemId { get; set; }
|
||||||
|
public Item? Item { get; set; }
|
||||||
|
|
||||||
|
public long WarehouseId { get; set; }
|
||||||
|
public Warehouse? Warehouse { get; set; }
|
||||||
|
|
||||||
|
public decimal ReorderPoint { get; set; }
|
||||||
|
public decimal ReorderQty { get; set; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
namespace ERPCore.Domain.Entities;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Unit of Measure (FR-MD-02). Referenced as an item's base UOM and as the
|
||||||
|
/// endpoints of a <see cref="UomConversion"/>. Model: docs/10-BACKEND-PHASE1.md Part C.1.
|
||||||
|
/// </summary>
|
||||||
|
public class Uom
|
||||||
|
{
|
||||||
|
public long UomId { get; set; }
|
||||||
|
public string Name { get; set; } = string.Empty;
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
namespace ERPCore.Domain.Entities;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Per-item conversion factor between two UOMs (FR-MD-02/03): quantity in
|
||||||
|
/// <see cref="FromUomId"/> × <see cref="Factor"/> = quantity in <see cref="ToUomId"/>.
|
||||||
|
/// Model: docs/10-BACKEND-PHASE1.md Part C.1.
|
||||||
|
/// </summary>
|
||||||
|
public class UomConversion
|
||||||
|
{
|
||||||
|
public long ConversionId { get; set; }
|
||||||
|
|
||||||
|
public long ItemId { get; set; }
|
||||||
|
public Item? Item { get; set; }
|
||||||
|
|
||||||
|
public long FromUomId { get; set; }
|
||||||
|
public Uom? FromUom { get; set; }
|
||||||
|
|
||||||
|
public long ToUomId { get; set; }
|
||||||
|
public Uom? ToUom { get; set; }
|
||||||
|
|
||||||
|
public decimal Factor { get; set; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
using ERPCore.Domain.Enums;
|
||||||
|
|
||||||
|
namespace ERPCore.Domain.Entities;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Vendor master (FR-MD-06). Mutable aggregate with a <see cref="RowVersion"/>
|
||||||
|
/// ETag token. Deactivated, not deleted, when referenced (FR-MD-08).
|
||||||
|
/// Model: docs/10-BACKEND-PHASE1.md Part C.1.
|
||||||
|
/// </summary>
|
||||||
|
public class Vendor
|
||||||
|
{
|
||||||
|
public long VendorId { get; set; }
|
||||||
|
public string Code { get; set; } = string.Empty;
|
||||||
|
public string Name { get; set; } = string.Empty;
|
||||||
|
public string? Terms { get; set; }
|
||||||
|
public string? TaxReg { get; set; }
|
||||||
|
public string Currency { get; set; } = "LKR";
|
||||||
|
public EntityStatus Status { get; set; } = EntityStatus.Active;
|
||||||
|
|
||||||
|
public DateTime CreatedAt { get; set; }
|
||||||
|
public DateTime? UpdatedAt { get; set; }
|
||||||
|
|
||||||
|
/// <summary>PostgreSQL xmin-backed optimistic concurrency token (ETag source).</summary>
|
||||||
|
public uint RowVersion { get; set; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
namespace ERPCore.Domain.Entities;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Warehouse master (FR-MD-07, FR-WH-01). Owns a bin/location hierarchy.
|
||||||
|
/// Model: docs/10-BACKEND-PHASE1.md Part C.1.
|
||||||
|
/// </summary>
|
||||||
|
public class Warehouse
|
||||||
|
{
|
||||||
|
public long WarehouseId { get; set; }
|
||||||
|
public string Code { get; set; } = string.Empty;
|
||||||
|
public string Name { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
public ICollection<Bin> Bins { get; set; } = new List<Bin>();
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
namespace ERPCore.Domain.Enums;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Lifecycle status for deactivatable master data (Item, Vendor). Masters are
|
||||||
|
/// never hard-deleted while referenced — they are set <see cref="Inactive"/>
|
||||||
|
/// instead (FR-MD-08). Stored as a string.
|
||||||
|
/// </summary>
|
||||||
|
public enum EntityStatus
|
||||||
|
{
|
||||||
|
Active,
|
||||||
|
Inactive
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
namespace ERPCore.Domain.Enums;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Item classification (FR-MD-01). Values match the <c>itemType</c> enum in
|
||||||
|
/// docs/11-BACKEND-PHASE1.md §8. Stored as a string in the database.
|
||||||
|
/// </summary>
|
||||||
|
public enum ItemType
|
||||||
|
{
|
||||||
|
Stocked,
|
||||||
|
NonStocked,
|
||||||
|
Service
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
namespace ERPCore.Domain.Enums;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// How on-hand units of an item are individually tracked (FR-MD-01). Values match
|
||||||
|
/// the <c>trackingMode</c> enum in docs/11-BACKEND-PHASE1.md §8. Stored as a string.
|
||||||
|
/// </summary>
|
||||||
|
public enum TrackingMode
|
||||||
|
{
|
||||||
|
None,
|
||||||
|
Batch,
|
||||||
|
Serial
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
|
||||||
|
namespace ERPCore.Dtos.Categories;
|
||||||
|
|
||||||
|
/// <summary>Flat category resource (docs/11-BACKEND-PHASE1.md §2.3).</summary>
|
||||||
|
public sealed record CategoryDto(long CategoryId, string Name, long? ParentId);
|
||||||
|
|
||||||
|
/// <summary>Nested category node for <c>GET /categories?tree=true</c>.</summary>
|
||||||
|
public sealed record CategoryTreeDto(long CategoryId, string Name, long? ParentId, IReadOnlyList<CategoryTreeDto> Children);
|
||||||
|
|
||||||
|
public sealed class CreateCategoryRequest
|
||||||
|
{
|
||||||
|
[Required, StringLength(200)] public string Name { get; set; } = string.Empty;
|
||||||
|
public long? ParentId { get; set; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
namespace ERPCore.Dtos.Common;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Shared paging/sorting query binding (docs/11-BACKEND-PHASE1.md §1.5). Page size
|
||||||
|
/// is clamped to <see cref="MaxPageSize"/> to enforce pagination bounds
|
||||||
|
/// (02-SECURITY B.6). Bind from the query string on list endpoints.
|
||||||
|
/// </summary>
|
||||||
|
public class PageQuery
|
||||||
|
{
|
||||||
|
public const int MaxPageSize = 200;
|
||||||
|
public const int DefaultPageSize = 20;
|
||||||
|
|
||||||
|
private int _page = 1;
|
||||||
|
private int _pageSize = DefaultPageSize;
|
||||||
|
|
||||||
|
/// <summary>1-based page number (default 1).</summary>
|
||||||
|
public int Page
|
||||||
|
{
|
||||||
|
get => _page;
|
||||||
|
set => _page = value < 1 ? 1 : value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Page size (default 20, clamped to 1..200).</summary>
|
||||||
|
public int PageSize
|
||||||
|
{
|
||||||
|
get => _pageSize;
|
||||||
|
set => _pageSize = value < 1 ? DefaultPageSize : Math.Min(value, MaxPageSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Free-text search term (<c>q</c>).</summary>
|
||||||
|
public string? Q { get; set; }
|
||||||
|
|
||||||
|
/// <summary>Sort spec, e.g. <c>name</c> or <c>-createdAt</c>.</summary>
|
||||||
|
public string? Sort { get; set; }
|
||||||
|
|
||||||
|
public int Skip => (Page - 1) * PageSize;
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
namespace ERPCore.Dtos.Common;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// List envelope matching docs/11-BACKEND-PHASE1.md §1.4:
|
||||||
|
/// <c>{ "items": [...], "pagination": { page, pageSize, totalItems, totalPages } }</c>.
|
||||||
|
/// </summary>
|
||||||
|
public sealed record PagedResponse<T>(IReadOnlyList<T> Items, PaginationDto Pagination)
|
||||||
|
{
|
||||||
|
public static PagedResponse<T> Create(IReadOnlyList<T> items, int page, int pageSize, int totalItems)
|
||||||
|
{
|
||||||
|
var totalPages = pageSize <= 0 ? 0 : (int)Math.Ceiling(totalItems / (double)pageSize);
|
||||||
|
return new PagedResponse<T>(items, new PaginationDto(page, pageSize, totalItems, totalPages));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Pagination metadata block (docs/11 §1.4).</summary>
|
||||||
|
public sealed record PaginationDto(int Page, int PageSize, int TotalItems, int TotalPages);
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
using ERPCore.Domain.Enums;
|
||||||
|
|
||||||
|
namespace ERPCore.Dtos.Items;
|
||||||
|
|
||||||
|
// Response DTOs (docs/11-BACKEND-PHASE1.md §2.1) --------------------------------
|
||||||
|
|
||||||
|
/// <summary>Row shape for <c>GET /items</c>.</summary>
|
||||||
|
public sealed record ItemListItemDto(
|
||||||
|
long ItemId, string Sku, string Name, long CategoryId, long BaseUomId,
|
||||||
|
long? DefaultVendorId, ItemType ItemType, TrackingMode TrackingMode,
|
||||||
|
string? TaxClass, EntityStatus Status);
|
||||||
|
|
||||||
|
/// <summary>A single per-warehouse reorder policy row.</summary>
|
||||||
|
public sealed record ItemReorderDto(long WarehouseId, decimal ReorderPoint, decimal ReorderQty);
|
||||||
|
|
||||||
|
/// <summary>Full item resource for <c>GET /items/{id}</c> and create/update responses.</summary>
|
||||||
|
public sealed record ItemDetailDto(
|
||||||
|
long ItemId, string Sku, string Name, string? Description, long CategoryId,
|
||||||
|
long BaseUomId, long? DefaultVendorId, ItemType ItemType, TrackingMode TrackingMode,
|
||||||
|
string? TaxClass, EntityStatus Status, IReadOnlyList<ItemReorderDto> Reorder,
|
||||||
|
DateTime CreatedAt, DateTime? UpdatedAt);
|
||||||
|
|
||||||
|
/// <summary>UOM conversion row (docs/11 §2.2).</summary>
|
||||||
|
public sealed record UomConversionDto(long ConversionId, long FromUom, long ToUom, decimal Factor);
|
||||||
|
|
||||||
|
/// <summary>Response body for <c>PUT /items/{id}/uom-conversions</c>.</summary>
|
||||||
|
public sealed record ItemUomConversionsDto(long ItemId, long BaseUomId, IReadOnlyList<UomConversionDto> Conversions);
|
||||||
|
|
||||||
|
/// <summary>Response body for <c>PUT /items/{id}/reorder</c>.</summary>
|
||||||
|
public sealed record ItemReorderSettingsDto(IReadOnlyList<ItemReorderDto> Settings);
|
||||||
|
|
||||||
|
// Request DTOs — narrow: server-controlled fields (status, ids, timestamps)
|
||||||
|
// are intentionally excluded to prevent over-posting (02-SECURITY B.6 / C.1). ----
|
||||||
|
|
||||||
|
public sealed class CreateItemRequest
|
||||||
|
{
|
||||||
|
[Required, StringLength(50)] public string Sku { get; set; } = string.Empty;
|
||||||
|
[Required, StringLength(200)] public string Name { get; set; } = string.Empty;
|
||||||
|
[StringLength(1000)] public string? Description { get; set; }
|
||||||
|
[Required] public long CategoryId { get; set; }
|
||||||
|
[Required] public long BaseUomId { get; set; }
|
||||||
|
public long? DefaultVendorId { get; set; }
|
||||||
|
[Required, EnumDataType(typeof(ItemType))] public ItemType ItemType { get; set; }
|
||||||
|
[EnumDataType(typeof(TrackingMode))] public TrackingMode TrackingMode { get; set; } = TrackingMode.None;
|
||||||
|
[StringLength(20)] public string? TaxClass { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class UpdateItemRequest
|
||||||
|
{
|
||||||
|
[Required, StringLength(50)] public string Sku { get; set; } = string.Empty;
|
||||||
|
[Required, StringLength(200)] public string Name { get; set; } = string.Empty;
|
||||||
|
[StringLength(1000)] public string? Description { get; set; }
|
||||||
|
[Required] public long CategoryId { get; set; }
|
||||||
|
[Required] public long BaseUomId { get; set; }
|
||||||
|
public long? DefaultVendorId { get; set; }
|
||||||
|
[Required, EnumDataType(typeof(ItemType))] public ItemType ItemType { get; set; }
|
||||||
|
[EnumDataType(typeof(TrackingMode))] public TrackingMode TrackingMode { get; set; } = TrackingMode.None;
|
||||||
|
[StringLength(20)] public string? TaxClass { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class UpdateItemStatusRequest
|
||||||
|
{
|
||||||
|
[Required, EnumDataType(typeof(EntityStatus))] public EntityStatus Status { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class ReorderSettingInput
|
||||||
|
{
|
||||||
|
[Required] public long WarehouseId { get; set; }
|
||||||
|
[Range(0, double.MaxValue)] public decimal ReorderPoint { get; set; }
|
||||||
|
[Range(0, double.MaxValue)] public decimal ReorderQty { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class UpdateReorderRequest
|
||||||
|
{
|
||||||
|
[Required, MinLength(1)] public List<ReorderSettingInput> Settings { get; set; } = new();
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class UomConversionInput
|
||||||
|
{
|
||||||
|
[Required] public long FromUom { get; set; }
|
||||||
|
[Required] public long ToUom { get; set; }
|
||||||
|
[Range(0.000001, double.MaxValue)] public decimal Factor { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class UpdateUomConversionsRequest
|
||||||
|
{
|
||||||
|
[Required, MinLength(1)] public List<UomConversionInput> Conversions { get; set; } = new();
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
|
||||||
|
namespace ERPCore.Dtos.Uoms;
|
||||||
|
|
||||||
|
/// <summary>UOM resource (docs/11-BACKEND-PHASE1.md §2.2).</summary>
|
||||||
|
public sealed record UomDto(long UomId, string Name);
|
||||||
|
|
||||||
|
public sealed class CreateUomRequest
|
||||||
|
{
|
||||||
|
[Required, StringLength(50)] public string Name { get; set; } = string.Empty;
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
using ERPCore.Domain.Enums;
|
||||||
|
|
||||||
|
namespace ERPCore.Dtos.Vendors;
|
||||||
|
|
||||||
|
/// <summary>Vendor resource (docs/11-BACKEND-PHASE1.md §2.4).</summary>
|
||||||
|
public sealed record VendorDto(
|
||||||
|
long VendorId, string Code, string Name, string? Terms, string? TaxReg,
|
||||||
|
string Currency, EntityStatus Status, DateTime CreatedAt, DateTime? UpdatedAt);
|
||||||
|
|
||||||
|
public sealed class CreateVendorRequest
|
||||||
|
{
|
||||||
|
[Required, StringLength(50)] public string Code { get; set; } = string.Empty;
|
||||||
|
[Required, StringLength(200)] public string Name { get; set; } = string.Empty;
|
||||||
|
[StringLength(50)] public string? Terms { get; set; }
|
||||||
|
[StringLength(50)] public string? TaxReg { get; set; }
|
||||||
|
[Required, StringLength(3, MinimumLength = 3)] public string Currency { get; set; } = "LKR";
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class UpdateVendorRequest
|
||||||
|
{
|
||||||
|
[Required, StringLength(50)] public string Code { get; set; } = string.Empty;
|
||||||
|
[Required, StringLength(200)] public string Name { get; set; } = string.Empty;
|
||||||
|
[StringLength(50)] public string? Terms { get; set; }
|
||||||
|
[StringLength(50)] public string? TaxReg { get; set; }
|
||||||
|
[Required, StringLength(3, MinimumLength = 3)] public string Currency { get; set; } = "LKR";
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class UpdateVendorStatusRequest
|
||||||
|
{
|
||||||
|
[Required, EnumDataType(typeof(EntityStatus))] public EntityStatus Status { get; set; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
|
||||||
|
namespace ERPCore.Dtos.Warehouses;
|
||||||
|
|
||||||
|
/// <summary>Warehouse resource (docs/11-BACKEND-PHASE1.md §2.5).</summary>
|
||||||
|
public sealed record WarehouseDto(long WarehouseId, string Code, string Name);
|
||||||
|
|
||||||
|
/// <summary>Bin/location resource (docs/11 §2.5).</summary>
|
||||||
|
public sealed record BinDto(long BinId, long WarehouseId, string Code, string? BinType);
|
||||||
|
|
||||||
|
public sealed class CreateWarehouseRequest
|
||||||
|
{
|
||||||
|
[Required, StringLength(50)] public string Code { get; set; } = string.Empty;
|
||||||
|
[Required, StringLength(200)] public string Name { get; set; } = string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class CreateBinRequest
|
||||||
|
{
|
||||||
|
[Required, StringLength(50)] public string Code { get; set; } = string.Empty;
|
||||||
|
[StringLength(50)] public string? BinType { get; set; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
using ERPCore.Domain.Entities;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||||
|
|
||||||
|
namespace ERPCore.Infra.Persistence.Configurations;
|
||||||
|
|
||||||
|
public sealed class BinConfiguration : IEntityTypeConfiguration<Bin>
|
||||||
|
{
|
||||||
|
public void Configure(EntityTypeBuilder<Bin> builder)
|
||||||
|
{
|
||||||
|
builder.ToTable("bins");
|
||||||
|
builder.HasKey(b => b.BinId);
|
||||||
|
|
||||||
|
builder.Property(b => b.Code).IsRequired().HasMaxLength(50);
|
||||||
|
builder.Property(b => b.BinType).HasMaxLength(50);
|
||||||
|
|
||||||
|
builder.HasOne(b => b.Warehouse)
|
||||||
|
.WithMany(w => w.Bins)
|
||||||
|
.HasForeignKey(b => b.WarehouseId)
|
||||||
|
.OnDelete(DeleteBehavior.Cascade);
|
||||||
|
|
||||||
|
// Bin code unique within its warehouse.
|
||||||
|
builder.HasIndex(b => new { b.WarehouseId, b.Code }).IsUnique();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
using ERPCore.Domain.Entities;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||||
|
|
||||||
|
namespace ERPCore.Infra.Persistence.Configurations;
|
||||||
|
|
||||||
|
public sealed class CategoryConfiguration : IEntityTypeConfiguration<Category>
|
||||||
|
{
|
||||||
|
public void Configure(EntityTypeBuilder<Category> builder)
|
||||||
|
{
|
||||||
|
builder.ToTable("categories");
|
||||||
|
builder.HasKey(c => c.CategoryId);
|
||||||
|
|
||||||
|
builder.Property(c => c.Name).IsRequired().HasMaxLength(200);
|
||||||
|
|
||||||
|
builder.HasOne(c => c.Parent)
|
||||||
|
.WithMany(c => c.Children)
|
||||||
|
.HasForeignKey(c => c.ParentId)
|
||||||
|
.OnDelete(DeleteBehavior.Restrict);
|
||||||
|
|
||||||
|
builder.HasIndex(c => c.ParentId);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
using ERPCore.Domain.Entities;
|
||||||
|
using ERPCore.Domain.Enums;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||||
|
|
||||||
|
namespace ERPCore.Infra.Persistence.Configurations;
|
||||||
|
|
||||||
|
public sealed class ItemConfiguration : IEntityTypeConfiguration<Item>
|
||||||
|
{
|
||||||
|
public void Configure(EntityTypeBuilder<Item> builder)
|
||||||
|
{
|
||||||
|
builder.ToTable("items");
|
||||||
|
builder.HasKey(i => i.ItemId);
|
||||||
|
|
||||||
|
builder.Property(i => i.Sku).IsRequired().HasMaxLength(50);
|
||||||
|
builder.HasIndex(i => i.Sku).IsUnique();
|
||||||
|
|
||||||
|
builder.Property(i => i.Name).IsRequired().HasMaxLength(200);
|
||||||
|
builder.Property(i => i.Description).HasMaxLength(1000);
|
||||||
|
builder.Property(i => i.TaxClass).HasMaxLength(20);
|
||||||
|
|
||||||
|
builder.Property(i => i.ItemType)
|
||||||
|
.HasConversion<string>().HasMaxLength(20).IsRequired();
|
||||||
|
builder.Property(i => i.TrackingMode)
|
||||||
|
.HasConversion<string>().HasMaxLength(20).IsRequired();
|
||||||
|
builder.Property(i => i.Status)
|
||||||
|
.HasConversion<string>().HasMaxLength(20).IsRequired()
|
||||||
|
.HasDefaultValue(EntityStatus.Active);
|
||||||
|
|
||||||
|
builder.Property(i => i.CreatedAt).IsRequired();
|
||||||
|
|
||||||
|
// PostgreSQL xmin system column as the optimistic concurrency token (ETag).
|
||||||
|
builder.Property(i => i.RowVersion).IsRowVersion();
|
||||||
|
|
||||||
|
builder.HasOne(i => i.Category)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey(i => i.CategoryId)
|
||||||
|
.OnDelete(DeleteBehavior.Restrict);
|
||||||
|
|
||||||
|
builder.HasOne(i => i.BaseUom)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey(i => i.BaseUomId)
|
||||||
|
.OnDelete(DeleteBehavior.Restrict);
|
||||||
|
|
||||||
|
builder.HasOne(i => i.DefaultVendor)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey(i => i.DefaultVendorId)
|
||||||
|
.OnDelete(DeleteBehavior.Restrict);
|
||||||
|
|
||||||
|
builder.HasIndex(i => i.Status);
|
||||||
|
builder.HasIndex(i => i.CategoryId);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
using ERPCore.Domain.Entities;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||||
|
|
||||||
|
namespace ERPCore.Infra.Persistence.Configurations;
|
||||||
|
|
||||||
|
public sealed class ItemReorderConfiguration : IEntityTypeConfiguration<ItemReorder>
|
||||||
|
{
|
||||||
|
public void Configure(EntityTypeBuilder<ItemReorder> builder)
|
||||||
|
{
|
||||||
|
builder.ToTable("item_reorders");
|
||||||
|
builder.HasKey(r => r.ReorderId);
|
||||||
|
|
||||||
|
builder.Property(r => r.ReorderPoint).HasPrecision(18, 4);
|
||||||
|
builder.Property(r => r.ReorderQty).HasPrecision(18, 4);
|
||||||
|
|
||||||
|
builder.HasOne(r => r.Item)
|
||||||
|
.WithMany(i => i.ReorderSettings)
|
||||||
|
.HasForeignKey(r => r.ItemId)
|
||||||
|
.OnDelete(DeleteBehavior.Cascade);
|
||||||
|
|
||||||
|
builder.HasOne(r => r.Warehouse)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey(r => r.WarehouseId)
|
||||||
|
.OnDelete(DeleteBehavior.Restrict);
|
||||||
|
|
||||||
|
// One reorder policy per (item, warehouse).
|
||||||
|
builder.HasIndex(r => new { r.ItemId, r.WarehouseId }).IsUnique();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
using ERPCore.Domain.Entities;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||||
|
|
||||||
|
namespace ERPCore.Infra.Persistence.Configurations;
|
||||||
|
|
||||||
|
public sealed class UomConfiguration : IEntityTypeConfiguration<Uom>
|
||||||
|
{
|
||||||
|
public void Configure(EntityTypeBuilder<Uom> builder)
|
||||||
|
{
|
||||||
|
builder.ToTable("uoms");
|
||||||
|
builder.HasKey(u => u.UomId);
|
||||||
|
|
||||||
|
builder.Property(u => u.Name).IsRequired().HasMaxLength(50);
|
||||||
|
builder.HasIndex(u => u.Name).IsUnique();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
using ERPCore.Domain.Entities;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||||
|
|
||||||
|
namespace ERPCore.Infra.Persistence.Configurations;
|
||||||
|
|
||||||
|
public sealed class UomConversionConfiguration : IEntityTypeConfiguration<UomConversion>
|
||||||
|
{
|
||||||
|
public void Configure(EntityTypeBuilder<UomConversion> builder)
|
||||||
|
{
|
||||||
|
builder.ToTable("uom_conversions");
|
||||||
|
builder.HasKey(c => c.ConversionId);
|
||||||
|
|
||||||
|
builder.Property(c => c.Factor).HasPrecision(18, 6);
|
||||||
|
|
||||||
|
builder.HasOne(c => c.Item)
|
||||||
|
.WithMany(i => i.UomConversions)
|
||||||
|
.HasForeignKey(c => c.ItemId)
|
||||||
|
.OnDelete(DeleteBehavior.Cascade);
|
||||||
|
|
||||||
|
builder.HasOne(c => c.FromUom)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey(c => c.FromUomId)
|
||||||
|
.OnDelete(DeleteBehavior.Restrict);
|
||||||
|
|
||||||
|
builder.HasOne(c => c.ToUom)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey(c => c.ToUomId)
|
||||||
|
.OnDelete(DeleteBehavior.Restrict);
|
||||||
|
|
||||||
|
// One conversion per (item, from, to) triple.
|
||||||
|
builder.HasIndex(c => new { c.ItemId, c.FromUomId, c.ToUomId }).IsUnique();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
using ERPCore.Domain.Entities;
|
||||||
|
using ERPCore.Domain.Enums;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||||
|
|
||||||
|
namespace ERPCore.Infra.Persistence.Configurations;
|
||||||
|
|
||||||
|
public sealed class VendorConfiguration : IEntityTypeConfiguration<Vendor>
|
||||||
|
{
|
||||||
|
public void Configure(EntityTypeBuilder<Vendor> builder)
|
||||||
|
{
|
||||||
|
builder.ToTable("vendors");
|
||||||
|
builder.HasKey(v => v.VendorId);
|
||||||
|
|
||||||
|
builder.Property(v => v.Code).IsRequired().HasMaxLength(50);
|
||||||
|
builder.HasIndex(v => v.Code).IsUnique();
|
||||||
|
|
||||||
|
builder.Property(v => v.Name).IsRequired().HasMaxLength(200);
|
||||||
|
builder.Property(v => v.Terms).HasMaxLength(50);
|
||||||
|
builder.Property(v => v.TaxReg).HasMaxLength(50);
|
||||||
|
builder.Property(v => v.Currency).IsRequired().HasMaxLength(3).HasDefaultValue("LKR");
|
||||||
|
|
||||||
|
builder.Property(v => v.Status)
|
||||||
|
.HasConversion<string>().HasMaxLength(20).IsRequired()
|
||||||
|
.HasDefaultValue(EntityStatus.Active);
|
||||||
|
|
||||||
|
builder.Property(v => v.CreatedAt).IsRequired();
|
||||||
|
|
||||||
|
// PostgreSQL xmin system column as the optimistic concurrency token (ETag).
|
||||||
|
builder.Property(v => v.RowVersion).IsRowVersion();
|
||||||
|
|
||||||
|
builder.HasIndex(v => v.Status);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
using ERPCore.Domain.Entities;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||||
|
|
||||||
|
namespace ERPCore.Infra.Persistence.Configurations;
|
||||||
|
|
||||||
|
public sealed class WarehouseConfiguration : IEntityTypeConfiguration<Warehouse>
|
||||||
|
{
|
||||||
|
public void Configure(EntityTypeBuilder<Warehouse> builder)
|
||||||
|
{
|
||||||
|
builder.ToTable("warehouses");
|
||||||
|
builder.HasKey(w => w.WarehouseId);
|
||||||
|
|
||||||
|
builder.Property(w => w.Code).IsRequired().HasMaxLength(50);
|
||||||
|
builder.HasIndex(w => w.Code).IsUnique();
|
||||||
|
|
||||||
|
builder.Property(w => w.Name).IsRequired().HasMaxLength(200);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
using ERPCore.Domain.Entities;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
namespace ERPCore.Infra.Persistence;
|
namespace ERPCore.Infra.Persistence;
|
||||||
@@ -14,6 +15,16 @@ public class ErpDbContext : DbContext
|
|||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- Master Data (docs/10 Part C.1) ---
|
||||||
|
public DbSet<Category> Categories => Set<Category>();
|
||||||
|
public DbSet<Uom> Uoms => Set<Uom>();
|
||||||
|
public DbSet<UomConversion> UomConversions => Set<UomConversion>();
|
||||||
|
public DbSet<Item> Items => Set<Item>();
|
||||||
|
public DbSet<ItemReorder> ItemReorders => Set<ItemReorder>();
|
||||||
|
public DbSet<Vendor> Vendors => Set<Vendor>();
|
||||||
|
public DbSet<Warehouse> Warehouses => Set<Warehouse>();
|
||||||
|
public DbSet<Bin> Bins => Set<Bin>();
|
||||||
|
|
||||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||||
{
|
{
|
||||||
base.OnModelCreating(modelBuilder);
|
base.OnModelCreating(modelBuilder);
|
||||||
|
|||||||
+445
@@ -0,0 +1,445 @@
|
|||||||
|
// <auto-generated />
|
||||||
|
using System;
|
||||||
|
using ERPCore.Infra.Persistence;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||||
|
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace ERPCore.Infra.Persistence.Migrations
|
||||||
|
{
|
||||||
|
[DbContext(typeof(ErpDbContext))]
|
||||||
|
[Migration("20260709095653_InitialCreate")]
|
||||||
|
partial class InitialCreate
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||||
|
{
|
||||||
|
#pragma warning disable 612, 618
|
||||||
|
modelBuilder
|
||||||
|
.HasAnnotation("ProductVersion", "10.0.9")
|
||||||
|
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||||
|
|
||||||
|
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||||
|
|
||||||
|
modelBuilder.Entity("ERPCore.Domain.Entities.Bin", b =>
|
||||||
|
{
|
||||||
|
b.Property<long>("BinId")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("bigint");
|
||||||
|
|
||||||
|
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("BinId"));
|
||||||
|
|
||||||
|
b.Property<string>("BinType")
|
||||||
|
.HasMaxLength(50)
|
||||||
|
.HasColumnType("character varying(50)");
|
||||||
|
|
||||||
|
b.Property<string>("Code")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(50)
|
||||||
|
.HasColumnType("character varying(50)");
|
||||||
|
|
||||||
|
b.Property<long>("WarehouseId")
|
||||||
|
.HasColumnType("bigint");
|
||||||
|
|
||||||
|
b.HasKey("BinId");
|
||||||
|
|
||||||
|
b.HasIndex("WarehouseId", "Code")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("bins", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("ERPCore.Domain.Entities.Category", b =>
|
||||||
|
{
|
||||||
|
b.Property<long>("CategoryId")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("bigint");
|
||||||
|
|
||||||
|
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("CategoryId"));
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(200)
|
||||||
|
.HasColumnType("character varying(200)");
|
||||||
|
|
||||||
|
b.Property<long?>("ParentId")
|
||||||
|
.HasColumnType("bigint");
|
||||||
|
|
||||||
|
b.HasKey("CategoryId");
|
||||||
|
|
||||||
|
b.HasIndex("ParentId");
|
||||||
|
|
||||||
|
b.ToTable("categories", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("ERPCore.Domain.Entities.Item", b =>
|
||||||
|
{
|
||||||
|
b.Property<long>("ItemId")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("bigint");
|
||||||
|
|
||||||
|
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("ItemId"));
|
||||||
|
|
||||||
|
b.Property<long>("BaseUomId")
|
||||||
|
.HasColumnType("bigint");
|
||||||
|
|
||||||
|
b.Property<long>("CategoryId")
|
||||||
|
.HasColumnType("bigint");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<long?>("DefaultVendorId")
|
||||||
|
.HasColumnType("bigint");
|
||||||
|
|
||||||
|
b.Property<string>("Description")
|
||||||
|
.HasMaxLength(1000)
|
||||||
|
.HasColumnType("character varying(1000)");
|
||||||
|
|
||||||
|
b.Property<string>("ItemType")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(20)
|
||||||
|
.HasColumnType("character varying(20)");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(200)
|
||||||
|
.HasColumnType("character varying(200)");
|
||||||
|
|
||||||
|
b.Property<uint>("RowVersion")
|
||||||
|
.IsConcurrencyToken()
|
||||||
|
.ValueGeneratedOnAddOrUpdate()
|
||||||
|
.HasColumnType("xid")
|
||||||
|
.HasColumnName("xmin");
|
||||||
|
|
||||||
|
b.Property<string>("Sku")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(50)
|
||||||
|
.HasColumnType("character varying(50)");
|
||||||
|
|
||||||
|
b.Property<string>("Status")
|
||||||
|
.IsRequired()
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasMaxLength(20)
|
||||||
|
.HasColumnType("character varying(20)")
|
||||||
|
.HasDefaultValue("Active");
|
||||||
|
|
||||||
|
b.Property<string>("TaxClass")
|
||||||
|
.HasMaxLength(20)
|
||||||
|
.HasColumnType("character varying(20)");
|
||||||
|
|
||||||
|
b.Property<string>("TrackingMode")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(20)
|
||||||
|
.HasColumnType("character varying(20)");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("UpdatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.HasKey("ItemId");
|
||||||
|
|
||||||
|
b.HasIndex("BaseUomId");
|
||||||
|
|
||||||
|
b.HasIndex("CategoryId");
|
||||||
|
|
||||||
|
b.HasIndex("DefaultVendorId");
|
||||||
|
|
||||||
|
b.HasIndex("Sku")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.HasIndex("Status");
|
||||||
|
|
||||||
|
b.ToTable("items", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("ERPCore.Domain.Entities.ItemReorder", b =>
|
||||||
|
{
|
||||||
|
b.Property<long>("ReorderId")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("bigint");
|
||||||
|
|
||||||
|
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("ReorderId"));
|
||||||
|
|
||||||
|
b.Property<long>("ItemId")
|
||||||
|
.HasColumnType("bigint");
|
||||||
|
|
||||||
|
b.Property<decimal>("ReorderPoint")
|
||||||
|
.HasPrecision(18, 4)
|
||||||
|
.HasColumnType("numeric(18,4)");
|
||||||
|
|
||||||
|
b.Property<decimal>("ReorderQty")
|
||||||
|
.HasPrecision(18, 4)
|
||||||
|
.HasColumnType("numeric(18,4)");
|
||||||
|
|
||||||
|
b.Property<long>("WarehouseId")
|
||||||
|
.HasColumnType("bigint");
|
||||||
|
|
||||||
|
b.HasKey("ReorderId");
|
||||||
|
|
||||||
|
b.HasIndex("WarehouseId");
|
||||||
|
|
||||||
|
b.HasIndex("ItemId", "WarehouseId")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("item_reorders", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("ERPCore.Domain.Entities.Uom", b =>
|
||||||
|
{
|
||||||
|
b.Property<long>("UomId")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("bigint");
|
||||||
|
|
||||||
|
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("UomId"));
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(50)
|
||||||
|
.HasColumnType("character varying(50)");
|
||||||
|
|
||||||
|
b.HasKey("UomId");
|
||||||
|
|
||||||
|
b.HasIndex("Name")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("uoms", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("ERPCore.Domain.Entities.UomConversion", b =>
|
||||||
|
{
|
||||||
|
b.Property<long>("ConversionId")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("bigint");
|
||||||
|
|
||||||
|
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("ConversionId"));
|
||||||
|
|
||||||
|
b.Property<decimal>("Factor")
|
||||||
|
.HasPrecision(18, 6)
|
||||||
|
.HasColumnType("numeric(18,6)");
|
||||||
|
|
||||||
|
b.Property<long>("FromUomId")
|
||||||
|
.HasColumnType("bigint");
|
||||||
|
|
||||||
|
b.Property<long>("ItemId")
|
||||||
|
.HasColumnType("bigint");
|
||||||
|
|
||||||
|
b.Property<long>("ToUomId")
|
||||||
|
.HasColumnType("bigint");
|
||||||
|
|
||||||
|
b.HasKey("ConversionId");
|
||||||
|
|
||||||
|
b.HasIndex("FromUomId");
|
||||||
|
|
||||||
|
b.HasIndex("ToUomId");
|
||||||
|
|
||||||
|
b.HasIndex("ItemId", "FromUomId", "ToUomId")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("uom_conversions", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("ERPCore.Domain.Entities.Vendor", b =>
|
||||||
|
{
|
||||||
|
b.Property<long>("VendorId")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("bigint");
|
||||||
|
|
||||||
|
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("VendorId"));
|
||||||
|
|
||||||
|
b.Property<string>("Code")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(50)
|
||||||
|
.HasColumnType("character varying(50)");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<string>("Currency")
|
||||||
|
.IsRequired()
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasMaxLength(3)
|
||||||
|
.HasColumnType("character varying(3)")
|
||||||
|
.HasDefaultValue("LKR");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(200)
|
||||||
|
.HasColumnType("character varying(200)");
|
||||||
|
|
||||||
|
b.Property<uint>("RowVersion")
|
||||||
|
.IsConcurrencyToken()
|
||||||
|
.ValueGeneratedOnAddOrUpdate()
|
||||||
|
.HasColumnType("xid")
|
||||||
|
.HasColumnName("xmin");
|
||||||
|
|
||||||
|
b.Property<string>("Status")
|
||||||
|
.IsRequired()
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasMaxLength(20)
|
||||||
|
.HasColumnType("character varying(20)")
|
||||||
|
.HasDefaultValue("Active");
|
||||||
|
|
||||||
|
b.Property<string>("TaxReg")
|
||||||
|
.HasMaxLength(50)
|
||||||
|
.HasColumnType("character varying(50)");
|
||||||
|
|
||||||
|
b.Property<string>("Terms")
|
||||||
|
.HasMaxLength(50)
|
||||||
|
.HasColumnType("character varying(50)");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("UpdatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.HasKey("VendorId");
|
||||||
|
|
||||||
|
b.HasIndex("Code")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.HasIndex("Status");
|
||||||
|
|
||||||
|
b.ToTable("vendors", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("ERPCore.Domain.Entities.Warehouse", b =>
|
||||||
|
{
|
||||||
|
b.Property<long>("WarehouseId")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("bigint");
|
||||||
|
|
||||||
|
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("WarehouseId"));
|
||||||
|
|
||||||
|
b.Property<string>("Code")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(50)
|
||||||
|
.HasColumnType("character varying(50)");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(200)
|
||||||
|
.HasColumnType("character varying(200)");
|
||||||
|
|
||||||
|
b.HasKey("WarehouseId");
|
||||||
|
|
||||||
|
b.HasIndex("Code")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("warehouses", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("ERPCore.Domain.Entities.Bin", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("ERPCore.Domain.Entities.Warehouse", "Warehouse")
|
||||||
|
.WithMany("Bins")
|
||||||
|
.HasForeignKey("WarehouseId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Warehouse");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("ERPCore.Domain.Entities.Category", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("ERPCore.Domain.Entities.Category", "Parent")
|
||||||
|
.WithMany("Children")
|
||||||
|
.HasForeignKey("ParentId")
|
||||||
|
.OnDelete(DeleteBehavior.Restrict);
|
||||||
|
|
||||||
|
b.Navigation("Parent");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("ERPCore.Domain.Entities.Item", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("ERPCore.Domain.Entities.Uom", "BaseUom")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("BaseUomId")
|
||||||
|
.OnDelete(DeleteBehavior.Restrict)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasOne("ERPCore.Domain.Entities.Category", "Category")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("CategoryId")
|
||||||
|
.OnDelete(DeleteBehavior.Restrict)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasOne("ERPCore.Domain.Entities.Vendor", "DefaultVendor")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("DefaultVendorId")
|
||||||
|
.OnDelete(DeleteBehavior.Restrict);
|
||||||
|
|
||||||
|
b.Navigation("BaseUom");
|
||||||
|
|
||||||
|
b.Navigation("Category");
|
||||||
|
|
||||||
|
b.Navigation("DefaultVendor");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("ERPCore.Domain.Entities.ItemReorder", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("ERPCore.Domain.Entities.Item", "Item")
|
||||||
|
.WithMany("ReorderSettings")
|
||||||
|
.HasForeignKey("ItemId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasOne("ERPCore.Domain.Entities.Warehouse", "Warehouse")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("WarehouseId")
|
||||||
|
.OnDelete(DeleteBehavior.Restrict)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Item");
|
||||||
|
|
||||||
|
b.Navigation("Warehouse");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("ERPCore.Domain.Entities.UomConversion", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("ERPCore.Domain.Entities.Uom", "FromUom")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("FromUomId")
|
||||||
|
.OnDelete(DeleteBehavior.Restrict)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasOne("ERPCore.Domain.Entities.Item", "Item")
|
||||||
|
.WithMany("UomConversions")
|
||||||
|
.HasForeignKey("ItemId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasOne("ERPCore.Domain.Entities.Uom", "ToUom")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("ToUomId")
|
||||||
|
.OnDelete(DeleteBehavior.Restrict)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("FromUom");
|
||||||
|
|
||||||
|
b.Navigation("Item");
|
||||||
|
|
||||||
|
b.Navigation("ToUom");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("ERPCore.Domain.Entities.Category", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("Children");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("ERPCore.Domain.Entities.Item", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("ReorderSettings");
|
||||||
|
|
||||||
|
b.Navigation("UomConversions");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("ERPCore.Domain.Entities.Warehouse", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("Bins");
|
||||||
|
});
|
||||||
|
#pragma warning restore 612, 618
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,325 @@
|
|||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace ERPCore.Infra.Persistence.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class InitialCreate : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "categories",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
CategoryId = table.Column<long>(type: "bigint", nullable: false)
|
||||||
|
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||||
|
Name = table.Column<string>(type: "character varying(200)", maxLength: 200, nullable: false),
|
||||||
|
ParentId = table.Column<long>(type: "bigint", nullable: true)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_categories", x => x.CategoryId);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_categories_categories_ParentId",
|
||||||
|
column: x => x.ParentId,
|
||||||
|
principalTable: "categories",
|
||||||
|
principalColumn: "CategoryId",
|
||||||
|
onDelete: ReferentialAction.Restrict);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "uoms",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
UomId = table.Column<long>(type: "bigint", nullable: false)
|
||||||
|
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||||
|
Name = table.Column<string>(type: "character varying(50)", maxLength: 50, nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_uoms", x => x.UomId);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "vendors",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
VendorId = table.Column<long>(type: "bigint", nullable: false)
|
||||||
|
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||||
|
Code = table.Column<string>(type: "character varying(50)", maxLength: 50, nullable: false),
|
||||||
|
Name = table.Column<string>(type: "character varying(200)", maxLength: 200, nullable: false),
|
||||||
|
Terms = table.Column<string>(type: "character varying(50)", maxLength: 50, nullable: true),
|
||||||
|
TaxReg = table.Column<string>(type: "character varying(50)", maxLength: 50, nullable: true),
|
||||||
|
Currency = table.Column<string>(type: "character varying(3)", maxLength: 3, nullable: false, defaultValue: "LKR"),
|
||||||
|
Status = table.Column<string>(type: "character varying(20)", maxLength: 20, nullable: false, defaultValue: "Active"),
|
||||||
|
CreatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
|
||||||
|
UpdatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: true),
|
||||||
|
xmin = table.Column<uint>(type: "xid", rowVersion: true, nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_vendors", x => x.VendorId);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "warehouses",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
WarehouseId = table.Column<long>(type: "bigint", nullable: false)
|
||||||
|
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||||
|
Code = table.Column<string>(type: "character varying(50)", maxLength: 50, nullable: false),
|
||||||
|
Name = table.Column<string>(type: "character varying(200)", maxLength: 200, nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_warehouses", x => x.WarehouseId);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "items",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
ItemId = table.Column<long>(type: "bigint", nullable: false)
|
||||||
|
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||||
|
Sku = table.Column<string>(type: "character varying(50)", maxLength: 50, nullable: false),
|
||||||
|
Name = table.Column<string>(type: "character varying(200)", maxLength: 200, nullable: false),
|
||||||
|
Description = table.Column<string>(type: "character varying(1000)", maxLength: 1000, nullable: true),
|
||||||
|
CategoryId = table.Column<long>(type: "bigint", nullable: false),
|
||||||
|
BaseUomId = table.Column<long>(type: "bigint", nullable: false),
|
||||||
|
DefaultVendorId = table.Column<long>(type: "bigint", nullable: true),
|
||||||
|
ItemType = table.Column<string>(type: "character varying(20)", maxLength: 20, nullable: false),
|
||||||
|
TrackingMode = table.Column<string>(type: "character varying(20)", maxLength: 20, nullable: false),
|
||||||
|
TaxClass = table.Column<string>(type: "character varying(20)", maxLength: 20, nullable: true),
|
||||||
|
Status = table.Column<string>(type: "character varying(20)", maxLength: 20, nullable: false, defaultValue: "Active"),
|
||||||
|
CreatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
|
||||||
|
UpdatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: true),
|
||||||
|
xmin = table.Column<uint>(type: "xid", rowVersion: true, nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_items", x => x.ItemId);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_items_categories_CategoryId",
|
||||||
|
column: x => x.CategoryId,
|
||||||
|
principalTable: "categories",
|
||||||
|
principalColumn: "CategoryId",
|
||||||
|
onDelete: ReferentialAction.Restrict);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_items_uoms_BaseUomId",
|
||||||
|
column: x => x.BaseUomId,
|
||||||
|
principalTable: "uoms",
|
||||||
|
principalColumn: "UomId",
|
||||||
|
onDelete: ReferentialAction.Restrict);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_items_vendors_DefaultVendorId",
|
||||||
|
column: x => x.DefaultVendorId,
|
||||||
|
principalTable: "vendors",
|
||||||
|
principalColumn: "VendorId",
|
||||||
|
onDelete: ReferentialAction.Restrict);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "bins",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
BinId = table.Column<long>(type: "bigint", nullable: false)
|
||||||
|
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||||
|
WarehouseId = table.Column<long>(type: "bigint", nullable: false),
|
||||||
|
Code = table.Column<string>(type: "character varying(50)", maxLength: 50, nullable: false),
|
||||||
|
BinType = table.Column<string>(type: "character varying(50)", maxLength: 50, nullable: true)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_bins", x => x.BinId);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_bins_warehouses_WarehouseId",
|
||||||
|
column: x => x.WarehouseId,
|
||||||
|
principalTable: "warehouses",
|
||||||
|
principalColumn: "WarehouseId",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "item_reorders",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
ReorderId = table.Column<long>(type: "bigint", nullable: false)
|
||||||
|
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||||
|
ItemId = table.Column<long>(type: "bigint", nullable: false),
|
||||||
|
WarehouseId = table.Column<long>(type: "bigint", nullable: false),
|
||||||
|
ReorderPoint = table.Column<decimal>(type: "numeric(18,4)", precision: 18, scale: 4, nullable: false),
|
||||||
|
ReorderQty = table.Column<decimal>(type: "numeric(18,4)", precision: 18, scale: 4, nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_item_reorders", x => x.ReorderId);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_item_reorders_items_ItemId",
|
||||||
|
column: x => x.ItemId,
|
||||||
|
principalTable: "items",
|
||||||
|
principalColumn: "ItemId",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_item_reorders_warehouses_WarehouseId",
|
||||||
|
column: x => x.WarehouseId,
|
||||||
|
principalTable: "warehouses",
|
||||||
|
principalColumn: "WarehouseId",
|
||||||
|
onDelete: ReferentialAction.Restrict);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "uom_conversions",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
ConversionId = table.Column<long>(type: "bigint", nullable: false)
|
||||||
|
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||||
|
ItemId = table.Column<long>(type: "bigint", nullable: false),
|
||||||
|
FromUomId = table.Column<long>(type: "bigint", nullable: false),
|
||||||
|
ToUomId = table.Column<long>(type: "bigint", nullable: false),
|
||||||
|
Factor = table.Column<decimal>(type: "numeric(18,6)", precision: 18, scale: 6, nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_uom_conversions", x => x.ConversionId);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_uom_conversions_items_ItemId",
|
||||||
|
column: x => x.ItemId,
|
||||||
|
principalTable: "items",
|
||||||
|
principalColumn: "ItemId",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_uom_conversions_uoms_FromUomId",
|
||||||
|
column: x => x.FromUomId,
|
||||||
|
principalTable: "uoms",
|
||||||
|
principalColumn: "UomId",
|
||||||
|
onDelete: ReferentialAction.Restrict);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_uom_conversions_uoms_ToUomId",
|
||||||
|
column: x => x.ToUomId,
|
||||||
|
principalTable: "uoms",
|
||||||
|
principalColumn: "UomId",
|
||||||
|
onDelete: ReferentialAction.Restrict);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_bins_WarehouseId_Code",
|
||||||
|
table: "bins",
|
||||||
|
columns: new[] { "WarehouseId", "Code" },
|
||||||
|
unique: true);
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_categories_ParentId",
|
||||||
|
table: "categories",
|
||||||
|
column: "ParentId");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_item_reorders_ItemId_WarehouseId",
|
||||||
|
table: "item_reorders",
|
||||||
|
columns: new[] { "ItemId", "WarehouseId" },
|
||||||
|
unique: true);
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_item_reorders_WarehouseId",
|
||||||
|
table: "item_reorders",
|
||||||
|
column: "WarehouseId");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_items_BaseUomId",
|
||||||
|
table: "items",
|
||||||
|
column: "BaseUomId");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_items_CategoryId",
|
||||||
|
table: "items",
|
||||||
|
column: "CategoryId");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_items_DefaultVendorId",
|
||||||
|
table: "items",
|
||||||
|
column: "DefaultVendorId");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_items_Sku",
|
||||||
|
table: "items",
|
||||||
|
column: "Sku",
|
||||||
|
unique: true);
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_items_Status",
|
||||||
|
table: "items",
|
||||||
|
column: "Status");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_uom_conversions_FromUomId",
|
||||||
|
table: "uom_conversions",
|
||||||
|
column: "FromUomId");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_uom_conversions_ItemId_FromUomId_ToUomId",
|
||||||
|
table: "uom_conversions",
|
||||||
|
columns: new[] { "ItemId", "FromUomId", "ToUomId" },
|
||||||
|
unique: true);
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_uom_conversions_ToUomId",
|
||||||
|
table: "uom_conversions",
|
||||||
|
column: "ToUomId");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_uoms_Name",
|
||||||
|
table: "uoms",
|
||||||
|
column: "Name",
|
||||||
|
unique: true);
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_vendors_Code",
|
||||||
|
table: "vendors",
|
||||||
|
column: "Code",
|
||||||
|
unique: true);
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_vendors_Status",
|
||||||
|
table: "vendors",
|
||||||
|
column: "Status");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_warehouses_Code",
|
||||||
|
table: "warehouses",
|
||||||
|
column: "Code",
|
||||||
|
unique: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "bins");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "item_reorders");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "uom_conversions");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "warehouses");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "items");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "categories");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "uoms");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "vendors");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+445
@@ -0,0 +1,445 @@
|
|||||||
|
// <auto-generated />
|
||||||
|
using System;
|
||||||
|
using ERPCore.Infra.Persistence;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||||
|
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace ERPCore.Infra.Persistence.Migrations
|
||||||
|
{
|
||||||
|
[DbContext(typeof(ErpDbContext))]
|
||||||
|
[Migration("20260709124415_initial")]
|
||||||
|
partial class initial
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||||
|
{
|
||||||
|
#pragma warning disable 612, 618
|
||||||
|
modelBuilder
|
||||||
|
.HasAnnotation("ProductVersion", "10.0.9")
|
||||||
|
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||||
|
|
||||||
|
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||||
|
|
||||||
|
modelBuilder.Entity("ERPCore.Domain.Entities.Bin", b =>
|
||||||
|
{
|
||||||
|
b.Property<long>("BinId")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("bigint");
|
||||||
|
|
||||||
|
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("BinId"));
|
||||||
|
|
||||||
|
b.Property<string>("BinType")
|
||||||
|
.HasMaxLength(50)
|
||||||
|
.HasColumnType("character varying(50)");
|
||||||
|
|
||||||
|
b.Property<string>("Code")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(50)
|
||||||
|
.HasColumnType("character varying(50)");
|
||||||
|
|
||||||
|
b.Property<long>("WarehouseId")
|
||||||
|
.HasColumnType("bigint");
|
||||||
|
|
||||||
|
b.HasKey("BinId");
|
||||||
|
|
||||||
|
b.HasIndex("WarehouseId", "Code")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("bins", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("ERPCore.Domain.Entities.Category", b =>
|
||||||
|
{
|
||||||
|
b.Property<long>("CategoryId")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("bigint");
|
||||||
|
|
||||||
|
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("CategoryId"));
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(200)
|
||||||
|
.HasColumnType("character varying(200)");
|
||||||
|
|
||||||
|
b.Property<long?>("ParentId")
|
||||||
|
.HasColumnType("bigint");
|
||||||
|
|
||||||
|
b.HasKey("CategoryId");
|
||||||
|
|
||||||
|
b.HasIndex("ParentId");
|
||||||
|
|
||||||
|
b.ToTable("categories", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("ERPCore.Domain.Entities.Item", b =>
|
||||||
|
{
|
||||||
|
b.Property<long>("ItemId")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("bigint");
|
||||||
|
|
||||||
|
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("ItemId"));
|
||||||
|
|
||||||
|
b.Property<long>("BaseUomId")
|
||||||
|
.HasColumnType("bigint");
|
||||||
|
|
||||||
|
b.Property<long>("CategoryId")
|
||||||
|
.HasColumnType("bigint");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<long?>("DefaultVendorId")
|
||||||
|
.HasColumnType("bigint");
|
||||||
|
|
||||||
|
b.Property<string>("Description")
|
||||||
|
.HasMaxLength(1000)
|
||||||
|
.HasColumnType("character varying(1000)");
|
||||||
|
|
||||||
|
b.Property<string>("ItemType")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(20)
|
||||||
|
.HasColumnType("character varying(20)");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(200)
|
||||||
|
.HasColumnType("character varying(200)");
|
||||||
|
|
||||||
|
b.Property<uint>("RowVersion")
|
||||||
|
.IsConcurrencyToken()
|
||||||
|
.ValueGeneratedOnAddOrUpdate()
|
||||||
|
.HasColumnType("xid")
|
||||||
|
.HasColumnName("xmin");
|
||||||
|
|
||||||
|
b.Property<string>("Sku")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(50)
|
||||||
|
.HasColumnType("character varying(50)");
|
||||||
|
|
||||||
|
b.Property<string>("Status")
|
||||||
|
.IsRequired()
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasMaxLength(20)
|
||||||
|
.HasColumnType("character varying(20)")
|
||||||
|
.HasDefaultValue("Active");
|
||||||
|
|
||||||
|
b.Property<string>("TaxClass")
|
||||||
|
.HasMaxLength(20)
|
||||||
|
.HasColumnType("character varying(20)");
|
||||||
|
|
||||||
|
b.Property<string>("TrackingMode")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(20)
|
||||||
|
.HasColumnType("character varying(20)");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("UpdatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.HasKey("ItemId");
|
||||||
|
|
||||||
|
b.HasIndex("BaseUomId");
|
||||||
|
|
||||||
|
b.HasIndex("CategoryId");
|
||||||
|
|
||||||
|
b.HasIndex("DefaultVendorId");
|
||||||
|
|
||||||
|
b.HasIndex("Sku")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.HasIndex("Status");
|
||||||
|
|
||||||
|
b.ToTable("items", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("ERPCore.Domain.Entities.ItemReorder", b =>
|
||||||
|
{
|
||||||
|
b.Property<long>("ReorderId")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("bigint");
|
||||||
|
|
||||||
|
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("ReorderId"));
|
||||||
|
|
||||||
|
b.Property<long>("ItemId")
|
||||||
|
.HasColumnType("bigint");
|
||||||
|
|
||||||
|
b.Property<decimal>("ReorderPoint")
|
||||||
|
.HasPrecision(18, 4)
|
||||||
|
.HasColumnType("numeric(18,4)");
|
||||||
|
|
||||||
|
b.Property<decimal>("ReorderQty")
|
||||||
|
.HasPrecision(18, 4)
|
||||||
|
.HasColumnType("numeric(18,4)");
|
||||||
|
|
||||||
|
b.Property<long>("WarehouseId")
|
||||||
|
.HasColumnType("bigint");
|
||||||
|
|
||||||
|
b.HasKey("ReorderId");
|
||||||
|
|
||||||
|
b.HasIndex("WarehouseId");
|
||||||
|
|
||||||
|
b.HasIndex("ItemId", "WarehouseId")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("item_reorders", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("ERPCore.Domain.Entities.Uom", b =>
|
||||||
|
{
|
||||||
|
b.Property<long>("UomId")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("bigint");
|
||||||
|
|
||||||
|
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("UomId"));
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(50)
|
||||||
|
.HasColumnType("character varying(50)");
|
||||||
|
|
||||||
|
b.HasKey("UomId");
|
||||||
|
|
||||||
|
b.HasIndex("Name")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("uoms", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("ERPCore.Domain.Entities.UomConversion", b =>
|
||||||
|
{
|
||||||
|
b.Property<long>("ConversionId")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("bigint");
|
||||||
|
|
||||||
|
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("ConversionId"));
|
||||||
|
|
||||||
|
b.Property<decimal>("Factor")
|
||||||
|
.HasPrecision(18, 6)
|
||||||
|
.HasColumnType("numeric(18,6)");
|
||||||
|
|
||||||
|
b.Property<long>("FromUomId")
|
||||||
|
.HasColumnType("bigint");
|
||||||
|
|
||||||
|
b.Property<long>("ItemId")
|
||||||
|
.HasColumnType("bigint");
|
||||||
|
|
||||||
|
b.Property<long>("ToUomId")
|
||||||
|
.HasColumnType("bigint");
|
||||||
|
|
||||||
|
b.HasKey("ConversionId");
|
||||||
|
|
||||||
|
b.HasIndex("FromUomId");
|
||||||
|
|
||||||
|
b.HasIndex("ToUomId");
|
||||||
|
|
||||||
|
b.HasIndex("ItemId", "FromUomId", "ToUomId")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("uom_conversions", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("ERPCore.Domain.Entities.Vendor", b =>
|
||||||
|
{
|
||||||
|
b.Property<long>("VendorId")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("bigint");
|
||||||
|
|
||||||
|
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("VendorId"));
|
||||||
|
|
||||||
|
b.Property<string>("Code")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(50)
|
||||||
|
.HasColumnType("character varying(50)");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<string>("Currency")
|
||||||
|
.IsRequired()
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasMaxLength(3)
|
||||||
|
.HasColumnType("character varying(3)")
|
||||||
|
.HasDefaultValue("LKR");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(200)
|
||||||
|
.HasColumnType("character varying(200)");
|
||||||
|
|
||||||
|
b.Property<uint>("RowVersion")
|
||||||
|
.IsConcurrencyToken()
|
||||||
|
.ValueGeneratedOnAddOrUpdate()
|
||||||
|
.HasColumnType("xid")
|
||||||
|
.HasColumnName("xmin");
|
||||||
|
|
||||||
|
b.Property<string>("Status")
|
||||||
|
.IsRequired()
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasMaxLength(20)
|
||||||
|
.HasColumnType("character varying(20)")
|
||||||
|
.HasDefaultValue("Active");
|
||||||
|
|
||||||
|
b.Property<string>("TaxReg")
|
||||||
|
.HasMaxLength(50)
|
||||||
|
.HasColumnType("character varying(50)");
|
||||||
|
|
||||||
|
b.Property<string>("Terms")
|
||||||
|
.HasMaxLength(50)
|
||||||
|
.HasColumnType("character varying(50)");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("UpdatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.HasKey("VendorId");
|
||||||
|
|
||||||
|
b.HasIndex("Code")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.HasIndex("Status");
|
||||||
|
|
||||||
|
b.ToTable("vendors", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("ERPCore.Domain.Entities.Warehouse", b =>
|
||||||
|
{
|
||||||
|
b.Property<long>("WarehouseId")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("bigint");
|
||||||
|
|
||||||
|
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("WarehouseId"));
|
||||||
|
|
||||||
|
b.Property<string>("Code")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(50)
|
||||||
|
.HasColumnType("character varying(50)");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(200)
|
||||||
|
.HasColumnType("character varying(200)");
|
||||||
|
|
||||||
|
b.HasKey("WarehouseId");
|
||||||
|
|
||||||
|
b.HasIndex("Code")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("warehouses", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("ERPCore.Domain.Entities.Bin", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("ERPCore.Domain.Entities.Warehouse", "Warehouse")
|
||||||
|
.WithMany("Bins")
|
||||||
|
.HasForeignKey("WarehouseId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Warehouse");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("ERPCore.Domain.Entities.Category", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("ERPCore.Domain.Entities.Category", "Parent")
|
||||||
|
.WithMany("Children")
|
||||||
|
.HasForeignKey("ParentId")
|
||||||
|
.OnDelete(DeleteBehavior.Restrict);
|
||||||
|
|
||||||
|
b.Navigation("Parent");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("ERPCore.Domain.Entities.Item", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("ERPCore.Domain.Entities.Uom", "BaseUom")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("BaseUomId")
|
||||||
|
.OnDelete(DeleteBehavior.Restrict)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasOne("ERPCore.Domain.Entities.Category", "Category")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("CategoryId")
|
||||||
|
.OnDelete(DeleteBehavior.Restrict)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasOne("ERPCore.Domain.Entities.Vendor", "DefaultVendor")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("DefaultVendorId")
|
||||||
|
.OnDelete(DeleteBehavior.Restrict);
|
||||||
|
|
||||||
|
b.Navigation("BaseUom");
|
||||||
|
|
||||||
|
b.Navigation("Category");
|
||||||
|
|
||||||
|
b.Navigation("DefaultVendor");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("ERPCore.Domain.Entities.ItemReorder", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("ERPCore.Domain.Entities.Item", "Item")
|
||||||
|
.WithMany("ReorderSettings")
|
||||||
|
.HasForeignKey("ItemId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasOne("ERPCore.Domain.Entities.Warehouse", "Warehouse")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("WarehouseId")
|
||||||
|
.OnDelete(DeleteBehavior.Restrict)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Item");
|
||||||
|
|
||||||
|
b.Navigation("Warehouse");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("ERPCore.Domain.Entities.UomConversion", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("ERPCore.Domain.Entities.Uom", "FromUom")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("FromUomId")
|
||||||
|
.OnDelete(DeleteBehavior.Restrict)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasOne("ERPCore.Domain.Entities.Item", "Item")
|
||||||
|
.WithMany("UomConversions")
|
||||||
|
.HasForeignKey("ItemId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasOne("ERPCore.Domain.Entities.Uom", "ToUom")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("ToUomId")
|
||||||
|
.OnDelete(DeleteBehavior.Restrict)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("FromUom");
|
||||||
|
|
||||||
|
b.Navigation("Item");
|
||||||
|
|
||||||
|
b.Navigation("ToUom");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("ERPCore.Domain.Entities.Category", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("Children");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("ERPCore.Domain.Entities.Item", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("ReorderSettings");
|
||||||
|
|
||||||
|
b.Navigation("UomConversions");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("ERPCore.Domain.Entities.Warehouse", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("Bins");
|
||||||
|
});
|
||||||
|
#pragma warning restore 612, 618
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace ERPCore.Infra.Persistence.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class initial : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,442 @@
|
|||||||
|
// <auto-generated />
|
||||||
|
using System;
|
||||||
|
using ERPCore.Infra.Persistence;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||||
|
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace ERPCore.Infra.Persistence.Migrations
|
||||||
|
{
|
||||||
|
[DbContext(typeof(ErpDbContext))]
|
||||||
|
partial class ErpDbContextModelSnapshot : ModelSnapshot
|
||||||
|
{
|
||||||
|
protected override void BuildModel(ModelBuilder modelBuilder)
|
||||||
|
{
|
||||||
|
#pragma warning disable 612, 618
|
||||||
|
modelBuilder
|
||||||
|
.HasAnnotation("ProductVersion", "10.0.9")
|
||||||
|
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||||
|
|
||||||
|
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||||
|
|
||||||
|
modelBuilder.Entity("ERPCore.Domain.Entities.Bin", b =>
|
||||||
|
{
|
||||||
|
b.Property<long>("BinId")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("bigint");
|
||||||
|
|
||||||
|
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("BinId"));
|
||||||
|
|
||||||
|
b.Property<string>("BinType")
|
||||||
|
.HasMaxLength(50)
|
||||||
|
.HasColumnType("character varying(50)");
|
||||||
|
|
||||||
|
b.Property<string>("Code")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(50)
|
||||||
|
.HasColumnType("character varying(50)");
|
||||||
|
|
||||||
|
b.Property<long>("WarehouseId")
|
||||||
|
.HasColumnType("bigint");
|
||||||
|
|
||||||
|
b.HasKey("BinId");
|
||||||
|
|
||||||
|
b.HasIndex("WarehouseId", "Code")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("bins", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("ERPCore.Domain.Entities.Category", b =>
|
||||||
|
{
|
||||||
|
b.Property<long>("CategoryId")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("bigint");
|
||||||
|
|
||||||
|
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("CategoryId"));
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(200)
|
||||||
|
.HasColumnType("character varying(200)");
|
||||||
|
|
||||||
|
b.Property<long?>("ParentId")
|
||||||
|
.HasColumnType("bigint");
|
||||||
|
|
||||||
|
b.HasKey("CategoryId");
|
||||||
|
|
||||||
|
b.HasIndex("ParentId");
|
||||||
|
|
||||||
|
b.ToTable("categories", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("ERPCore.Domain.Entities.Item", b =>
|
||||||
|
{
|
||||||
|
b.Property<long>("ItemId")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("bigint");
|
||||||
|
|
||||||
|
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("ItemId"));
|
||||||
|
|
||||||
|
b.Property<long>("BaseUomId")
|
||||||
|
.HasColumnType("bigint");
|
||||||
|
|
||||||
|
b.Property<long>("CategoryId")
|
||||||
|
.HasColumnType("bigint");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<long?>("DefaultVendorId")
|
||||||
|
.HasColumnType("bigint");
|
||||||
|
|
||||||
|
b.Property<string>("Description")
|
||||||
|
.HasMaxLength(1000)
|
||||||
|
.HasColumnType("character varying(1000)");
|
||||||
|
|
||||||
|
b.Property<string>("ItemType")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(20)
|
||||||
|
.HasColumnType("character varying(20)");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(200)
|
||||||
|
.HasColumnType("character varying(200)");
|
||||||
|
|
||||||
|
b.Property<uint>("RowVersion")
|
||||||
|
.IsConcurrencyToken()
|
||||||
|
.ValueGeneratedOnAddOrUpdate()
|
||||||
|
.HasColumnType("xid")
|
||||||
|
.HasColumnName("xmin");
|
||||||
|
|
||||||
|
b.Property<string>("Sku")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(50)
|
||||||
|
.HasColumnType("character varying(50)");
|
||||||
|
|
||||||
|
b.Property<string>("Status")
|
||||||
|
.IsRequired()
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasMaxLength(20)
|
||||||
|
.HasColumnType("character varying(20)")
|
||||||
|
.HasDefaultValue("Active");
|
||||||
|
|
||||||
|
b.Property<string>("TaxClass")
|
||||||
|
.HasMaxLength(20)
|
||||||
|
.HasColumnType("character varying(20)");
|
||||||
|
|
||||||
|
b.Property<string>("TrackingMode")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(20)
|
||||||
|
.HasColumnType("character varying(20)");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("UpdatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.HasKey("ItemId");
|
||||||
|
|
||||||
|
b.HasIndex("BaseUomId");
|
||||||
|
|
||||||
|
b.HasIndex("CategoryId");
|
||||||
|
|
||||||
|
b.HasIndex("DefaultVendorId");
|
||||||
|
|
||||||
|
b.HasIndex("Sku")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.HasIndex("Status");
|
||||||
|
|
||||||
|
b.ToTable("items", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("ERPCore.Domain.Entities.ItemReorder", b =>
|
||||||
|
{
|
||||||
|
b.Property<long>("ReorderId")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("bigint");
|
||||||
|
|
||||||
|
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("ReorderId"));
|
||||||
|
|
||||||
|
b.Property<long>("ItemId")
|
||||||
|
.HasColumnType("bigint");
|
||||||
|
|
||||||
|
b.Property<decimal>("ReorderPoint")
|
||||||
|
.HasPrecision(18, 4)
|
||||||
|
.HasColumnType("numeric(18,4)");
|
||||||
|
|
||||||
|
b.Property<decimal>("ReorderQty")
|
||||||
|
.HasPrecision(18, 4)
|
||||||
|
.HasColumnType("numeric(18,4)");
|
||||||
|
|
||||||
|
b.Property<long>("WarehouseId")
|
||||||
|
.HasColumnType("bigint");
|
||||||
|
|
||||||
|
b.HasKey("ReorderId");
|
||||||
|
|
||||||
|
b.HasIndex("WarehouseId");
|
||||||
|
|
||||||
|
b.HasIndex("ItemId", "WarehouseId")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("item_reorders", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("ERPCore.Domain.Entities.Uom", b =>
|
||||||
|
{
|
||||||
|
b.Property<long>("UomId")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("bigint");
|
||||||
|
|
||||||
|
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("UomId"));
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(50)
|
||||||
|
.HasColumnType("character varying(50)");
|
||||||
|
|
||||||
|
b.HasKey("UomId");
|
||||||
|
|
||||||
|
b.HasIndex("Name")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("uoms", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("ERPCore.Domain.Entities.UomConversion", b =>
|
||||||
|
{
|
||||||
|
b.Property<long>("ConversionId")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("bigint");
|
||||||
|
|
||||||
|
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("ConversionId"));
|
||||||
|
|
||||||
|
b.Property<decimal>("Factor")
|
||||||
|
.HasPrecision(18, 6)
|
||||||
|
.HasColumnType("numeric(18,6)");
|
||||||
|
|
||||||
|
b.Property<long>("FromUomId")
|
||||||
|
.HasColumnType("bigint");
|
||||||
|
|
||||||
|
b.Property<long>("ItemId")
|
||||||
|
.HasColumnType("bigint");
|
||||||
|
|
||||||
|
b.Property<long>("ToUomId")
|
||||||
|
.HasColumnType("bigint");
|
||||||
|
|
||||||
|
b.HasKey("ConversionId");
|
||||||
|
|
||||||
|
b.HasIndex("FromUomId");
|
||||||
|
|
||||||
|
b.HasIndex("ToUomId");
|
||||||
|
|
||||||
|
b.HasIndex("ItemId", "FromUomId", "ToUomId")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("uom_conversions", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("ERPCore.Domain.Entities.Vendor", b =>
|
||||||
|
{
|
||||||
|
b.Property<long>("VendorId")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("bigint");
|
||||||
|
|
||||||
|
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("VendorId"));
|
||||||
|
|
||||||
|
b.Property<string>("Code")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(50)
|
||||||
|
.HasColumnType("character varying(50)");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<string>("Currency")
|
||||||
|
.IsRequired()
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasMaxLength(3)
|
||||||
|
.HasColumnType("character varying(3)")
|
||||||
|
.HasDefaultValue("LKR");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(200)
|
||||||
|
.HasColumnType("character varying(200)");
|
||||||
|
|
||||||
|
b.Property<uint>("RowVersion")
|
||||||
|
.IsConcurrencyToken()
|
||||||
|
.ValueGeneratedOnAddOrUpdate()
|
||||||
|
.HasColumnType("xid")
|
||||||
|
.HasColumnName("xmin");
|
||||||
|
|
||||||
|
b.Property<string>("Status")
|
||||||
|
.IsRequired()
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasMaxLength(20)
|
||||||
|
.HasColumnType("character varying(20)")
|
||||||
|
.HasDefaultValue("Active");
|
||||||
|
|
||||||
|
b.Property<string>("TaxReg")
|
||||||
|
.HasMaxLength(50)
|
||||||
|
.HasColumnType("character varying(50)");
|
||||||
|
|
||||||
|
b.Property<string>("Terms")
|
||||||
|
.HasMaxLength(50)
|
||||||
|
.HasColumnType("character varying(50)");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("UpdatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.HasKey("VendorId");
|
||||||
|
|
||||||
|
b.HasIndex("Code")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.HasIndex("Status");
|
||||||
|
|
||||||
|
b.ToTable("vendors", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("ERPCore.Domain.Entities.Warehouse", b =>
|
||||||
|
{
|
||||||
|
b.Property<long>("WarehouseId")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("bigint");
|
||||||
|
|
||||||
|
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("WarehouseId"));
|
||||||
|
|
||||||
|
b.Property<string>("Code")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(50)
|
||||||
|
.HasColumnType("character varying(50)");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(200)
|
||||||
|
.HasColumnType("character varying(200)");
|
||||||
|
|
||||||
|
b.HasKey("WarehouseId");
|
||||||
|
|
||||||
|
b.HasIndex("Code")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("warehouses", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("ERPCore.Domain.Entities.Bin", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("ERPCore.Domain.Entities.Warehouse", "Warehouse")
|
||||||
|
.WithMany("Bins")
|
||||||
|
.HasForeignKey("WarehouseId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Warehouse");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("ERPCore.Domain.Entities.Category", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("ERPCore.Domain.Entities.Category", "Parent")
|
||||||
|
.WithMany("Children")
|
||||||
|
.HasForeignKey("ParentId")
|
||||||
|
.OnDelete(DeleteBehavior.Restrict);
|
||||||
|
|
||||||
|
b.Navigation("Parent");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("ERPCore.Domain.Entities.Item", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("ERPCore.Domain.Entities.Uom", "BaseUom")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("BaseUomId")
|
||||||
|
.OnDelete(DeleteBehavior.Restrict)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasOne("ERPCore.Domain.Entities.Category", "Category")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("CategoryId")
|
||||||
|
.OnDelete(DeleteBehavior.Restrict)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasOne("ERPCore.Domain.Entities.Vendor", "DefaultVendor")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("DefaultVendorId")
|
||||||
|
.OnDelete(DeleteBehavior.Restrict);
|
||||||
|
|
||||||
|
b.Navigation("BaseUom");
|
||||||
|
|
||||||
|
b.Navigation("Category");
|
||||||
|
|
||||||
|
b.Navigation("DefaultVendor");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("ERPCore.Domain.Entities.ItemReorder", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("ERPCore.Domain.Entities.Item", "Item")
|
||||||
|
.WithMany("ReorderSettings")
|
||||||
|
.HasForeignKey("ItemId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasOne("ERPCore.Domain.Entities.Warehouse", "Warehouse")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("WarehouseId")
|
||||||
|
.OnDelete(DeleteBehavior.Restrict)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Item");
|
||||||
|
|
||||||
|
b.Navigation("Warehouse");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("ERPCore.Domain.Entities.UomConversion", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("ERPCore.Domain.Entities.Uom", "FromUom")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("FromUomId")
|
||||||
|
.OnDelete(DeleteBehavior.Restrict)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasOne("ERPCore.Domain.Entities.Item", "Item")
|
||||||
|
.WithMany("UomConversions")
|
||||||
|
.HasForeignKey("ItemId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasOne("ERPCore.Domain.Entities.Uom", "ToUom")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("ToUomId")
|
||||||
|
.OnDelete(DeleteBehavior.Restrict)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("FromUom");
|
||||||
|
|
||||||
|
b.Navigation("Item");
|
||||||
|
|
||||||
|
b.Navigation("ToUom");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("ERPCore.Domain.Entities.Category", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("Children");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("ERPCore.Domain.Entities.Item", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("ReorderSettings");
|
||||||
|
|
||||||
|
b.Navigation("UomConversions");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("ERPCore.Domain.Entities.Warehouse", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("Bins");
|
||||||
|
});
|
||||||
|
#pragma warning restore 612, 618
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,8 +1,11 @@
|
|||||||
|
using System.Text.Json.Serialization;
|
||||||
using ERPCore.Infra.Auth;
|
using ERPCore.Infra.Auth;
|
||||||
using ERPCore.Infra.Persistence;
|
using ERPCore.Infra.Persistence;
|
||||||
using ERPCore.Infra.UoW;
|
using ERPCore.Infra.UoW;
|
||||||
using ERPCore.Repositories;
|
using ERPCore.Repositories;
|
||||||
using ERPCore.Repositories.Interfaces;
|
using ERPCore.Repositories.Interfaces;
|
||||||
|
using ERPCore.Services;
|
||||||
|
using ERPCore.Services.Interfaces;
|
||||||
using ERPCore.System.Errors;
|
using ERPCore.System.Errors;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.OpenApi;
|
using Microsoft.OpenApi;
|
||||||
@@ -15,7 +18,9 @@ builder.Host.UseSerilog((ctx, cfg) => cfg
|
|||||||
.ReadFrom.Configuration(ctx.Configuration)
|
.ReadFrom.Configuration(ctx.Configuration)
|
||||||
.WriteTo.File("logs/erpcore-.log", rollingInterval: RollingInterval.Day));
|
.WriteTo.File("logs/erpcore-.log", rollingInterval: RollingInterval.Day));
|
||||||
|
|
||||||
builder.Services.AddControllers();
|
// Controllers + JSON: serialize enums as their string names (docs/11 §8, camelCase).
|
||||||
|
builder.Services.AddControllers()
|
||||||
|
.AddJsonOptions(o => o.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter()));
|
||||||
|
|
||||||
// EF Core + PostgreSQL
|
// EF Core + PostgreSQL
|
||||||
builder.Services.AddDbContext<ErpDbContext>(o =>
|
builder.Services.AddDbContext<ErpDbContext>(o =>
|
||||||
@@ -36,6 +41,13 @@ builder.Services.AddScoped<ICurrentUser, CurrentUser>();
|
|||||||
builder.Services.AddScoped<IUnitOfWork, UnitOfWork>();
|
builder.Services.AddScoped<IUnitOfWork, UnitOfWork>();
|
||||||
builder.Services.AddScoped(typeof(IRepository<>), typeof(Repository<>));
|
builder.Services.AddScoped(typeof(IRepository<>), typeof(Repository<>));
|
||||||
|
|
||||||
|
// Master-data services (docs/11 §2)
|
||||||
|
builder.Services.AddScoped<IItemService, ItemService>();
|
||||||
|
builder.Services.AddScoped<IUomService, UomService>();
|
||||||
|
builder.Services.AddScoped<ICategoryService, CategoryService>();
|
||||||
|
builder.Services.AddScoped<IVendorService, VendorService>();
|
||||||
|
builder.Services.AddScoped<IWarehouseService, WarehouseService>();
|
||||||
|
|
||||||
// Health checks (EF Core DB)
|
// Health checks (EF Core DB)
|
||||||
builder.Services.AddHealthChecks().AddDbContextCheck<ErpDbContext>();
|
builder.Services.AddHealthChecks().AddDbContextCheck<ErpDbContext>();
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,70 @@
|
|||||||
|
using ERPCore.Domain.Entities;
|
||||||
|
using ERPCore.Dtos.Categories;
|
||||||
|
using ERPCore.Dtos.Common;
|
||||||
|
using ERPCore.Infra.UoW;
|
||||||
|
using ERPCore.Repositories.Interfaces;
|
||||||
|
using ERPCore.Services.Interfaces;
|
||||||
|
using ERPCore.System.Errors;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
|
namespace ERPCore.Services;
|
||||||
|
|
||||||
|
public sealed class CategoryService : ICategoryService
|
||||||
|
{
|
||||||
|
private readonly IRepository<Category> _categories;
|
||||||
|
private readonly IUnitOfWork _uow;
|
||||||
|
|
||||||
|
public CategoryService(IRepository<Category> categories, IUnitOfWork uow)
|
||||||
|
{
|
||||||
|
_categories = categories;
|
||||||
|
_uow = uow;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<PagedResponse<CategoryDto>> ListAsync(PageQuery query, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
var q = _categories.Query().AsNoTracking();
|
||||||
|
if (!string.IsNullOrWhiteSpace(query.Q))
|
||||||
|
{
|
||||||
|
var term = query.Q.Trim();
|
||||||
|
q = q.Where(c => EF.Functions.ILike(c.Name, $"%{term}%"));
|
||||||
|
}
|
||||||
|
|
||||||
|
var total = await q.CountAsync(ct);
|
||||||
|
var rows = await q.OrderBy(c => c.Name)
|
||||||
|
.Skip(query.Skip).Take(query.PageSize)
|
||||||
|
.Select(c => new CategoryDto(c.CategoryId, c.Name, c.ParentId))
|
||||||
|
.ToListAsync(ct);
|
||||||
|
|
||||||
|
return PagedResponse<CategoryDto>.Create(rows, query.Page, query.PageSize, total);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<IReadOnlyList<CategoryTreeDto>> GetTreeAsync(CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
var all = await _categories.Query().AsNoTracking()
|
||||||
|
.OrderBy(c => c.Name)
|
||||||
|
.Select(c => new CategoryDto(c.CategoryId, c.Name, c.ParentId))
|
||||||
|
.ToListAsync(ct);
|
||||||
|
|
||||||
|
var byParent = all.ToLookup(c => c.ParentId);
|
||||||
|
|
||||||
|
List<CategoryTreeDto> Build(long? parentId) =>
|
||||||
|
byParent[parentId]
|
||||||
|
.Select(c => new CategoryTreeDto(c.CategoryId, c.Name, c.ParentId, Build(c.CategoryId)))
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
return Build(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<CategoryDto> CreateAsync(CreateCategoryRequest request, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
if (request.ParentId is not null
|
||||||
|
&& !await _categories.Query().AnyAsync(c => c.CategoryId == request.ParentId, ct))
|
||||||
|
throw new DomainException(ErrorCodes.Validation, $"Parent category {request.ParentId} does not exist.", 422);
|
||||||
|
|
||||||
|
var category = new Category { Name = request.Name.Trim(), ParentId = request.ParentId };
|
||||||
|
await _categories.AddAsync(category, ct);
|
||||||
|
await _uow.SaveChangesAsync(ct);
|
||||||
|
|
||||||
|
return new CategoryDto(category.CategoryId, category.Name, category.ParentId);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
using ERPCore.Dtos.Categories;
|
||||||
|
using ERPCore.Dtos.Common;
|
||||||
|
|
||||||
|
namespace ERPCore.Services.Interfaces;
|
||||||
|
|
||||||
|
/// <summary>Category master business logic (docs/11-BACKEND-PHASE1.md §2.3).</summary>
|
||||||
|
public interface ICategoryService
|
||||||
|
{
|
||||||
|
Task<PagedResponse<CategoryDto>> ListAsync(PageQuery query, CancellationToken ct = default);
|
||||||
|
Task<IReadOnlyList<CategoryTreeDto>> GetTreeAsync(CancellationToken ct = default);
|
||||||
|
Task<CategoryDto> CreateAsync(CreateCategoryRequest request, CancellationToken ct = default);
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
using ERPCore.Common.Http;
|
||||||
|
using ERPCore.Domain.Enums;
|
||||||
|
using ERPCore.Dtos.Common;
|
||||||
|
using ERPCore.Dtos.Items;
|
||||||
|
|
||||||
|
namespace ERPCore.Services.Interfaces;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Item master business logic (docs/11-BACKEND-PHASE1.md §2.1–2.2). Returns DTOs;
|
||||||
|
/// entities never cross this boundary (00-CORE §4).
|
||||||
|
/// </summary>
|
||||||
|
public interface IItemService
|
||||||
|
{
|
||||||
|
Task<PagedResponse<ItemListItemDto>> ListAsync(
|
||||||
|
PageQuery query, EntityStatus? status, long? categoryId, TrackingMode? trackingMode, CancellationToken ct = default);
|
||||||
|
|
||||||
|
Task<ETagged<ItemDetailDto>?> GetAsync(long itemId, CancellationToken ct = default);
|
||||||
|
|
||||||
|
Task<ETagged<ItemDetailDto>> CreateAsync(CreateItemRequest request, CancellationToken ct = default);
|
||||||
|
|
||||||
|
Task<ETagged<ItemDetailDto>> UpdateAsync(long itemId, UpdateItemRequest request, uint expectedRowVersion, CancellationToken ct = default);
|
||||||
|
|
||||||
|
Task SetStatusAsync(long itemId, EntityStatus status, CancellationToken ct = default);
|
||||||
|
|
||||||
|
Task<ItemReorderSettingsDto> UpdateReorderAsync(long itemId, UpdateReorderRequest request, CancellationToken ct = default);
|
||||||
|
|
||||||
|
Task<ItemUomConversionsDto> UpdateUomConversionsAsync(long itemId, UpdateUomConversionsRequest request, CancellationToken ct = default);
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
using ERPCore.Dtos.Common;
|
||||||
|
using ERPCore.Dtos.Uoms;
|
||||||
|
|
||||||
|
namespace ERPCore.Services.Interfaces;
|
||||||
|
|
||||||
|
/// <summary>UOM master business logic (docs/11-BACKEND-PHASE1.md §2.2).</summary>
|
||||||
|
public interface IUomService
|
||||||
|
{
|
||||||
|
Task<PagedResponse<UomDto>> ListAsync(PageQuery query, CancellationToken ct = default);
|
||||||
|
Task<UomDto> CreateAsync(CreateUomRequest request, CancellationToken ct = default);
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
using ERPCore.Common.Http;
|
||||||
|
using ERPCore.Domain.Enums;
|
||||||
|
using ERPCore.Dtos.Common;
|
||||||
|
using ERPCore.Dtos.Vendors;
|
||||||
|
|
||||||
|
namespace ERPCore.Services.Interfaces;
|
||||||
|
|
||||||
|
/// <summary>Vendor master business logic (docs/11-BACKEND-PHASE1.md §2.4).</summary>
|
||||||
|
public interface IVendorService
|
||||||
|
{
|
||||||
|
Task<PagedResponse<VendorDto>> ListAsync(PageQuery query, EntityStatus? status, CancellationToken ct = default);
|
||||||
|
Task<ETagged<VendorDto>?> GetAsync(long vendorId, CancellationToken ct = default);
|
||||||
|
Task<ETagged<VendorDto>> CreateAsync(CreateVendorRequest request, CancellationToken ct = default);
|
||||||
|
Task<ETagged<VendorDto>> UpdateAsync(long vendorId, UpdateVendorRequest request, uint expectedRowVersion, CancellationToken ct = default);
|
||||||
|
Task SetStatusAsync(long vendorId, EntityStatus status, CancellationToken ct = default);
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
using ERPCore.Dtos.Common;
|
||||||
|
using ERPCore.Dtos.Warehouses;
|
||||||
|
|
||||||
|
namespace ERPCore.Services.Interfaces;
|
||||||
|
|
||||||
|
/// <summary>Warehouse & bin master business logic (docs/11-BACKEND-PHASE1.md §2.5).</summary>
|
||||||
|
public interface IWarehouseService
|
||||||
|
{
|
||||||
|
Task<PagedResponse<WarehouseDto>> ListAsync(PageQuery query, CancellationToken ct = default);
|
||||||
|
Task<WarehouseDto?> GetAsync(long warehouseId, CancellationToken ct = default);
|
||||||
|
Task<WarehouseDto> CreateAsync(CreateWarehouseRequest request, CancellationToken ct = default);
|
||||||
|
|
||||||
|
Task<IReadOnlyList<BinDto>> ListBinsAsync(long warehouseId, CancellationToken ct = default);
|
||||||
|
Task<BinDto> CreateBinAsync(long warehouseId, CreateBinRequest request, CancellationToken ct = default);
|
||||||
|
}
|
||||||
@@ -0,0 +1,282 @@
|
|||||||
|
using ERPCore.Common.Http;
|
||||||
|
using ERPCore.Domain.Entities;
|
||||||
|
using ERPCore.Domain.Enums;
|
||||||
|
using ERPCore.Dtos.Common;
|
||||||
|
using ERPCore.Dtos.Items;
|
||||||
|
using ERPCore.Infra.UoW;
|
||||||
|
using ERPCore.Repositories.Interfaces;
|
||||||
|
using ERPCore.Services.Interfaces;
|
||||||
|
using ERPCore.System.Errors;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
|
namespace ERPCore.Services;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Item master service. Enforces SKU uniqueness (SKU_DUPLICATE), reference
|
||||||
|
/// integrity, and optimistic concurrency (CONCURRENCY_CONFLICT) per
|
||||||
|
/// docs/11-BACKEND-PHASE1.md §2.1–2.2 and 02-SECURITY C.1.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class ItemService : IItemService
|
||||||
|
{
|
||||||
|
private readonly IRepository<Item> _items;
|
||||||
|
private readonly IRepository<Category> _categories;
|
||||||
|
private readonly IRepository<Uom> _uoms;
|
||||||
|
private readonly IRepository<Vendor> _vendors;
|
||||||
|
private readonly IRepository<Warehouse> _warehouses;
|
||||||
|
private readonly IUnitOfWork _uow;
|
||||||
|
|
||||||
|
public ItemService(
|
||||||
|
IRepository<Item> items,
|
||||||
|
IRepository<Category> categories,
|
||||||
|
IRepository<Uom> uoms,
|
||||||
|
IRepository<Vendor> vendors,
|
||||||
|
IRepository<Warehouse> warehouses,
|
||||||
|
IUnitOfWork uow)
|
||||||
|
{
|
||||||
|
_items = items;
|
||||||
|
_categories = categories;
|
||||||
|
_uoms = uoms;
|
||||||
|
_vendors = vendors;
|
||||||
|
_warehouses = warehouses;
|
||||||
|
_uow = uow;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<PagedResponse<ItemListItemDto>> ListAsync(
|
||||||
|
PageQuery query, EntityStatus? status, long? categoryId, TrackingMode? trackingMode, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
var q = _items.Query().AsNoTracking();
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(query.Q))
|
||||||
|
{
|
||||||
|
var term = query.Q.Trim();
|
||||||
|
q = q.Where(i => EF.Functions.ILike(i.Sku, $"%{term}%") || EF.Functions.ILike(i.Name, $"%{term}%"));
|
||||||
|
}
|
||||||
|
if (status is not null) q = q.Where(i => i.Status == status);
|
||||||
|
if (categoryId is not null) q = q.Where(i => i.CategoryId == categoryId);
|
||||||
|
if (trackingMode is not null) q = q.Where(i => i.TrackingMode == trackingMode);
|
||||||
|
|
||||||
|
var total = await q.CountAsync(ct);
|
||||||
|
var rows = await q.OrderBy(i => i.Sku)
|
||||||
|
.Skip(query.Skip).Take(query.PageSize)
|
||||||
|
.Select(i => new ItemListItemDto(
|
||||||
|
i.ItemId, i.Sku, i.Name, i.CategoryId, i.BaseUomId, i.DefaultVendorId,
|
||||||
|
i.ItemType, i.TrackingMode, i.TaxClass, i.Status))
|
||||||
|
.ToListAsync(ct);
|
||||||
|
|
||||||
|
return PagedResponse<ItemListItemDto>.Create(rows, query.Page, query.PageSize, total);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<ETagged<ItemDetailDto>?> GetAsync(long itemId, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
var item = await _items.Query().AsNoTracking()
|
||||||
|
.Include(i => i.ReorderSettings)
|
||||||
|
.FirstOrDefaultAsync(i => i.ItemId == itemId, ct);
|
||||||
|
|
||||||
|
return item is null ? null : new ETagged<ItemDetailDto>(ToDetail(item), item.RowVersion);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<ETagged<ItemDetailDto>> CreateAsync(CreateItemRequest request, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
if (await _items.Query().AnyAsync(i => i.Sku == request.Sku, ct))
|
||||||
|
throw new DomainException(ErrorCodes.SkuDuplicate, $"An item with SKU '{request.Sku}' already exists.", 400);
|
||||||
|
|
||||||
|
await ValidateReferencesAsync(request.CategoryId, request.BaseUomId, request.DefaultVendorId, ct);
|
||||||
|
|
||||||
|
var item = new Item
|
||||||
|
{
|
||||||
|
Sku = request.Sku.Trim(),
|
||||||
|
Name = request.Name.Trim(),
|
||||||
|
Description = request.Description,
|
||||||
|
CategoryId = request.CategoryId,
|
||||||
|
BaseUomId = request.BaseUomId,
|
||||||
|
DefaultVendorId = request.DefaultVendorId,
|
||||||
|
ItemType = request.ItemType,
|
||||||
|
TrackingMode = request.TrackingMode,
|
||||||
|
TaxClass = request.TaxClass,
|
||||||
|
Status = EntityStatus.Active,
|
||||||
|
CreatedAt = DateTime.UtcNow
|
||||||
|
};
|
||||||
|
|
||||||
|
await _items.AddAsync(item, ct);
|
||||||
|
await _uow.SaveChangesAsync(ct);
|
||||||
|
|
||||||
|
return new ETagged<ItemDetailDto>(ToDetail(item), item.RowVersion);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<ETagged<ItemDetailDto>> UpdateAsync(
|
||||||
|
long itemId, UpdateItemRequest request, uint expectedRowVersion, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
var item = await _items.Query()
|
||||||
|
.Include(i => i.ReorderSettings)
|
||||||
|
.FirstOrDefaultAsync(i => i.ItemId == itemId, ct)
|
||||||
|
?? throw new NotFoundException($"Item {itemId} was not found.");
|
||||||
|
|
||||||
|
if (item.RowVersion != expectedRowVersion)
|
||||||
|
throw new DomainException(ErrorCodes.ConcurrencyConflict, "The item was modified by another request.", 412);
|
||||||
|
|
||||||
|
if (!string.Equals(item.Sku, request.Sku, StringComparison.Ordinal)
|
||||||
|
&& await _items.Query().AnyAsync(i => i.Sku == request.Sku && i.ItemId != itemId, ct))
|
||||||
|
throw new DomainException(ErrorCodes.SkuDuplicate, $"An item with SKU '{request.Sku}' already exists.", 400);
|
||||||
|
|
||||||
|
await ValidateReferencesAsync(request.CategoryId, request.BaseUomId, request.DefaultVendorId, ct);
|
||||||
|
|
||||||
|
item.Sku = request.Sku.Trim();
|
||||||
|
item.Name = request.Name.Trim();
|
||||||
|
item.Description = request.Description;
|
||||||
|
item.CategoryId = request.CategoryId;
|
||||||
|
item.BaseUomId = request.BaseUomId;
|
||||||
|
item.DefaultVendorId = request.DefaultVendorId;
|
||||||
|
item.ItemType = request.ItemType;
|
||||||
|
item.TrackingMode = request.TrackingMode;
|
||||||
|
item.TaxClass = request.TaxClass;
|
||||||
|
item.UpdatedAt = DateTime.UtcNow;
|
||||||
|
|
||||||
|
await SaveGuardingConcurrencyAsync(ct);
|
||||||
|
return new ETagged<ItemDetailDto>(ToDetail(item), item.RowVersion);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task SetStatusAsync(long itemId, EntityStatus status, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
var item = await _items.GetByIdAsync(itemId, ct)
|
||||||
|
?? throw new NotFoundException($"Item {itemId} was not found.");
|
||||||
|
|
||||||
|
item.Status = status;
|
||||||
|
item.UpdatedAt = DateTime.UtcNow;
|
||||||
|
await _uow.SaveChangesAsync(ct);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<ItemReorderSettingsDto> UpdateReorderAsync(
|
||||||
|
long itemId, UpdateReorderRequest request, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
if (request.Settings.Select(s => s.WarehouseId).Distinct().Count() != request.Settings.Count)
|
||||||
|
throw new DomainException(ErrorCodes.Validation, "Duplicate warehouseId in reorder settings.", 400);
|
||||||
|
|
||||||
|
var item = await _items.Query()
|
||||||
|
.Include(i => i.ReorderSettings)
|
||||||
|
.FirstOrDefaultAsync(i => i.ItemId == itemId, ct)
|
||||||
|
?? throw new NotFoundException($"Item {itemId} was not found.");
|
||||||
|
|
||||||
|
foreach (var warehouseId in request.Settings.Select(s => s.WarehouseId))
|
||||||
|
if (!await _warehouses.Query().AnyAsync(w => w.WarehouseId == warehouseId, ct))
|
||||||
|
throw new DomainException(ErrorCodes.Validation, $"Warehouse {warehouseId} does not exist.", 422);
|
||||||
|
|
||||||
|
// Full-replacement upsert (avoids delete+insert clashes on the unique index).
|
||||||
|
foreach (var stale in item.ReorderSettings.Where(r => request.Settings.All(s => s.WarehouseId != r.WarehouseId)).ToList())
|
||||||
|
item.ReorderSettings.Remove(stale);
|
||||||
|
foreach (var input in request.Settings)
|
||||||
|
{
|
||||||
|
var existing = item.ReorderSettings.FirstOrDefault(r => r.WarehouseId == input.WarehouseId);
|
||||||
|
if (existing is null)
|
||||||
|
{
|
||||||
|
item.ReorderSettings.Add(new ItemReorder
|
||||||
|
{
|
||||||
|
ItemId = itemId,
|
||||||
|
WarehouseId = input.WarehouseId,
|
||||||
|
ReorderPoint = input.ReorderPoint,
|
||||||
|
ReorderQty = input.ReorderQty
|
||||||
|
});
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
existing.ReorderPoint = input.ReorderPoint;
|
||||||
|
existing.ReorderQty = input.ReorderQty;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
item.UpdatedAt = DateTime.UtcNow;
|
||||||
|
await _uow.SaveChangesAsync(ct);
|
||||||
|
|
||||||
|
var settings = item.ReorderSettings
|
||||||
|
.OrderBy(r => r.WarehouseId)
|
||||||
|
.Select(r => new ItemReorderDto(r.WarehouseId, r.ReorderPoint, r.ReorderQty))
|
||||||
|
.ToList();
|
||||||
|
return new ItemReorderSettingsDto(settings);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<ItemUomConversionsDto> UpdateUomConversionsAsync(
|
||||||
|
long itemId, UpdateUomConversionsRequest request, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
var pairs = request.Conversions.Select(c => (c.FromUom, c.ToUom)).ToList();
|
||||||
|
if (pairs.Distinct().Count() != pairs.Count)
|
||||||
|
throw new DomainException(ErrorCodes.Validation, "Duplicate (fromUom, toUom) in conversions.", 400);
|
||||||
|
|
||||||
|
var item = await _items.Query()
|
||||||
|
.Include(i => i.UomConversions)
|
||||||
|
.FirstOrDefaultAsync(i => i.ItemId == itemId, ct)
|
||||||
|
?? throw new NotFoundException($"Item {itemId} was not found.");
|
||||||
|
|
||||||
|
foreach (var uomId in request.Conversions.SelectMany(c => new[] { c.FromUom, c.ToUom }).Distinct())
|
||||||
|
if (!await _uoms.Query().AnyAsync(u => u.UomId == uomId, ct))
|
||||||
|
throw new DomainException(ErrorCodes.Validation, $"UOM {uomId} does not exist.", 422);
|
||||||
|
|
||||||
|
foreach (var stale in item.UomConversions.Where(c => request.Conversions.All(r => r.FromUom != c.FromUomId || r.ToUom != c.ToUomId)).ToList())
|
||||||
|
item.UomConversions.Remove(stale);
|
||||||
|
foreach (var input in request.Conversions)
|
||||||
|
{
|
||||||
|
var existing = item.UomConversions.FirstOrDefault(c => c.FromUomId == input.FromUom && c.ToUomId == input.ToUom);
|
||||||
|
if (existing is null)
|
||||||
|
{
|
||||||
|
item.UomConversions.Add(new UomConversion
|
||||||
|
{
|
||||||
|
ItemId = itemId,
|
||||||
|
FromUomId = input.FromUom,
|
||||||
|
ToUomId = input.ToUom,
|
||||||
|
Factor = input.Factor
|
||||||
|
});
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
existing.Factor = input.Factor;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
item.UpdatedAt = DateTime.UtcNow;
|
||||||
|
await _uow.SaveChangesAsync(ct);
|
||||||
|
|
||||||
|
var conversions = item.UomConversions
|
||||||
|
.OrderBy(c => c.ConversionId)
|
||||||
|
.Select(c => new UomConversionDto(c.ConversionId, c.FromUomId, c.ToUomId, c.Factor))
|
||||||
|
.ToList();
|
||||||
|
return new ItemUomConversionsDto(item.ItemId, item.BaseUomId, conversions);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task ValidateReferencesAsync(long categoryId, long baseUomId, long? defaultVendorId, CancellationToken ct)
|
||||||
|
{
|
||||||
|
if (!await _categories.Query().AnyAsync(c => c.CategoryId == categoryId, ct))
|
||||||
|
throw new DomainException(ErrorCodes.Validation, $"Category {categoryId} does not exist.", 422);
|
||||||
|
|
||||||
|
if (!await _uoms.Query().AnyAsync(u => u.UomId == baseUomId, ct))
|
||||||
|
throw new DomainException(ErrorCodes.Validation, $"UOM {baseUomId} does not exist.", 422);
|
||||||
|
|
||||||
|
if (defaultVendorId is not null)
|
||||||
|
{
|
||||||
|
var vendor = await _vendors.Query().AsNoTracking()
|
||||||
|
.FirstOrDefaultAsync(v => v.VendorId == defaultVendorId, ct);
|
||||||
|
if (vendor is null)
|
||||||
|
throw new DomainException(ErrorCodes.Validation, $"Vendor {defaultVendorId} does not exist.", 422);
|
||||||
|
if (vendor.Status != EntityStatus.Active)
|
||||||
|
throw new DomainException(ErrorCodes.Validation, $"Vendor {defaultVendorId} is inactive.", 422);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task SaveGuardingConcurrencyAsync(CancellationToken ct)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await _uow.SaveChangesAsync(ct);
|
||||||
|
}
|
||||||
|
catch (DbUpdateConcurrencyException)
|
||||||
|
{
|
||||||
|
throw new DomainException(ErrorCodes.ConcurrencyConflict, "The item was modified by another request.", 412);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ItemDetailDto ToDetail(Item i) => new(
|
||||||
|
i.ItemId, i.Sku, i.Name, i.Description, i.CategoryId, i.BaseUomId, i.DefaultVendorId,
|
||||||
|
i.ItemType, i.TrackingMode, i.TaxClass, i.Status,
|
||||||
|
i.ReorderSettings
|
||||||
|
.OrderBy(r => r.WarehouseId)
|
||||||
|
.Select(r => new ItemReorderDto(r.WarehouseId, r.ReorderPoint, r.ReorderQty))
|
||||||
|
.ToList(),
|
||||||
|
i.CreatedAt, i.UpdatedAt);
|
||||||
|
}
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
using ERPCore.Domain.Entities;
|
||||||
|
using ERPCore.Dtos.Common;
|
||||||
|
using ERPCore.Dtos.Uoms;
|
||||||
|
using ERPCore.Infra.UoW;
|
||||||
|
using ERPCore.Repositories.Interfaces;
|
||||||
|
using ERPCore.Services.Interfaces;
|
||||||
|
using ERPCore.System.Errors;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
|
namespace ERPCore.Services;
|
||||||
|
|
||||||
|
public sealed class UomService : IUomService
|
||||||
|
{
|
||||||
|
private readonly IRepository<Uom> _uoms;
|
||||||
|
private readonly IUnitOfWork _uow;
|
||||||
|
|
||||||
|
public UomService(IRepository<Uom> uoms, IUnitOfWork uow)
|
||||||
|
{
|
||||||
|
_uoms = uoms;
|
||||||
|
_uow = uow;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<PagedResponse<UomDto>> ListAsync(PageQuery query, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
var q = _uoms.Query().AsNoTracking();
|
||||||
|
if (!string.IsNullOrWhiteSpace(query.Q))
|
||||||
|
{
|
||||||
|
var term = query.Q.Trim();
|
||||||
|
q = q.Where(u => EF.Functions.ILike(u.Name, $"%{term}%"));
|
||||||
|
}
|
||||||
|
|
||||||
|
var total = await q.CountAsync(ct);
|
||||||
|
var rows = await q.OrderBy(u => u.Name)
|
||||||
|
.Skip(query.Skip).Take(query.PageSize)
|
||||||
|
.Select(u => new UomDto(u.UomId, u.Name))
|
||||||
|
.ToListAsync(ct);
|
||||||
|
|
||||||
|
return PagedResponse<UomDto>.Create(rows, query.Page, query.PageSize, total);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<UomDto> CreateAsync(CreateUomRequest request, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
var name = request.Name.Trim();
|
||||||
|
if (await _uoms.Query().AnyAsync(u => u.Name == name, ct))
|
||||||
|
throw new ConflictException($"A UOM named '{name}' already exists.");
|
||||||
|
|
||||||
|
var uom = new Uom { Name = name };
|
||||||
|
await _uoms.AddAsync(uom, ct);
|
||||||
|
await _uow.SaveChangesAsync(ct);
|
||||||
|
|
||||||
|
return new UomDto(uom.UomId, uom.Name);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,118 @@
|
|||||||
|
using ERPCore.Common.Http;
|
||||||
|
using ERPCore.Domain.Entities;
|
||||||
|
using ERPCore.Domain.Enums;
|
||||||
|
using ERPCore.Dtos.Common;
|
||||||
|
using ERPCore.Dtos.Vendors;
|
||||||
|
using ERPCore.Infra.UoW;
|
||||||
|
using ERPCore.Repositories.Interfaces;
|
||||||
|
using ERPCore.Services.Interfaces;
|
||||||
|
using ERPCore.System.Errors;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
|
namespace ERPCore.Services;
|
||||||
|
|
||||||
|
public sealed class VendorService : IVendorService
|
||||||
|
{
|
||||||
|
private readonly IRepository<Vendor> _vendors;
|
||||||
|
private readonly IUnitOfWork _uow;
|
||||||
|
|
||||||
|
public VendorService(IRepository<Vendor> vendors, IUnitOfWork uow)
|
||||||
|
{
|
||||||
|
_vendors = vendors;
|
||||||
|
_uow = uow;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<PagedResponse<VendorDto>> ListAsync(PageQuery query, EntityStatus? status, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
var q = _vendors.Query().AsNoTracking();
|
||||||
|
if (!string.IsNullOrWhiteSpace(query.Q))
|
||||||
|
{
|
||||||
|
var term = query.Q.Trim();
|
||||||
|
q = q.Where(v => EF.Functions.ILike(v.Code, $"%{term}%") || EF.Functions.ILike(v.Name, $"%{term}%"));
|
||||||
|
}
|
||||||
|
if (status is not null) q = q.Where(v => v.Status == status);
|
||||||
|
|
||||||
|
var total = await q.CountAsync(ct);
|
||||||
|
var rows = await q.OrderBy(v => v.Code)
|
||||||
|
.Skip(query.Skip).Take(query.PageSize)
|
||||||
|
.ToListAsync(ct);
|
||||||
|
|
||||||
|
return PagedResponse<VendorDto>.Create(rows.Select(Map).ToList(), query.Page, query.PageSize, total);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<ETagged<VendorDto>?> GetAsync(long vendorId, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
var vendor = await _vendors.Query().AsNoTracking()
|
||||||
|
.FirstOrDefaultAsync(v => v.VendorId == vendorId, ct);
|
||||||
|
return vendor is null ? null : new ETagged<VendorDto>(Map(vendor), vendor.RowVersion);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<ETagged<VendorDto>> CreateAsync(CreateVendorRequest request, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
var code = request.Code.Trim();
|
||||||
|
if (await _vendors.Query().AnyAsync(v => v.Code == code, ct))
|
||||||
|
throw new ConflictException($"A vendor with code '{code}' already exists.");
|
||||||
|
|
||||||
|
var vendor = new Vendor
|
||||||
|
{
|
||||||
|
Code = code,
|
||||||
|
Name = request.Name.Trim(),
|
||||||
|
Terms = request.Terms,
|
||||||
|
TaxReg = request.TaxReg,
|
||||||
|
Currency = request.Currency.Trim().ToUpperInvariant(),
|
||||||
|
Status = EntityStatus.Active,
|
||||||
|
CreatedAt = DateTime.UtcNow
|
||||||
|
};
|
||||||
|
|
||||||
|
await _vendors.AddAsync(vendor, ct);
|
||||||
|
await _uow.SaveChangesAsync(ct);
|
||||||
|
|
||||||
|
return new ETagged<VendorDto>(Map(vendor), vendor.RowVersion);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<ETagged<VendorDto>> UpdateAsync(
|
||||||
|
long vendorId, UpdateVendorRequest request, uint expectedRowVersion, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
var vendor = await _vendors.GetByIdAsync(vendorId, ct)
|
||||||
|
?? throw new NotFoundException($"Vendor {vendorId} was not found.");
|
||||||
|
|
||||||
|
if (vendor.RowVersion != expectedRowVersion)
|
||||||
|
throw new DomainException(ErrorCodes.ConcurrencyConflict, "The vendor was modified by another request.", 412);
|
||||||
|
|
||||||
|
var code = request.Code.Trim();
|
||||||
|
if (!string.Equals(vendor.Code, code, StringComparison.Ordinal)
|
||||||
|
&& await _vendors.Query().AnyAsync(v => v.Code == code && v.VendorId != vendorId, ct))
|
||||||
|
throw new ConflictException($"A vendor with code '{code}' already exists.");
|
||||||
|
|
||||||
|
vendor.Code = code;
|
||||||
|
vendor.Name = request.Name.Trim();
|
||||||
|
vendor.Terms = request.Terms;
|
||||||
|
vendor.TaxReg = request.TaxReg;
|
||||||
|
vendor.Currency = request.Currency.Trim().ToUpperInvariant();
|
||||||
|
vendor.UpdatedAt = DateTime.UtcNow;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await _uow.SaveChangesAsync(ct);
|
||||||
|
}
|
||||||
|
catch (DbUpdateConcurrencyException)
|
||||||
|
{
|
||||||
|
throw new DomainException(ErrorCodes.ConcurrencyConflict, "The vendor was modified by another request.", 412);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new ETagged<VendorDto>(Map(vendor), vendor.RowVersion);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task SetStatusAsync(long vendorId, EntityStatus status, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
var vendor = await _vendors.GetByIdAsync(vendorId, ct)
|
||||||
|
?? throw new NotFoundException($"Vendor {vendorId} was not found.");
|
||||||
|
|
||||||
|
vendor.Status = status;
|
||||||
|
vendor.UpdatedAt = DateTime.UtcNow;
|
||||||
|
await _uow.SaveChangesAsync(ct);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static VendorDto Map(Vendor v) => new(
|
||||||
|
v.VendorId, v.Code, v.Name, v.Terms, v.TaxReg, v.Currency, v.Status, v.CreatedAt, v.UpdatedAt);
|
||||||
|
}
|
||||||
@@ -0,0 +1,94 @@
|
|||||||
|
using ERPCore.Domain.Entities;
|
||||||
|
using ERPCore.Dtos.Common;
|
||||||
|
using ERPCore.Dtos.Warehouses;
|
||||||
|
using ERPCore.Infra.UoW;
|
||||||
|
using ERPCore.Repositories.Interfaces;
|
||||||
|
using ERPCore.Services.Interfaces;
|
||||||
|
using ERPCore.System.Errors;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
|
namespace ERPCore.Services;
|
||||||
|
|
||||||
|
public sealed class WarehouseService : IWarehouseService
|
||||||
|
{
|
||||||
|
private readonly IRepository<Warehouse> _warehouses;
|
||||||
|
private readonly IRepository<Bin> _bins;
|
||||||
|
private readonly IUnitOfWork _uow;
|
||||||
|
|
||||||
|
public WarehouseService(IRepository<Warehouse> warehouses, IRepository<Bin> bins, IUnitOfWork uow)
|
||||||
|
{
|
||||||
|
_warehouses = warehouses;
|
||||||
|
_bins = bins;
|
||||||
|
_uow = uow;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<PagedResponse<WarehouseDto>> ListAsync(PageQuery query, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
var q = _warehouses.Query().AsNoTracking();
|
||||||
|
if (!string.IsNullOrWhiteSpace(query.Q))
|
||||||
|
{
|
||||||
|
var term = query.Q.Trim();
|
||||||
|
q = q.Where(w => EF.Functions.ILike(w.Code, $"%{term}%") || EF.Functions.ILike(w.Name, $"%{term}%"));
|
||||||
|
}
|
||||||
|
|
||||||
|
var total = await q.CountAsync(ct);
|
||||||
|
var rows = await q.OrderBy(w => w.Code)
|
||||||
|
.Skip(query.Skip).Take(query.PageSize)
|
||||||
|
.Select(w => new WarehouseDto(w.WarehouseId, w.Code, w.Name))
|
||||||
|
.ToListAsync(ct);
|
||||||
|
|
||||||
|
return PagedResponse<WarehouseDto>.Create(rows, query.Page, query.PageSize, total);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<WarehouseDto?> GetAsync(long warehouseId, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
var w = await _warehouses.Query().AsNoTracking()
|
||||||
|
.FirstOrDefaultAsync(x => x.WarehouseId == warehouseId, ct);
|
||||||
|
return w is null ? null : new WarehouseDto(w.WarehouseId, w.Code, w.Name);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<WarehouseDto> CreateAsync(CreateWarehouseRequest request, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
var code = request.Code.Trim();
|
||||||
|
if (await _warehouses.Query().AnyAsync(w => w.Code == code, ct))
|
||||||
|
throw new ConflictException($"A warehouse with code '{code}' already exists.");
|
||||||
|
|
||||||
|
var warehouse = new Warehouse { Code = code, Name = request.Name.Trim() };
|
||||||
|
await _warehouses.AddAsync(warehouse, ct);
|
||||||
|
await _uow.SaveChangesAsync(ct);
|
||||||
|
|
||||||
|
return new WarehouseDto(warehouse.WarehouseId, warehouse.Code, warehouse.Name);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<IReadOnlyList<BinDto>> ListBinsAsync(long warehouseId, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
await EnsureWarehouseExistsAsync(warehouseId, ct);
|
||||||
|
|
||||||
|
return await _bins.Query().AsNoTracking()
|
||||||
|
.Where(b => b.WarehouseId == warehouseId)
|
||||||
|
.OrderBy(b => b.Code)
|
||||||
|
.Select(b => new BinDto(b.BinId, b.WarehouseId, b.Code, b.BinType))
|
||||||
|
.ToListAsync(ct);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<BinDto> CreateBinAsync(long warehouseId, CreateBinRequest request, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
await EnsureWarehouseExistsAsync(warehouseId, ct);
|
||||||
|
|
||||||
|
var code = request.Code.Trim();
|
||||||
|
if (await _bins.Query().AnyAsync(b => b.WarehouseId == warehouseId && b.Code == code, ct))
|
||||||
|
throw new ConflictException($"Bin '{code}' already exists in warehouse {warehouseId}.");
|
||||||
|
|
||||||
|
var bin = new Bin { WarehouseId = warehouseId, Code = code, BinType = request.BinType };
|
||||||
|
await _bins.AddAsync(bin, ct);
|
||||||
|
await _uow.SaveChangesAsync(ct);
|
||||||
|
|
||||||
|
return new BinDto(bin.BinId, bin.WarehouseId, bin.Code, bin.BinType);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task EnsureWarehouseExistsAsync(long warehouseId, CancellationToken ct)
|
||||||
|
{
|
||||||
|
if (!await _warehouses.Query().AnyAsync(w => w.WarehouseId == warehouseId, ct))
|
||||||
|
throw new NotFoundException($"Warehouse {warehouseId} was not found.");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,12 +3,25 @@ namespace ERPCore.System.Errors;
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Stable domain error codes carried in the <c>code</c> extension of RFC 7807
|
/// Stable domain error codes carried in the <c>code</c> extension of RFC 7807
|
||||||
/// ProblemDetails responses. The authoritative catalog lives in
|
/// ProblemDetails responses. The authoritative catalog lives in
|
||||||
/// docs/11-BACKEND-PHASE1.md; add codes here as endpoints are implemented so
|
/// docs/11-BACKEND-PHASE1.md §7; add codes here as endpoints are implemented so
|
||||||
/// the two stay in sync.
|
/// the two stay in sync.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static class ErrorCodes
|
public static class ErrorCodes
|
||||||
{
|
{
|
||||||
|
// Generic (framework-level) helpers used by the exception types below.
|
||||||
public const string Validation = "validation_error";
|
public const string Validation = "validation_error";
|
||||||
public const string NotFound = "not_found";
|
public const string NotFound = "not_found";
|
||||||
public const string Conflict = "conflict";
|
public const string Conflict = "conflict";
|
||||||
|
|
||||||
|
// Catalog (docs/11 §7) — exact strings surfaced to clients.
|
||||||
|
public const string SkuDuplicate = "SKU_DUPLICATE";
|
||||||
|
public const string MasterInUse = "MASTER_IN_USE";
|
||||||
|
public const string PoNotEditable = "PO_NOT_EDITABLE";
|
||||||
|
public const string OverReceiptTolerance = "OVER_RECEIPT_TOLERANCE";
|
||||||
|
public const string StockNegativeBlocked = "STOCK_NEGATIVE_BLOCKED";
|
||||||
|
public const string ExpiredBatchBlocked = "EXPIRED_BATCH_BLOCKED";
|
||||||
|
public const string OnHoldNotIssuable = "ONHOLD_NOT_ISSUABLE";
|
||||||
|
public const string ReasonCodeRequired = "REASON_CODE_REQUIRED";
|
||||||
|
public const string ConcurrencyConflict = "CONCURRENCY_CONFLICT";
|
||||||
|
public const string IdempotencyReplay = "IDEMPOTENCY_REPLAY";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"ConnectionStrings": {
|
"ConnectionStrings": {
|
||||||
"DefaultConnection": "Host=localhost;Port=5432;Database=ERPCore;Username=postgres;Password=postgres"
|
"DefaultConnection": "Host=localhost;Port=5432;Database=ERPCore;Username=postgres;Password=root"
|
||||||
},
|
},
|
||||||
"Jwt": {
|
"Jwt": {
|
||||||
"SigningKey": "dev-only-signing-key-please-change-me-0123456789"
|
"SigningKey": "dev-only-signing-key-please-change-me-0123456789"
|
||||||
|
|||||||
+25
-15
@@ -5,23 +5,24 @@ Spec: `docs/10-BACKEND-PHASE1.md` (model + rules) · `docs/11-BACKEND-PHASE1.md`
|
|||||||
Convention: `docs/01-DOC-GUIDE.md §6`. Update this file in the **same commit** as the code. When ticking `[x]`, append a short note + any deviation.
|
Convention: `docs/01-DOC-GUIDE.md §6`. Update this file in the **same commit** as the code. When ticking `[x]`, append a short note + any deviation.
|
||||||
|
|
||||||
## 0. Bootstrap
|
## 0. Bootstrap
|
||||||
- [ ] Solution + Web API project (`net10.0`), packages restored (00-CORE §5.4)
|
- [x] Solution + Web API project (`net10.0`), packages restored (00-CORE §5.4)
|
||||||
- [ ] Folder structure per 00-CORE §5.3
|
- [x] Folder structure per 00-CORE §5.3
|
||||||
- [ ] `ErpDbContext` + Npgsql wired; `InitialCreate` migration applied
|
- [~] `ErpDbContext` + Npgsql wired; `InitialCreate` migration **created** (`Infra/Persistence/Migrations`, 8 master-data tables) — **not yet applied**: `dotnet ef database update` fails `28P01 password authentication failed for user "postgres"` (local Postgres is running on :5432 but the `postgres/postgres` dev creds in `appsettings.Development.json` don't match this box). Generated SQL script validates cleanly. **Action needed:** set the real local creds, then `ASPNETCORE_ENVIRONMENT=Development dotnet ef database update`.
|
||||||
- [ ] Serilog, JWT, Swagger, HealthChecks, ProblemDetails in `Program.cs`
|
- [x] Serilog, JWT, Swagger, HealthChecks, ProblemDetails in `Program.cs` (JWT bearer *validated*; endpoints not yet `[Authorize]`-gated — see §6 auth note)
|
||||||
- [ ] `IUnitOfWork` + `UnitOfWork` (transaction boundary)
|
- [x] `IUnitOfWork` + `UnitOfWork` (transaction boundary)
|
||||||
- [ ] Generic repository base + interfaces
|
- [x] Generic repository base + interfaces
|
||||||
- [ ] `ICurrentUser` (audit stamp from token `sub`)
|
- [x] `ICurrentUser` (audit stamp from token `sub`)
|
||||||
- [ ] ProblemDetails middleware + domain exception → `code` mapping (System/Errors)
|
- [x] ProblemDetails middleware + domain exception → `code` mapping (System/Errors; full §7 catalog added to `ErrorCodes`)
|
||||||
|
|
||||||
## 1. Master Data
|
## 1. Master Data
|
||||||
- [ ] Item: entity + config + enums (ItemType, TrackingMode)
|
> Code complete for all items below (2026-07-09): entities, EF configs, DTOs, services, controllers — solution builds clean, app boots, and the generated OpenAPI exposes every path in `docs/11 §2`. Marked `[~]` (not `[x]`) because the **security gate** (00-CORE §8) is not yet fully met: the foundational auth control (02-SECURITY B.1) and the audit trail (B.3, the AR-01 compensating control) land in §6, and the schema is not yet applied to a DB. No live DB integration test has run (creds blocker above). Flip to `[x]` once §6 auth+audit are in and endpoints are exercised against Postgres.
|
||||||
- [ ] Item: repository + service + controller (CRUD, DTOs, ETag)
|
- [x] Item: entity + config + enums (ItemType, TrackingMode; EntityStatus added) — `xmin`/RowVersion concurrency token (Npgsql), unique SKU
|
||||||
- [ ] UOM + UOM conversions
|
- [~] Item: repository (generic) + service + controller (CRUD, narrow DTOs, ETag/If-Match→412, SKU_DUPLICATE, reference validation) — server-controlled fields excluded (02-SECURITY C.1)
|
||||||
- [ ] Category (hierarchy, `?tree=true`)
|
- [~] UOM + UOM conversions (`GET/POST /uoms`, `PUT /items/{id}/uom-conversions` full-replace upsert)
|
||||||
- [ ] Vendor
|
- [~] Category (hierarchy, `GET /categories?tree=true` nested build, parent-exists validation)
|
||||||
- [ ] Warehouse + Bin
|
- [~] Vendor (CRUD, ETag/If-Match, unique code, deactivate via `PATCH /vendors/{id}/status`)
|
||||||
- [ ] Item reorder settings (`PUT /items/{id}/reorder`)
|
- [~] Warehouse + Bin (`/warehouses`, nested `/warehouses/{id}/bins`, bin code unique per warehouse)
|
||||||
|
- [~] Item reorder settings (`PUT /items/{id}/reorder` full-replace upsert, warehouse-exists validation)
|
||||||
|
|
||||||
## 2. Procurement
|
## 2. Procurement
|
||||||
- [ ] Requisition (+ lines) + submit
|
- [ ] Requisition (+ lines) + submit
|
||||||
@@ -47,6 +48,7 @@ Convention: `docs/01-DOC-GUIDE.md §6`. Update this file in the **same commit**
|
|||||||
- [ ] Reorder alerts (query) + suggest requisition
|
- [ ] Reorder alerts (query) + suggest requisition
|
||||||
|
|
||||||
## 6. Cross-cutting
|
## 6. Cross-cutting
|
||||||
|
> **Auth-enforcement gap (open):** JWT bearer *validation* is wired, but no token issuer exists yet and controllers are **not** `[Authorize]`-gated, so §1 endpoints are currently open. This is the AR-01/NFR-03 control surface — gate all v1 endpoints (fallback authorization policy) in the same change as `POST /auth/login`, then re-run the 02-SECURITY B.1 checklist and flip §1 items to `[x]`.
|
||||||
- [ ] Audit log on every mutation (who/when/old→new)
|
- [ ] Audit log on every mutation (who/when/old→new)
|
||||||
- [ ] Document numbering sequences (per type, per year)
|
- [ ] Document numbering sequences (per type, per year)
|
||||||
- [ ] Auth: simple in-app login → JWT (`POST /auth/login`)
|
- [ ] Auth: simple in-app login → JWT (`POST /auth/login`)
|
||||||
@@ -61,3 +63,11 @@ Convention: `docs/01-DOC-GUIDE.md §6`. Update this file in the **same commit**
|
|||||||
|
|
||||||
## Done
|
## Done
|
||||||
<!-- move [x] items here with date + note if the active list grows long -->
|
<!-- move [x] items here with date + note if the active list grows long -->
|
||||||
|
|
||||||
|
### 2026-07-09 — Bootstrap verified + Master Data (§1) implemented
|
||||||
|
- Bootstrap scaffolding confirmed against 00-CORE §5 (solution, packages, `Program.cs` wiring, UoW, generic repo, `ICurrentUser`, ProblemDetails handler). Added enum-as-string JSON (`JsonStringEnumConverter`) and registered the 5 master-data services.
|
||||||
|
- Domain: 3 enums (`ItemType`, `TrackingMode`, `EntityStatus`) + 8 entities (Category, Uom, UomConversion, Item, ItemReorder, Vendor, Warehouse, Bin) with one `IEntityTypeConfiguration` each; FKs `Restrict` (masters deactivate, not cascade-delete), unique indexes (SKU, vendor/warehouse code, uom name, bin code per-warehouse), decimal precision, `xmin` concurrency token on Item/Vendor.
|
||||||
|
- API: 5 controllers, lowercase routes matching `docs/11 §2` exactly (verified via generated `swagger.json`). ETag/If-Match (428 if missing, 412 on mismatch), narrow request DTOs (no over-posting), `PagedResponse<T>` list envelope (§1.4), `PageQuery` with pageSize clamp ≤200 (B.6).
|
||||||
|
- Migration `InitialCreate` generated (`xmin` correctly produces no DDL — uses the PG system column).
|
||||||
|
- **Verified:** `dotnet build` clean (0 warn/0 err); app boots (`Now listening… Application started`); `/api/meta` 200; `swagger.json` 200 with all 13 master-data paths; DI resolves controller→service→repo→DbContext (a DB-backed call reaches Npgsql, failing only on creds).
|
||||||
|
- **Blocked / follow-ups:** (1) apply migration — needs real local Postgres creds (see §0 note); (2) auth enforcement + audit trail — §6 (security gate for `[x]`); (3) no `DELETE` master endpoints — `MASTER_IN_USE` code reserved until transaction tables exist (deactivate-only per FR-MD-08).
|
||||||
|
|||||||
Reference in New Issue
Block a user