diff --git a/Backend/ERPCore/Common/Http/ETag.cs b/Backend/ERPCore/Common/Http/ETag.cs
new file mode 100644
index 0000000..711d165
--- /dev/null
+++ b/Backend/ERPCore/Common/Http/ETag.cs
@@ -0,0 +1,37 @@
+namespace ERPCore.Common.Http;
+
+///
+/// Encodes the PostgreSQL xmin concurrency token (a ) as an
+/// opaque, quoted HTTP ETag and parses If-Match values back. Round-trips
+/// via base64 so the value is stable and content-type agnostic
+/// (docs/11-BACKEND-PHASE1.md §1.6).
+///
+public static class ETag
+{
+ /// Quoted ETag string for a row-version token, e.g. "0RsAAA==".
+ public static string From(uint rowVersion)
+ => "\"" + Convert.ToBase64String(BitConverter.GetBytes(rowVersion)) + "\"";
+
+ /// Parse an If-Match header value (quoted, optionally weak) to a token.
+ 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;
+ }
+ }
+}
diff --git a/Backend/ERPCore/Common/Http/ETagged.cs b/Backend/ERPCore/Common/Http/ETagged.cs
new file mode 100644
index 0000000..527cf79
--- /dev/null
+++ b/Backend/ERPCore/Common/Http/ETagged.cs
@@ -0,0 +1,7 @@
+namespace ERPCore.Common.Http;
+
+///
+/// Pairs a response DTO with the aggregate's current row-version so the controller
+/// can emit an ETag header without the token leaking into the JSON body.
+///
+public sealed record ETagged(T Value, uint RowVersion);
diff --git a/Backend/ERPCore/Controllers/ApiControllerBase.cs b/Backend/ERPCore/Controllers/ApiControllerBase.cs
new file mode 100644
index 0000000..ad4d1e3
--- /dev/null
+++ b/Backend/ERPCore/Controllers/ApiControllerBase.cs
@@ -0,0 +1,28 @@
+using ERPCore.Common.Http;
+using ERPCore.System.Errors;
+using Microsoft.AspNetCore.Mvc;
+
+namespace ERPCore.Controllers;
+
+///
+/// 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 [Route] to match
+/// the API contract paths (docs/11 §1.1).
+///
+[ApiController]
+[Produces("application/json")]
+public abstract class ApiControllerBase : ControllerBase
+{
+ /// Parse a mandatory If-Match header, or 428 if absent/malformed.
+ 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;
+ }
+
+ /// Emit the strong ETag response header for a row-version token.
+ protected void SetETag(uint rowVersion) => Response.Headers.ETag = ETag.From(rowVersion);
+}
diff --git a/Backend/ERPCore/Controllers/CategoriesController.cs b/Backend/ERPCore/Controllers/CategoriesController.cs
new file mode 100644
index 0000000..dab7c60
--- /dev/null
+++ b/Backend/ERPCore/Controllers/CategoriesController.cs
@@ -0,0 +1,31 @@
+using ERPCore.Dtos.Categories;
+using ERPCore.Dtos.Common;
+using ERPCore.Services.Interfaces;
+using Microsoft.AspNetCore.Mvc;
+
+namespace ERPCore.Controllers;
+
+/// Category endpoints (docs/11-BACKEND-PHASE1.md §2.3).
+[Route("api/v1/categories")]
+public sealed class CategoriesController : ApiControllerBase
+{
+ private readonly ICategoryService _categories;
+
+ public CategoriesController(ICategoryService categories) => _categories = categories;
+
+ /// Flat paged list, or a nested tree when tree=true.
+ [HttpGet]
+ [ProducesResponseType(typeof(PagedResponse), StatusCodes.Status200OK)]
+ [ProducesResponseType(typeof(IReadOnlyList), StatusCodes.Status200OK)]
+ public async Task 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> Create([FromBody] CreateCategoryRequest request, CancellationToken ct)
+ {
+ var dto = await _categories.CreateAsync(request, ct);
+ return Created($"/api/v1/categories/{dto.CategoryId}", dto);
+ }
+}
diff --git a/Backend/ERPCore/Controllers/ItemsController.cs b/Backend/ERPCore/Controllers/ItemsController.cs
new file mode 100644
index 0000000..eab6c79
--- /dev/null
+++ b/Backend/ERPCore/Controllers/ItemsController.cs
@@ -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;
+
+/// Item master endpoints (docs/11-BACKEND-PHASE1.md §2.1–2.2).
+[Route("api/v1/items")]
+public sealed class ItemsController : ApiControllerBase
+{
+ private readonly IItemService _items;
+
+ public ItemsController(IItemService items) => _items = items;
+
+ /// List items with optional filters and paging.
+ [HttpGet]
+ [ProducesResponseType(typeof(PagedResponse), StatusCodes.Status200OK)]
+ public async Task>> List(
+ [FromQuery] PageQuery query,
+ [FromQuery] EntityStatus? status,
+ [FromQuery] long? categoryId,
+ [FromQuery] TrackingMode? trackingMode,
+ CancellationToken ct)
+ => Ok(await _items.ListAsync(query, status, categoryId, trackingMode, ct));
+
+ /// Get a single item; returns an ETag for optimistic concurrency.
+ [HttpGet("{itemId:long}")]
+ [ProducesResponseType(typeof(ItemDetailDto), StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ public async Task> 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);
+ }
+
+ /// Create an item (SKU unique). Server sets status and timestamps.
+ [HttpPost]
+ [ProducesResponseType(typeof(ItemDetailDto), StatusCodes.Status201Created)]
+ [ProducesResponseType(StatusCodes.Status400BadRequest)]
+ [ProducesResponseType(StatusCodes.Status422UnprocessableEntity)]
+ public async Task> 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);
+ }
+
+ /// Full update; requires If-Match (412 on stale ETag).
+ [HttpPut("{itemId:long}")]
+ [ProducesResponseType(typeof(ItemDetailDto), StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ [ProducesResponseType(StatusCodes.Status412PreconditionFailed)]
+ public async Task> 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);
+ }
+
+ /// Activate / deactivate the item (FR-MD-08 — deactivate, not delete).
+ [HttpPatch("{itemId:long}/status")]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ public async Task SetStatus(long itemId, [FromBody] UpdateItemStatusRequest request, CancellationToken ct)
+ {
+ await _items.SetStatusAsync(itemId, request.Status, ct);
+ return NoContent();
+ }
+
+ /// Replace the item's per-warehouse reorder settings (FR-MD-05).
+ [HttpPut("{itemId:long}/reorder")]
+ [ProducesResponseType(typeof(ItemReorderSettingsDto), StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ public async Task> UpdateReorder(long itemId, [FromBody] UpdateReorderRequest request, CancellationToken ct)
+ => Ok(await _items.UpdateReorderAsync(itemId, request, ct));
+
+ /// Replace the item's UOM conversions (FR-MD-02).
+ [HttpPut("{itemId:long}/uom-conversions")]
+ [ProducesResponseType(typeof(ItemUomConversionsDto), StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ public async Task> UpdateUomConversions(long itemId, [FromBody] UpdateUomConversionsRequest request, CancellationToken ct)
+ => Ok(await _items.UpdateUomConversionsAsync(itemId, request, ct));
+}
diff --git a/Backend/ERPCore/Controllers/UomsController.cs b/Backend/ERPCore/Controllers/UomsController.cs
new file mode 100644
index 0000000..55219dc
--- /dev/null
+++ b/Backend/ERPCore/Controllers/UomsController.cs
@@ -0,0 +1,29 @@
+using ERPCore.Dtos.Common;
+using ERPCore.Dtos.Uoms;
+using ERPCore.Services.Interfaces;
+using Microsoft.AspNetCore.Mvc;
+
+namespace ERPCore.Controllers;
+
+/// Unit-of-measure endpoints (docs/11-BACKEND-PHASE1.md §2.2).
+[Route("api/v1/uoms")]
+public sealed class UomsController : ApiControllerBase
+{
+ private readonly IUomService _uoms;
+
+ public UomsController(IUomService uoms) => _uoms = uoms;
+
+ [HttpGet]
+ [ProducesResponseType(typeof(PagedResponse), StatusCodes.Status200OK)]
+ public async Task>> List([FromQuery] PageQuery query, CancellationToken ct)
+ => Ok(await _uoms.ListAsync(query, ct));
+
+ [HttpPost]
+ [ProducesResponseType(typeof(UomDto), StatusCodes.Status201Created)]
+ [ProducesResponseType(StatusCodes.Status409Conflict)]
+ public async Task> Create([FromBody] CreateUomRequest request, CancellationToken ct)
+ {
+ var dto = await _uoms.CreateAsync(request, ct);
+ return Created($"/api/v1/uoms/{dto.UomId}", dto);
+ }
+}
diff --git a/Backend/ERPCore/Controllers/VendorsController.cs b/Backend/ERPCore/Controllers/VendorsController.cs
new file mode 100644
index 0000000..40b17b3
--- /dev/null
+++ b/Backend/ERPCore/Controllers/VendorsController.cs
@@ -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;
+
+/// Vendor master endpoints (docs/11-BACKEND-PHASE1.md §2.4).
+[Route("api/v1/vendors")]
+public sealed class VendorsController : ApiControllerBase
+{
+ private readonly IVendorService _vendors;
+
+ public VendorsController(IVendorService vendors) => _vendors = vendors;
+
+ [HttpGet]
+ [ProducesResponseType(typeof(PagedResponse), StatusCodes.Status200OK)]
+ public async Task>> 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> 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> 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> 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 SetStatus(long vendorId, [FromBody] UpdateVendorStatusRequest request, CancellationToken ct)
+ {
+ await _vendors.SetStatusAsync(vendorId, request.Status, ct);
+ return NoContent();
+ }
+}
diff --git a/Backend/ERPCore/Controllers/WarehousesController.cs b/Backend/ERPCore/Controllers/WarehousesController.cs
new file mode 100644
index 0000000..4c2eb5c
--- /dev/null
+++ b/Backend/ERPCore/Controllers/WarehousesController.cs
@@ -0,0 +1,54 @@
+using ERPCore.Dtos.Common;
+using ERPCore.Dtos.Warehouses;
+using ERPCore.Services.Interfaces;
+using Microsoft.AspNetCore.Mvc;
+
+namespace ERPCore.Controllers;
+
+/// Warehouse & bin endpoints (docs/11-BACKEND-PHASE1.md §2.5).
+[Route("api/v1/warehouses")]
+public sealed class WarehousesController : ApiControllerBase
+{
+ private readonly IWarehouseService _warehouses;
+
+ public WarehousesController(IWarehouseService warehouses) => _warehouses = warehouses;
+
+ [HttpGet]
+ [ProducesResponseType(typeof(PagedResponse), StatusCodes.Status200OK)]
+ public async Task>> 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> 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> 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), StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ public async Task>> 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> 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);
+ }
+}
diff --git a/Backend/ERPCore/Domain/Entities/Bin.cs b/Backend/ERPCore/Domain/Entities/Bin.cs
new file mode 100644
index 0000000..331fce1
--- /dev/null
+++ b/Backend/ERPCore/Domain/Entities/Bin.cs
@@ -0,0 +1,16 @@
+namespace ERPCore.Domain.Entities;
+
+///
+/// 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.
+///
+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; }
+}
diff --git a/Backend/ERPCore/Domain/Entities/Category.cs b/Backend/ERPCore/Domain/Entities/Category.cs
new file mode 100644
index 0000000..5285373
--- /dev/null
+++ b/Backend/ERPCore/Domain/Entities/Category.cs
@@ -0,0 +1,15 @@
+namespace ERPCore.Domain.Entities;
+
+///
+/// Hierarchical item category (FR-MD-04). A null denotes a
+/// root category. Model: docs/10-BACKEND-PHASE1.md Part C.1.
+///
+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 Children { get; set; } = new List();
+}
diff --git a/Backend/ERPCore/Domain/Entities/Item.cs b/Backend/ERPCore/Domain/Entities/Item.cs
new file mode 100644
index 0000000..4083f57
--- /dev/null
+++ b/Backend/ERPCore/Domain/Entities/Item.cs
@@ -0,0 +1,39 @@
+using ERPCore.Domain.Enums;
+
+namespace ERPCore.Domain.Entities;
+
+///
+/// Item master (FR-MD-01). Mutable aggregate: carries a
+/// concurrency token surfaced as an ETag (docs/10 Part C.10). SKU is unique.
+/// Model: docs/10-BACKEND-PHASE1.md Part C.1.
+///
+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; }
+
+ /// PostgreSQL xmin-backed optimistic concurrency token (ETag source).
+ public uint RowVersion { get; set; }
+
+ public ICollection ReorderSettings { get; set; } = new List();
+ public ICollection UomConversions { get; set; } = new List();
+}
diff --git a/Backend/ERPCore/Domain/Entities/ItemReorder.cs b/Backend/ERPCore/Domain/Entities/ItemReorder.cs
new file mode 100644
index 0000000..8a3ecfe
--- /dev/null
+++ b/Backend/ERPCore/Domain/Entities/ItemReorder.cs
@@ -0,0 +1,20 @@
+namespace ERPCore.Domain.Entities;
+
+///
+/// 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.
+///
+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; }
+}
diff --git a/Backend/ERPCore/Domain/Entities/Uom.cs b/Backend/ERPCore/Domain/Entities/Uom.cs
new file mode 100644
index 0000000..2200e55
--- /dev/null
+++ b/Backend/ERPCore/Domain/Entities/Uom.cs
@@ -0,0 +1,11 @@
+namespace ERPCore.Domain.Entities;
+
+///
+/// Unit of Measure (FR-MD-02). Referenced as an item's base UOM and as the
+/// endpoints of a . Model: docs/10-BACKEND-PHASE1.md Part C.1.
+///
+public class Uom
+{
+ public long UomId { get; set; }
+ public string Name { get; set; } = string.Empty;
+}
diff --git a/Backend/ERPCore/Domain/Entities/UomConversion.cs b/Backend/ERPCore/Domain/Entities/UomConversion.cs
new file mode 100644
index 0000000..fa76ef4
--- /dev/null
+++ b/Backend/ERPCore/Domain/Entities/UomConversion.cs
@@ -0,0 +1,22 @@
+namespace ERPCore.Domain.Entities;
+
+///
+/// Per-item conversion factor between two UOMs (FR-MD-02/03): quantity in
+/// × = quantity in .
+/// Model: docs/10-BACKEND-PHASE1.md Part C.1.
+///
+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; }
+}
diff --git a/Backend/ERPCore/Domain/Entities/Vendor.cs b/Backend/ERPCore/Domain/Entities/Vendor.cs
new file mode 100644
index 0000000..22d1e09
--- /dev/null
+++ b/Backend/ERPCore/Domain/Entities/Vendor.cs
@@ -0,0 +1,25 @@
+using ERPCore.Domain.Enums;
+
+namespace ERPCore.Domain.Entities;
+
+///
+/// Vendor master (FR-MD-06). Mutable aggregate with a
+/// ETag token. Deactivated, not deleted, when referenced (FR-MD-08).
+/// Model: docs/10-BACKEND-PHASE1.md Part C.1.
+///
+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; }
+
+ /// PostgreSQL xmin-backed optimistic concurrency token (ETag source).
+ public uint RowVersion { get; set; }
+}
diff --git a/Backend/ERPCore/Domain/Entities/Warehouse.cs b/Backend/ERPCore/Domain/Entities/Warehouse.cs
new file mode 100644
index 0000000..fe7a402
--- /dev/null
+++ b/Backend/ERPCore/Domain/Entities/Warehouse.cs
@@ -0,0 +1,14 @@
+namespace ERPCore.Domain.Entities;
+
+///
+/// Warehouse master (FR-MD-07, FR-WH-01). Owns a bin/location hierarchy.
+/// Model: docs/10-BACKEND-PHASE1.md Part C.1.
+///
+public class Warehouse
+{
+ public long WarehouseId { get; set; }
+ public string Code { get; set; } = string.Empty;
+ public string Name { get; set; } = string.Empty;
+
+ public ICollection Bins { get; set; } = new List();
+}
diff --git a/Backend/ERPCore/Domain/Enums/EntityStatus.cs b/Backend/ERPCore/Domain/Enums/EntityStatus.cs
new file mode 100644
index 0000000..1a6c807
--- /dev/null
+++ b/Backend/ERPCore/Domain/Enums/EntityStatus.cs
@@ -0,0 +1,12 @@
+namespace ERPCore.Domain.Enums;
+
+///
+/// Lifecycle status for deactivatable master data (Item, Vendor). Masters are
+/// never hard-deleted while referenced — they are set
+/// instead (FR-MD-08). Stored as a string.
+///
+public enum EntityStatus
+{
+ Active,
+ Inactive
+}
diff --git a/Backend/ERPCore/Domain/Enums/ItemType.cs b/Backend/ERPCore/Domain/Enums/ItemType.cs
new file mode 100644
index 0000000..c81a1d6
--- /dev/null
+++ b/Backend/ERPCore/Domain/Enums/ItemType.cs
@@ -0,0 +1,12 @@
+namespace ERPCore.Domain.Enums;
+
+///
+/// Item classification (FR-MD-01). Values match the itemType enum in
+/// docs/11-BACKEND-PHASE1.md §8. Stored as a string in the database.
+///
+public enum ItemType
+{
+ Stocked,
+ NonStocked,
+ Service
+}
diff --git a/Backend/ERPCore/Domain/Enums/TrackingMode.cs b/Backend/ERPCore/Domain/Enums/TrackingMode.cs
new file mode 100644
index 0000000..23af2e4
--- /dev/null
+++ b/Backend/ERPCore/Domain/Enums/TrackingMode.cs
@@ -0,0 +1,12 @@
+namespace ERPCore.Domain.Enums;
+
+///
+/// How on-hand units of an item are individually tracked (FR-MD-01). Values match
+/// the trackingMode enum in docs/11-BACKEND-PHASE1.md §8. Stored as a string.
+///
+public enum TrackingMode
+{
+ None,
+ Batch,
+ Serial
+}
diff --git a/Backend/ERPCore/Dtos/Categories/CategoryDtos.cs b/Backend/ERPCore/Dtos/Categories/CategoryDtos.cs
new file mode 100644
index 0000000..856e23b
--- /dev/null
+++ b/Backend/ERPCore/Dtos/Categories/CategoryDtos.cs
@@ -0,0 +1,15 @@
+using System.ComponentModel.DataAnnotations;
+
+namespace ERPCore.Dtos.Categories;
+
+/// Flat category resource (docs/11-BACKEND-PHASE1.md §2.3).
+public sealed record CategoryDto(long CategoryId, string Name, long? ParentId);
+
+/// Nested category node for GET /categories?tree=true.
+public sealed record CategoryTreeDto(long CategoryId, string Name, long? ParentId, IReadOnlyList Children);
+
+public sealed class CreateCategoryRequest
+{
+ [Required, StringLength(200)] public string Name { get; set; } = string.Empty;
+ public long? ParentId { get; set; }
+}
diff --git a/Backend/ERPCore/Dtos/Common/PageQuery.cs b/Backend/ERPCore/Dtos/Common/PageQuery.cs
new file mode 100644
index 0000000..639ce34
--- /dev/null
+++ b/Backend/ERPCore/Dtos/Common/PageQuery.cs
@@ -0,0 +1,37 @@
+namespace ERPCore.Dtos.Common;
+
+///
+/// Shared paging/sorting query binding (docs/11-BACKEND-PHASE1.md §1.5). Page size
+/// is clamped to to enforce pagination bounds
+/// (02-SECURITY B.6). Bind from the query string on list endpoints.
+///
+public class PageQuery
+{
+ public const int MaxPageSize = 200;
+ public const int DefaultPageSize = 20;
+
+ private int _page = 1;
+ private int _pageSize = DefaultPageSize;
+
+ /// 1-based page number (default 1).
+ public int Page
+ {
+ get => _page;
+ set => _page = value < 1 ? 1 : value;
+ }
+
+ /// Page size (default 20, clamped to 1..200).
+ public int PageSize
+ {
+ get => _pageSize;
+ set => _pageSize = value < 1 ? DefaultPageSize : Math.Min(value, MaxPageSize);
+ }
+
+ /// Free-text search term (q).
+ public string? Q { get; set; }
+
+ /// Sort spec, e.g. name or -createdAt.
+ public string? Sort { get; set; }
+
+ public int Skip => (Page - 1) * PageSize;
+}
diff --git a/Backend/ERPCore/Dtos/Common/PagedResponse.cs b/Backend/ERPCore/Dtos/Common/PagedResponse.cs
new file mode 100644
index 0000000..889fb97
--- /dev/null
+++ b/Backend/ERPCore/Dtos/Common/PagedResponse.cs
@@ -0,0 +1,17 @@
+namespace ERPCore.Dtos.Common;
+
+///
+/// List envelope matching docs/11-BACKEND-PHASE1.md §1.4:
+/// { "items": [...], "pagination": { page, pageSize, totalItems, totalPages } }.
+///
+public sealed record PagedResponse(IReadOnlyList Items, PaginationDto Pagination)
+{
+ public static PagedResponse Create(IReadOnlyList items, int page, int pageSize, int totalItems)
+ {
+ var totalPages = pageSize <= 0 ? 0 : (int)Math.Ceiling(totalItems / (double)pageSize);
+ return new PagedResponse(items, new PaginationDto(page, pageSize, totalItems, totalPages));
+ }
+}
+
+/// Pagination metadata block (docs/11 §1.4).
+public sealed record PaginationDto(int Page, int PageSize, int TotalItems, int TotalPages);
diff --git a/Backend/ERPCore/Dtos/Items/ItemDtos.cs b/Backend/ERPCore/Dtos/Items/ItemDtos.cs
new file mode 100644
index 0000000..c372cad
--- /dev/null
+++ b/Backend/ERPCore/Dtos/Items/ItemDtos.cs
@@ -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) --------------------------------
+
+/// Row shape for GET /items.
+public sealed record ItemListItemDto(
+ long ItemId, string Sku, string Name, long CategoryId, long BaseUomId,
+ long? DefaultVendorId, ItemType ItemType, TrackingMode TrackingMode,
+ string? TaxClass, EntityStatus Status);
+
+/// A single per-warehouse reorder policy row.
+public sealed record ItemReorderDto(long WarehouseId, decimal ReorderPoint, decimal ReorderQty);
+
+/// Full item resource for GET /items/{id} and create/update responses.
+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 Reorder,
+ DateTime CreatedAt, DateTime? UpdatedAt);
+
+/// UOM conversion row (docs/11 §2.2).
+public sealed record UomConversionDto(long ConversionId, long FromUom, long ToUom, decimal Factor);
+
+/// Response body for PUT /items/{id}/uom-conversions.
+public sealed record ItemUomConversionsDto(long ItemId, long BaseUomId, IReadOnlyList Conversions);
+
+/// Response body for PUT /items/{id}/reorder.
+public sealed record ItemReorderSettingsDto(IReadOnlyList 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 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 Conversions { get; set; } = new();
+}
diff --git a/Backend/ERPCore/Dtos/Uoms/UomDtos.cs b/Backend/ERPCore/Dtos/Uoms/UomDtos.cs
new file mode 100644
index 0000000..90f5259
--- /dev/null
+++ b/Backend/ERPCore/Dtos/Uoms/UomDtos.cs
@@ -0,0 +1,11 @@
+using System.ComponentModel.DataAnnotations;
+
+namespace ERPCore.Dtos.Uoms;
+
+/// UOM resource (docs/11-BACKEND-PHASE1.md §2.2).
+public sealed record UomDto(long UomId, string Name);
+
+public sealed class CreateUomRequest
+{
+ [Required, StringLength(50)] public string Name { get; set; } = string.Empty;
+}
diff --git a/Backend/ERPCore/Dtos/Vendors/VendorDtos.cs b/Backend/ERPCore/Dtos/Vendors/VendorDtos.cs
new file mode 100644
index 0000000..731099c
--- /dev/null
+++ b/Backend/ERPCore/Dtos/Vendors/VendorDtos.cs
@@ -0,0 +1,32 @@
+using System.ComponentModel.DataAnnotations;
+using ERPCore.Domain.Enums;
+
+namespace ERPCore.Dtos.Vendors;
+
+/// Vendor resource (docs/11-BACKEND-PHASE1.md §2.4).
+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; }
+}
diff --git a/Backend/ERPCore/Dtos/Warehouses/WarehouseDtos.cs b/Backend/ERPCore/Dtos/Warehouses/WarehouseDtos.cs
new file mode 100644
index 0000000..ad91678
--- /dev/null
+++ b/Backend/ERPCore/Dtos/Warehouses/WarehouseDtos.cs
@@ -0,0 +1,21 @@
+using System.ComponentModel.DataAnnotations;
+
+namespace ERPCore.Dtos.Warehouses;
+
+/// Warehouse resource (docs/11-BACKEND-PHASE1.md §2.5).
+public sealed record WarehouseDto(long WarehouseId, string Code, string Name);
+
+/// Bin/location resource (docs/11 §2.5).
+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; }
+}
diff --git a/Backend/ERPCore/Infra/Persistence/Configurations/BinConfiguration.cs b/Backend/ERPCore/Infra/Persistence/Configurations/BinConfiguration.cs
new file mode 100644
index 0000000..89b5aa8
--- /dev/null
+++ b/Backend/ERPCore/Infra/Persistence/Configurations/BinConfiguration.cs
@@ -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
+{
+ public void Configure(EntityTypeBuilder 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();
+ }
+}
diff --git a/Backend/ERPCore/Infra/Persistence/Configurations/CategoryConfiguration.cs b/Backend/ERPCore/Infra/Persistence/Configurations/CategoryConfiguration.cs
new file mode 100644
index 0000000..a0565cb
--- /dev/null
+++ b/Backend/ERPCore/Infra/Persistence/Configurations/CategoryConfiguration.cs
@@ -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
+{
+ public void Configure(EntityTypeBuilder 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);
+ }
+}
diff --git a/Backend/ERPCore/Infra/Persistence/Configurations/ItemConfiguration.cs b/Backend/ERPCore/Infra/Persistence/Configurations/ItemConfiguration.cs
new file mode 100644
index 0000000..4a74f03
--- /dev/null
+++ b/Backend/ERPCore/Infra/Persistence/Configurations/ItemConfiguration.cs
@@ -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-
+{
+ public void Configure(EntityTypeBuilder
- 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().HasMaxLength(20).IsRequired();
+ builder.Property(i => i.TrackingMode)
+ .HasConversion().HasMaxLength(20).IsRequired();
+ builder.Property(i => i.Status)
+ .HasConversion().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);
+ }
+}
diff --git a/Backend/ERPCore/Infra/Persistence/Configurations/ItemReorderConfiguration.cs b/Backend/ERPCore/Infra/Persistence/Configurations/ItemReorderConfiguration.cs
new file mode 100644
index 0000000..ceca4bf
--- /dev/null
+++ b/Backend/ERPCore/Infra/Persistence/Configurations/ItemReorderConfiguration.cs
@@ -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
+{
+ public void Configure(EntityTypeBuilder 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();
+ }
+}
diff --git a/Backend/ERPCore/Infra/Persistence/Configurations/UomConfiguration.cs b/Backend/ERPCore/Infra/Persistence/Configurations/UomConfiguration.cs
new file mode 100644
index 0000000..3425885
--- /dev/null
+++ b/Backend/ERPCore/Infra/Persistence/Configurations/UomConfiguration.cs
@@ -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
+{
+ public void Configure(EntityTypeBuilder builder)
+ {
+ builder.ToTable("uoms");
+ builder.HasKey(u => u.UomId);
+
+ builder.Property(u => u.Name).IsRequired().HasMaxLength(50);
+ builder.HasIndex(u => u.Name).IsUnique();
+ }
+}
diff --git a/Backend/ERPCore/Infra/Persistence/Configurations/UomConversionConfiguration.cs b/Backend/ERPCore/Infra/Persistence/Configurations/UomConversionConfiguration.cs
new file mode 100644
index 0000000..15445d4
--- /dev/null
+++ b/Backend/ERPCore/Infra/Persistence/Configurations/UomConversionConfiguration.cs
@@ -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
+{
+ public void Configure(EntityTypeBuilder 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();
+ }
+}
diff --git a/Backend/ERPCore/Infra/Persistence/Configurations/VendorConfiguration.cs b/Backend/ERPCore/Infra/Persistence/Configurations/VendorConfiguration.cs
new file mode 100644
index 0000000..b8dffed
--- /dev/null
+++ b/Backend/ERPCore/Infra/Persistence/Configurations/VendorConfiguration.cs
@@ -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
+{
+ public void Configure(EntityTypeBuilder 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().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);
+ }
+}
diff --git a/Backend/ERPCore/Infra/Persistence/Configurations/WarehouseConfiguration.cs b/Backend/ERPCore/Infra/Persistence/Configurations/WarehouseConfiguration.cs
new file mode 100644
index 0000000..acbcfad
--- /dev/null
+++ b/Backend/ERPCore/Infra/Persistence/Configurations/WarehouseConfiguration.cs
@@ -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
+{
+ public void Configure(EntityTypeBuilder 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);
+ }
+}
diff --git a/Backend/ERPCore/Infra/Persistence/ErpDbContext.cs b/Backend/ERPCore/Infra/Persistence/ErpDbContext.cs
index e9368c9..e55b00f 100644
--- a/Backend/ERPCore/Infra/Persistence/ErpDbContext.cs
+++ b/Backend/ERPCore/Infra/Persistence/ErpDbContext.cs
@@ -1,3 +1,4 @@
+using ERPCore.Domain.Entities;
using Microsoft.EntityFrameworkCore;
namespace ERPCore.Infra.Persistence;
@@ -14,6 +15,16 @@ public class ErpDbContext : DbContext
{
}
+ // --- Master Data (docs/10 Part C.1) ---
+ public DbSet Categories => Set();
+ public DbSet Uoms => Set();
+ public DbSet UomConversions => Set();
+ public DbSet
- Items => Set
- ();
+ public DbSet ItemReorders => Set();
+ public DbSet Vendors => Set();
+ public DbSet Warehouses => Set();
+ public DbSet Bins => Set();
+
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
diff --git a/Backend/ERPCore/Infra/Persistence/Migrations/20260709095653_InitialCreate.Designer.cs b/Backend/ERPCore/Infra/Persistence/Migrations/20260709095653_InitialCreate.Designer.cs
new file mode 100644
index 0000000..4d6fe75
--- /dev/null
+++ b/Backend/ERPCore/Infra/Persistence/Migrations/20260709095653_InitialCreate.Designer.cs
@@ -0,0 +1,445 @@
+//
+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
+ {
+ ///
+ 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("BinId")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("bigint");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("BinId"));
+
+ b.Property("BinType")
+ .HasMaxLength(50)
+ .HasColumnType("character varying(50)");
+
+ b.Property("Code")
+ .IsRequired()
+ .HasMaxLength(50)
+ .HasColumnType("character varying(50)");
+
+ b.Property("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("CategoryId")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("bigint");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("CategoryId"));
+
+ b.Property("Name")
+ .IsRequired()
+ .HasMaxLength(200)
+ .HasColumnType("character varying(200)");
+
+ b.Property("ParentId")
+ .HasColumnType("bigint");
+
+ b.HasKey("CategoryId");
+
+ b.HasIndex("ParentId");
+
+ b.ToTable("categories", (string)null);
+ });
+
+ modelBuilder.Entity("ERPCore.Domain.Entities.Item", b =>
+ {
+ b.Property("ItemId")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("bigint");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("ItemId"));
+
+ b.Property("BaseUomId")
+ .HasColumnType("bigint");
+
+ b.Property("CategoryId")
+ .HasColumnType("bigint");
+
+ b.Property("CreatedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("DefaultVendorId")
+ .HasColumnType("bigint");
+
+ b.Property("Description")
+ .HasMaxLength(1000)
+ .HasColumnType("character varying(1000)");
+
+ b.Property("ItemType")
+ .IsRequired()
+ .HasMaxLength(20)
+ .HasColumnType("character varying(20)");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasMaxLength(200)
+ .HasColumnType("character varying(200)");
+
+ b.Property("RowVersion")
+ .IsConcurrencyToken()
+ .ValueGeneratedOnAddOrUpdate()
+ .HasColumnType("xid")
+ .HasColumnName("xmin");
+
+ b.Property("Sku")
+ .IsRequired()
+ .HasMaxLength(50)
+ .HasColumnType("character varying(50)");
+
+ b.Property("Status")
+ .IsRequired()
+ .ValueGeneratedOnAdd()
+ .HasMaxLength(20)
+ .HasColumnType("character varying(20)")
+ .HasDefaultValue("Active");
+
+ b.Property("TaxClass")
+ .HasMaxLength(20)
+ .HasColumnType("character varying(20)");
+
+ b.Property("TrackingMode")
+ .IsRequired()
+ .HasMaxLength(20)
+ .HasColumnType("character varying(20)");
+
+ b.Property("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("ReorderId")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("bigint");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("ReorderId"));
+
+ b.Property("ItemId")
+ .HasColumnType("bigint");
+
+ b.Property("ReorderPoint")
+ .HasPrecision(18, 4)
+ .HasColumnType("numeric(18,4)");
+
+ b.Property("ReorderQty")
+ .HasPrecision(18, 4)
+ .HasColumnType("numeric(18,4)");
+
+ b.Property("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("UomId")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("bigint");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("UomId"));
+
+ b.Property("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("ConversionId")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("bigint");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("ConversionId"));
+
+ b.Property("Factor")
+ .HasPrecision(18, 6)
+ .HasColumnType("numeric(18,6)");
+
+ b.Property("FromUomId")
+ .HasColumnType("bigint");
+
+ b.Property("ItemId")
+ .HasColumnType("bigint");
+
+ b.Property("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("VendorId")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("bigint");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("VendorId"));
+
+ b.Property("Code")
+ .IsRequired()
+ .HasMaxLength(50)
+ .HasColumnType("character varying(50)");
+
+ b.Property("CreatedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("Currency")
+ .IsRequired()
+ .ValueGeneratedOnAdd()
+ .HasMaxLength(3)
+ .HasColumnType("character varying(3)")
+ .HasDefaultValue("LKR");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasMaxLength(200)
+ .HasColumnType("character varying(200)");
+
+ b.Property("RowVersion")
+ .IsConcurrencyToken()
+ .ValueGeneratedOnAddOrUpdate()
+ .HasColumnType("xid")
+ .HasColumnName("xmin");
+
+ b.Property("Status")
+ .IsRequired()
+ .ValueGeneratedOnAdd()
+ .HasMaxLength(20)
+ .HasColumnType("character varying(20)")
+ .HasDefaultValue("Active");
+
+ b.Property("TaxReg")
+ .HasMaxLength(50)
+ .HasColumnType("character varying(50)");
+
+ b.Property("Terms")
+ .HasMaxLength(50)
+ .HasColumnType("character varying(50)");
+
+ b.Property("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("WarehouseId")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("bigint");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("WarehouseId"));
+
+ b.Property("Code")
+ .IsRequired()
+ .HasMaxLength(50)
+ .HasColumnType("character varying(50)");
+
+ b.Property("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
+ }
+ }
+}
diff --git a/Backend/ERPCore/Infra/Persistence/Migrations/20260709095653_InitialCreate.cs b/Backend/ERPCore/Infra/Persistence/Migrations/20260709095653_InitialCreate.cs
new file mode 100644
index 0000000..7ba3005
--- /dev/null
+++ b/Backend/ERPCore/Infra/Persistence/Migrations/20260709095653_InitialCreate.cs
@@ -0,0 +1,325 @@
+using System;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
+
+#nullable disable
+
+namespace ERPCore.Infra.Persistence.Migrations
+{
+ ///
+ public partial class InitialCreate : Migration
+ {
+ ///
+ protected override void Up(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.CreateTable(
+ name: "categories",
+ columns: table => new
+ {
+ CategoryId = table.Column(type: "bigint", nullable: false)
+ .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
+ Name = table.Column(type: "character varying(200)", maxLength: 200, nullable: false),
+ ParentId = table.Column(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(type: "bigint", nullable: false)
+ .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
+ Name = table.Column(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(type: "bigint", nullable: false)
+ .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
+ Code = table.Column(type: "character varying(50)", maxLength: 50, nullable: false),
+ Name = table.Column(type: "character varying(200)", maxLength: 200, nullable: false),
+ Terms = table.Column(type: "character varying(50)", maxLength: 50, nullable: true),
+ TaxReg = table.Column(type: "character varying(50)", maxLength: 50, nullable: true),
+ Currency = table.Column(type: "character varying(3)", maxLength: 3, nullable: false, defaultValue: "LKR"),
+ Status = table.Column(type: "character varying(20)", maxLength: 20, nullable: false, defaultValue: "Active"),
+ CreatedAt = table.Column(type: "timestamp with time zone", nullable: false),
+ UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true),
+ xmin = table.Column(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(type: "bigint", nullable: false)
+ .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
+ Code = table.Column(type: "character varying(50)", maxLength: 50, nullable: false),
+ Name = table.Column(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(type: "bigint", nullable: false)
+ .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
+ Sku = table.Column(type: "character varying(50)", maxLength: 50, nullable: false),
+ Name = table.Column(type: "character varying(200)", maxLength: 200, nullable: false),
+ Description = table.Column(type: "character varying(1000)", maxLength: 1000, nullable: true),
+ CategoryId = table.Column(type: "bigint", nullable: false),
+ BaseUomId = table.Column(type: "bigint", nullable: false),
+ DefaultVendorId = table.Column(type: "bigint", nullable: true),
+ ItemType = table.Column(type: "character varying(20)", maxLength: 20, nullable: false),
+ TrackingMode = table.Column(type: "character varying(20)", maxLength: 20, nullable: false),
+ TaxClass = table.Column(type: "character varying(20)", maxLength: 20, nullable: true),
+ Status = table.Column(type: "character varying(20)", maxLength: 20, nullable: false, defaultValue: "Active"),
+ CreatedAt = table.Column(type: "timestamp with time zone", nullable: false),
+ UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true),
+ xmin = table.Column(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(type: "bigint", nullable: false)
+ .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
+ WarehouseId = table.Column(type: "bigint", nullable: false),
+ Code = table.Column(type: "character varying(50)", maxLength: 50, nullable: false),
+ BinType = table.Column(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(type: "bigint", nullable: false)
+ .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
+ ItemId = table.Column(type: "bigint", nullable: false),
+ WarehouseId = table.Column(type: "bigint", nullable: false),
+ ReorderPoint = table.Column(type: "numeric(18,4)", precision: 18, scale: 4, nullable: false),
+ ReorderQty = table.Column(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(type: "bigint", nullable: false)
+ .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
+ ItemId = table.Column(type: "bigint", nullable: false),
+ FromUomId = table.Column(type: "bigint", nullable: false),
+ ToUomId = table.Column(type: "bigint", nullable: false),
+ Factor = table.Column(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);
+ }
+
+ ///
+ 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");
+ }
+ }
+}
diff --git a/Backend/ERPCore/Infra/Persistence/Migrations/20260709124415_initial.Designer.cs b/Backend/ERPCore/Infra/Persistence/Migrations/20260709124415_initial.Designer.cs
new file mode 100644
index 0000000..ee80b14
--- /dev/null
+++ b/Backend/ERPCore/Infra/Persistence/Migrations/20260709124415_initial.Designer.cs
@@ -0,0 +1,445 @@
+//
+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
+ {
+ ///
+ 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("BinId")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("bigint");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("BinId"));
+
+ b.Property("BinType")
+ .HasMaxLength(50)
+ .HasColumnType("character varying(50)");
+
+ b.Property("Code")
+ .IsRequired()
+ .HasMaxLength(50)
+ .HasColumnType("character varying(50)");
+
+ b.Property("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("CategoryId")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("bigint");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("CategoryId"));
+
+ b.Property("Name")
+ .IsRequired()
+ .HasMaxLength(200)
+ .HasColumnType("character varying(200)");
+
+ b.Property("ParentId")
+ .HasColumnType("bigint");
+
+ b.HasKey("CategoryId");
+
+ b.HasIndex("ParentId");
+
+ b.ToTable("categories", (string)null);
+ });
+
+ modelBuilder.Entity("ERPCore.Domain.Entities.Item", b =>
+ {
+ b.Property("ItemId")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("bigint");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("ItemId"));
+
+ b.Property("BaseUomId")
+ .HasColumnType("bigint");
+
+ b.Property("CategoryId")
+ .HasColumnType("bigint");
+
+ b.Property("CreatedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("DefaultVendorId")
+ .HasColumnType("bigint");
+
+ b.Property("Description")
+ .HasMaxLength(1000)
+ .HasColumnType("character varying(1000)");
+
+ b.Property("ItemType")
+ .IsRequired()
+ .HasMaxLength(20)
+ .HasColumnType("character varying(20)");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasMaxLength(200)
+ .HasColumnType("character varying(200)");
+
+ b.Property("RowVersion")
+ .IsConcurrencyToken()
+ .ValueGeneratedOnAddOrUpdate()
+ .HasColumnType("xid")
+ .HasColumnName("xmin");
+
+ b.Property("Sku")
+ .IsRequired()
+ .HasMaxLength(50)
+ .HasColumnType("character varying(50)");
+
+ b.Property("Status")
+ .IsRequired()
+ .ValueGeneratedOnAdd()
+ .HasMaxLength(20)
+ .HasColumnType("character varying(20)")
+ .HasDefaultValue("Active");
+
+ b.Property("TaxClass")
+ .HasMaxLength(20)
+ .HasColumnType("character varying(20)");
+
+ b.Property("TrackingMode")
+ .IsRequired()
+ .HasMaxLength(20)
+ .HasColumnType("character varying(20)");
+
+ b.Property("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("ReorderId")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("bigint");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("ReorderId"));
+
+ b.Property("ItemId")
+ .HasColumnType("bigint");
+
+ b.Property("ReorderPoint")
+ .HasPrecision(18, 4)
+ .HasColumnType("numeric(18,4)");
+
+ b.Property("ReorderQty")
+ .HasPrecision(18, 4)
+ .HasColumnType("numeric(18,4)");
+
+ b.Property("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("UomId")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("bigint");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("UomId"));
+
+ b.Property("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("ConversionId")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("bigint");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("ConversionId"));
+
+ b.Property("Factor")
+ .HasPrecision(18, 6)
+ .HasColumnType("numeric(18,6)");
+
+ b.Property("FromUomId")
+ .HasColumnType("bigint");
+
+ b.Property("ItemId")
+ .HasColumnType("bigint");
+
+ b.Property("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("VendorId")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("bigint");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("VendorId"));
+
+ b.Property("Code")
+ .IsRequired()
+ .HasMaxLength(50)
+ .HasColumnType("character varying(50)");
+
+ b.Property("CreatedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("Currency")
+ .IsRequired()
+ .ValueGeneratedOnAdd()
+ .HasMaxLength(3)
+ .HasColumnType("character varying(3)")
+ .HasDefaultValue("LKR");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasMaxLength(200)
+ .HasColumnType("character varying(200)");
+
+ b.Property("RowVersion")
+ .IsConcurrencyToken()
+ .ValueGeneratedOnAddOrUpdate()
+ .HasColumnType("xid")
+ .HasColumnName("xmin");
+
+ b.Property("Status")
+ .IsRequired()
+ .ValueGeneratedOnAdd()
+ .HasMaxLength(20)
+ .HasColumnType("character varying(20)")
+ .HasDefaultValue("Active");
+
+ b.Property("TaxReg")
+ .HasMaxLength(50)
+ .HasColumnType("character varying(50)");
+
+ b.Property("Terms")
+ .HasMaxLength(50)
+ .HasColumnType("character varying(50)");
+
+ b.Property("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("WarehouseId")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("bigint");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("WarehouseId"));
+
+ b.Property("Code")
+ .IsRequired()
+ .HasMaxLength(50)
+ .HasColumnType("character varying(50)");
+
+ b.Property("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
+ }
+ }
+}
diff --git a/Backend/ERPCore/Infra/Persistence/Migrations/20260709124415_initial.cs b/Backend/ERPCore/Infra/Persistence/Migrations/20260709124415_initial.cs
new file mode 100644
index 0000000..f83a4b3
--- /dev/null
+++ b/Backend/ERPCore/Infra/Persistence/Migrations/20260709124415_initial.cs
@@ -0,0 +1,22 @@
+using Microsoft.EntityFrameworkCore.Migrations;
+
+#nullable disable
+
+namespace ERPCore.Infra.Persistence.Migrations
+{
+ ///
+ public partial class initial : Migration
+ {
+ ///
+ protected override void Up(MigrationBuilder migrationBuilder)
+ {
+
+ }
+
+ ///
+ protected override void Down(MigrationBuilder migrationBuilder)
+ {
+
+ }
+ }
+}
diff --git a/Backend/ERPCore/Infra/Persistence/Migrations/ErpDbContextModelSnapshot.cs b/Backend/ERPCore/Infra/Persistence/Migrations/ErpDbContextModelSnapshot.cs
new file mode 100644
index 0000000..c608573
--- /dev/null
+++ b/Backend/ERPCore/Infra/Persistence/Migrations/ErpDbContextModelSnapshot.cs
@@ -0,0 +1,442 @@
+//
+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("BinId")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("bigint");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("BinId"));
+
+ b.Property("BinType")
+ .HasMaxLength(50)
+ .HasColumnType("character varying(50)");
+
+ b.Property("Code")
+ .IsRequired()
+ .HasMaxLength(50)
+ .HasColumnType("character varying(50)");
+
+ b.Property("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("CategoryId")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("bigint");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("CategoryId"));
+
+ b.Property("Name")
+ .IsRequired()
+ .HasMaxLength(200)
+ .HasColumnType("character varying(200)");
+
+ b.Property("ParentId")
+ .HasColumnType("bigint");
+
+ b.HasKey("CategoryId");
+
+ b.HasIndex("ParentId");
+
+ b.ToTable("categories", (string)null);
+ });
+
+ modelBuilder.Entity("ERPCore.Domain.Entities.Item", b =>
+ {
+ b.Property("ItemId")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("bigint");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("ItemId"));
+
+ b.Property("BaseUomId")
+ .HasColumnType("bigint");
+
+ b.Property("CategoryId")
+ .HasColumnType("bigint");
+
+ b.Property("CreatedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("DefaultVendorId")
+ .HasColumnType("bigint");
+
+ b.Property("Description")
+ .HasMaxLength(1000)
+ .HasColumnType("character varying(1000)");
+
+ b.Property