diff --git a/Backend/ERPCore/Controllers/ApiControllerBase.cs b/Backend/ERPCore/Controllers/ApiControllerBase.cs
index ad4d1e3..e00e8b2 100644
--- a/Backend/ERPCore/Controllers/ApiControllerBase.cs
+++ b/Backend/ERPCore/Controllers/ApiControllerBase.cs
@@ -1,5 +1,7 @@
using ERPCore.Common.Http;
+using ERPCore.Infra.Auth;
using ERPCore.System.Errors;
+using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace ERPCore.Controllers;
@@ -8,10 +10,12 @@ 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).
+/// the API contract paths (docs/11 §1.1). Every v1 endpoint requires a valid
+/// AuthHex token satisfying the ERP door policy (docs/10 A.4).
///
[ApiController]
[Produces("application/json")]
+[Authorize(JwtAuthExtensions.ErpAccessPolicy)]
public abstract class ApiControllerBase : ControllerBase
{
/// Parse a mandatory If-Match header, or 428 if absent/malformed.
diff --git a/Backend/ERPCore/Domain/Entities/User.cs b/Backend/ERPCore/Domain/Entities/User.cs
index 24b58db..2966e68 100644
--- a/Backend/ERPCore/Domain/Entities/User.cs
+++ b/Backend/ERPCore/Domain/Entities/User.cs
@@ -3,18 +3,23 @@ using ERPCore.Domain.Enums;
namespace ERPCore.Domain.Entities;
///
-/// Application user (FR-X-01). In Phase 1 authentication/RBAC are deferred; this
-/// table exists so mutations can be stamped with an audit actor and documents can
-/// carry a `createdBy`/`requestedBy` FK. A seeded system user (id 1) is the
-/// fallback actor until `/auth/login` lands (§6). Model: docs/10 Part C.7.
+/// Application user (FR-X-01) — a **local shadow/projection** of an AuthHex identity.
+/// The local (long) is what every `createdBy`/`requestedBy`/
+/// audit/ledger FK references; maps it to the AuthHex
+/// UserId (GUID) and is JIT-provisioned on first authenticated request
+/// (docs/10 A.4/C.7). A seeded system user (id 1, null AuthUserId) is the
+/// fallback actor for unauthenticated/system operations. Model: docs/10 Part C.7.
///
public class User
{
- /// Seeded fallback actor used while auth is deferred.
+ /// Seeded fallback actor for unauthenticated/system operations.
public const long SystemUserId = 1;
public long UserId { get; set; }
public string Username { get; set; } = string.Empty;
public string DisplayName { get; set; } = string.Empty;
public EntityStatus Status { get; set; } = EntityStatus.Active;
+
+ /// AuthHex identity (token UserId GUID); null for the seeded system user.
+ public Guid? AuthUserId { get; set; }
}
diff --git a/Backend/ERPCore/Infra/Auth/AuthHexClaims.cs b/Backend/ERPCore/Infra/Auth/AuthHexClaims.cs
new file mode 100644
index 0000000..e35400a
--- /dev/null
+++ b/Backend/ERPCore/Infra/Auth/AuthHexClaims.cs
@@ -0,0 +1,17 @@
+namespace ERPCore.Infra.Auth;
+
+///
+/// Claim type names emitted by the AuthHex IdP (see its JwtTokenHelper).
+/// AuthHex uses no standard sub/nameid; identity is the custom
+/// (GUID). These are read verbatim (JWT bearer is configured
+/// with MapInboundClaims = false).
+///
+public static class AuthHexClaims
+{
+ public const string UserId = "UserId";
+ public const string UserTypeId = "UserTypeId";
+ public const string UserTypeCode = "UserTypeCode";
+ public const string RoleId = "RoleId";
+ public const string RoleCode = "RoleCode";
+ public const string Nic = "NIC";
+}
diff --git a/Backend/ERPCore/Infra/Auth/JwtAuthExtensions.cs b/Backend/ERPCore/Infra/Auth/JwtAuthExtensions.cs
index 10ca70f..ea879ca 100644
--- a/Backend/ERPCore/Infra/Auth/JwtAuthExtensions.cs
+++ b/Backend/ERPCore/Infra/Auth/JwtAuthExtensions.cs
@@ -1,25 +1,42 @@
-using System.Text;
+using System.Security.Cryptography;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.IdentityModel.Tokens;
namespace ERPCore.Infra.Auth;
///
-/// JWT bearer wiring. Authentication only — RBAC/authorization policies are
-/// deferred for Phase 1; the validated principal exists solely so that
-/// can stamp the audit actor.
+/// Auth wiring for ERPCore as a **resource server** for the external AuthHex IdP
+/// (docs/10 A.4). Validates AuthHex's **RS256** tokens against AuthHex's RSA public
+/// key (configured statically — no JWKS), issuer AuthHex, audience
+/// AuthHexClient. A single door policy () admits
+/// only ERP UserType/Role holders when those codes are configured;
+/// per-endpoint RBAC stays deferred. Identity → audit actor is resolved by
+/// + .
///
public static class JwtAuthExtensions
{
+ /// Authorization policy applied to every v1 controller (via ApiControllerBase).
+ public const string ErpAccessPolicy = "ErpAccess";
+
public static IServiceCollection AddErpJwtAuth(this IServiceCollection services, IConfiguration config)
{
- var issuer = config["Jwt:Issuer"];
- var audience = config["Jwt:Audience"];
- var signingKey = config["Jwt:SigningKey"] ?? string.Empty;
+ var issuer = config["Auth:Issuer"];
+ var audience = config["Auth:Audience"];
+ var publicKeyXml = config["Auth:RsaPublicKeyXml"]
+ ?? throw new InvalidOperationException("Auth:RsaPublicKeyXml (AuthHex RSA public key) is not configured.");
+ var requiredUserType = config["Auth:RequiredUserTypeCode"];
+ var requiredRole = config["Auth:RequiredRoleCode"];
+
+ // AuthHex publishes no JWKS; the RSA public key is configured statically.
+ var rsa = RSA.Create();
+ rsa.FromXmlString(publicKeyXml);
+ var signingKey = new RsaSecurityKey(rsa);
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
+ // Keep AuthHex's claim names verbatim (UserId, UserTypeCode, RoleCode …).
+ options.MapInboundClaims = false;
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
@@ -28,12 +45,26 @@ public static class JwtAuthExtensions
ValidAudience = audience,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
- IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(signingKey)),
+ IssuerSigningKey = signingKey,
+ ValidAlgorithms = new[] { SecurityAlgorithms.RsaSha256 },
ClockSkew = TimeSpan.FromSeconds(30)
};
});
- services.AddAuthorization();
+ services.AddAuthorization(options =>
+ {
+ options.AddPolicy(ErpAccessPolicy, policy =>
+ {
+ policy.RequireAuthenticatedUser();
+ // Door gate: only enforce a UserType/Role when configured (AuthHex is a
+ // shared IdP). Empty config = require a valid ERP token only.
+ if (!string.IsNullOrWhiteSpace(requiredUserType))
+ policy.RequireClaim(AuthHexClaims.UserTypeCode, requiredUserType);
+ if (!string.IsNullOrWhiteSpace(requiredRole))
+ policy.RequireClaim(AuthHexClaims.RoleCode, requiredRole);
+ });
+ });
+
return services;
}
}
diff --git a/Backend/ERPCore/Infra/Auth/ShadowUserClaimsTransformation.cs b/Backend/ERPCore/Infra/Auth/ShadowUserClaimsTransformation.cs
new file mode 100644
index 0000000..ebf7f21
--- /dev/null
+++ b/Backend/ERPCore/Infra/Auth/ShadowUserClaimsTransformation.cs
@@ -0,0 +1,76 @@
+using System.Security.Claims;
+using ERPCore.Domain.Entities;
+using ERPCore.Domain.Enums;
+using ERPCore.Infra.Persistence;
+using Microsoft.AspNetCore.Authentication;
+using Microsoft.EntityFrameworkCore;
+
+namespace ERPCore.Infra.Auth;
+
+///
+/// Maps an authenticated AuthHex principal to ERPCore's local identity (docs/10 A.4/A.5).
+/// AuthHex tokens carry the user as a custom UserId (GUID) claim and no
+/// sub/nameid. This transformation JIT-provisions a local shadow
+/// (keyed by auth_user_id) and injects the local
+/// long id as , so
+/// /AuditUserId resolve the real user unchanged.
+/// Idempotent — may run several times per request.
+///
+public sealed class ShadowUserClaimsTransformation : IClaimsTransformation
+{
+ private readonly ErpDbContext _db;
+
+ public ShadowUserClaimsTransformation(ErpDbContext db) => _db = db;
+
+ public async Task TransformAsync(ClaimsPrincipal principal)
+ {
+ if (principal.Identity is not ClaimsIdentity identity || !identity.IsAuthenticated)
+ return principal;
+ if (identity.HasClaim(c => c.Type == ClaimTypes.NameIdentifier))
+ return principal; // already resolved this request
+
+ var raw = principal.FindFirstValue(AuthHexClaims.UserId);
+ if (!Guid.TryParse(raw, out var authUserId))
+ return principal; // no mappable identity → CurrentUser falls back to system
+
+ var nic = principal.FindFirstValue(AuthHexClaims.Nic);
+ var localId = await ResolveOrProvisionAsync(authUserId, nic);
+
+ identity.AddClaim(new Claim(ClaimTypes.NameIdentifier, localId.ToString()));
+ return principal;
+ }
+
+ private async Task ResolveOrProvisionAsync(Guid authUserId, string? nic)
+ {
+ var existing = await _db.Users.AsNoTracking()
+ .Where(u => u.AuthUserId == authUserId)
+ .Select(u => u.UserId)
+ .FirstOrDefaultAsync();
+ if (existing != 0) return existing;
+
+ var label = string.IsNullOrWhiteSpace(nic) ? authUserId.ToString() : nic.Trim();
+ var user = new User
+ {
+ AuthUserId = authUserId,
+ Username = label,
+ DisplayName = string.IsNullOrWhiteSpace(nic) ? "AuthHex User" : nic.Trim(),
+ Status = EntityStatus.Active
+ };
+
+ try
+ {
+ _db.Users.Add(user);
+ await _db.SaveChangesAsync();
+ return user.UserId;
+ }
+ catch (DbUpdateException)
+ {
+ // Lost a race (unique auth_user_id) — the row now exists; re-read it.
+ _db.Entry(user).State = EntityState.Detached;
+ return await _db.Users.AsNoTracking()
+ .Where(u => u.AuthUserId == authUserId)
+ .Select(u => u.UserId)
+ .FirstAsync();
+ }
+ }
+}
diff --git a/Backend/ERPCore/Infra/Persistence/Configurations/UserConfiguration.cs b/Backend/ERPCore/Infra/Persistence/Configurations/UserConfiguration.cs
index 7a1c48b..eee8827 100644
--- a/Backend/ERPCore/Infra/Persistence/Configurations/UserConfiguration.cs
+++ b/Backend/ERPCore/Infra/Persistence/Configurations/UserConfiguration.cs
@@ -18,6 +18,11 @@ public sealed class UserConfiguration : IEntityTypeConfiguration
builder.Property(u => u.Status)
.HasConversion().HasMaxLength(20).IsRequired();
+ // Maps the local shadow user to its AuthHex identity (unique; NULL for the
+ // system user — Postgres allows multiple NULLs in a unique index).
+ builder.Property(u => u.AuthUserId).HasColumnName("auth_user_id");
+ builder.HasIndex(u => u.AuthUserId).IsUnique();
+
// Seeded fallback audit actor while auth is deferred (§6).
builder.HasData(new User
{
diff --git a/Backend/ERPCore/Infra/Persistence/Migrations/20260714103443_AddAuthUserId.Designer.cs b/Backend/ERPCore/Infra/Persistence/Migrations/20260714103443_AddAuthUserId.Designer.cs
new file mode 100644
index 0000000..7251936
--- /dev/null
+++ b/Backend/ERPCore/Infra/Persistence/Migrations/20260714103443_AddAuthUserId.Designer.cs
@@ -0,0 +1,2229 @@
+//
+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("20260714103443_AddAuthUserId")]
+ partial class AddAuthUserId
+ {
+ ///
+ 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.AuditLog", b =>
+ {
+ b.Property("AuditId")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("bigint");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("AuditId"));
+
+ b.Property("Action")
+ .IsRequired()
+ .HasMaxLength(10)
+ .HasColumnType("character varying(10)");
+
+ b.Property("ChangeSet")
+ .IsRequired()
+ .HasColumnType("jsonb");
+
+ b.Property("CreatedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("EntityId")
+ .HasColumnType("bigint");
+
+ b.Property("EntityType")
+ .IsRequired()
+ .HasMaxLength(80)
+ .HasColumnType("character varying(80)");
+
+ b.Property("UserId")
+ .HasColumnType("bigint");
+
+ b.HasKey("AuditId");
+
+ b.HasIndex("CreatedAt");
+
+ b.HasIndex("UserId");
+
+ b.HasIndex("EntityType", "EntityId");
+
+ b.ToTable("audit_logs", (string)null);
+ });
+
+ modelBuilder.Entity("ERPCore.Domain.Entities.Batch", b =>
+ {
+ b.Property("BatchId")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("bigint");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("BatchId"));
+
+ b.Property("BatchNo")
+ .IsRequired()
+ .HasMaxLength(50)
+ .HasColumnType("character varying(50)");
+
+ b.Property("ExpiryDate")
+ .HasColumnType("date");
+
+ b.Property("ItemId")
+ .HasColumnType("bigint");
+
+ b.HasKey("BatchId");
+
+ b.HasIndex("ItemId", "BatchNo")
+ .IsUnique();
+
+ b.ToTable("batches", (string)null);
+ });
+
+ 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.Grn", b =>
+ {
+ b.Property("GrnId")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("bigint");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("GrnId"));
+
+ b.Property("CreatedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("CreatedBy")
+ .HasColumnType("bigint");
+
+ b.Property("DocNo")
+ .IsRequired()
+ .HasMaxLength(30)
+ .HasColumnType("character varying(30)");
+
+ b.Property("PoId")
+ .HasColumnType("bigint");
+
+ b.Property("PostedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("RowVersion")
+ .IsConcurrencyToken()
+ .ValueGeneratedOnAddOrUpdate()
+ .HasColumnType("xid")
+ .HasColumnName("xmin");
+
+ b.Property("Status")
+ .IsRequired()
+ .HasMaxLength(20)
+ .HasColumnType("character varying(20)");
+
+ b.Property("VendorId")
+ .HasColumnType("bigint");
+
+ b.Property("WarehouseId")
+ .HasColumnType("bigint");
+
+ b.HasKey("GrnId");
+
+ b.HasIndex("CreatedBy");
+
+ b.HasIndex("DocNo")
+ .IsUnique();
+
+ b.HasIndex("PoId");
+
+ b.HasIndex("Status");
+
+ b.HasIndex("VendorId");
+
+ b.HasIndex("WarehouseId");
+
+ b.ToTable("grns", (string)null);
+ });
+
+ modelBuilder.Entity("ERPCore.Domain.Entities.GrnLine", b =>
+ {
+ b.Property("GrnLineId")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("bigint");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("GrnLineId"));
+
+ b.Property("BatchId")
+ .HasColumnType("bigint");
+
+ b.Property("BinId")
+ .HasColumnType("bigint");
+
+ b.Property("GrnId")
+ .HasColumnType("bigint");
+
+ b.Property("HoldStatus")
+ .IsRequired()
+ .HasMaxLength(20)
+ .HasColumnType("character varying(20)");
+
+ b.Property("ItemId")
+ .HasColumnType("bigint");
+
+ b.Property("PoLineId")
+ .HasColumnType("bigint");
+
+ b.Property("Qty")
+ .HasPrecision(18, 4)
+ .HasColumnType("numeric(18,4)");
+
+ b.Property("ReceivedValue")
+ .HasPrecision(18, 4)
+ .HasColumnType("numeric(18,4)");
+
+ b.Property("UnitCost")
+ .HasPrecision(18, 6)
+ .HasColumnType("numeric(18,6)");
+
+ b.Property("UomId")
+ .HasColumnType("bigint");
+
+ b.HasKey("GrnLineId");
+
+ b.HasIndex("BatchId");
+
+ b.HasIndex("BinId");
+
+ b.HasIndex("GrnId");
+
+ b.HasIndex("ItemId");
+
+ b.HasIndex("PoLineId");
+
+ b.HasIndex("UomId");
+
+ b.ToTable("grn_lines", (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.JournalEntryStub", b =>
+ {
+ b.Property("JournalId")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("bigint");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("JournalId"));
+
+ b.Property("Amount")
+ .HasPrecision(18, 4)
+ .HasColumnType("numeric(18,4)");
+
+ b.Property("CreditAccount")
+ .IsRequired()
+ .HasMaxLength(20)
+ .HasColumnType("character varying(20)");
+
+ b.Property("DebitAccount")
+ .IsRequired()
+ .HasMaxLength(20)
+ .HasColumnType("character varying(20)");
+
+ b.Property("SourceDocId")
+ .HasColumnType("bigint");
+
+ b.Property("SourceDocType")
+ .IsRequired()
+ .HasMaxLength(10)
+ .HasColumnType("character varying(10)");
+
+ b.HasKey("JournalId");
+
+ b.HasIndex("SourceDocType", "SourceDocId");
+
+ b.ToTable("journal_entry_stubs", (string)null);
+ });
+
+ modelBuilder.Entity("ERPCore.Domain.Entities.NumberSequence", b =>
+ {
+ b.Property("SequenceId")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("bigint");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("SequenceId"));
+
+ b.Property("DocType")
+ .IsRequired()
+ .HasMaxLength(10)
+ .HasColumnType("character varying(10)")
+ .HasColumnName("doc_type");
+
+ b.Property("LastNumber")
+ .HasColumnType("bigint")
+ .HasColumnName("last_number");
+
+ b.Property("Year")
+ .HasColumnType("integer")
+ .HasColumnName("year");
+
+ b.HasKey("SequenceId");
+
+ b.HasIndex("DocType", "Year")
+ .IsUnique();
+
+ b.ToTable("number_sequences", (string)null);
+ });
+
+ modelBuilder.Entity("ERPCore.Domain.Entities.PoLine", b =>
+ {
+ b.Property("PoLineId")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("bigint");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("PoLineId"));
+
+ b.Property("ItemId")
+ .HasColumnType("bigint");
+
+ b.Property("PoId")
+ .HasColumnType("bigint");
+
+ b.Property("Qty")
+ .HasPrecision(18, 4)
+ .HasColumnType("numeric(18,4)");
+
+ b.Property("QtyReceived")
+ .HasPrecision(18, 4)
+ .HasColumnType("numeric(18,4)");
+
+ b.Property("Tax")
+ .HasPrecision(9, 4)
+ .HasColumnType("numeric(9,4)");
+
+ b.Property("UnitPrice")
+ .HasPrecision(18, 4)
+ .HasColumnType("numeric(18,4)");
+
+ b.Property("UomId")
+ .HasColumnType("bigint");
+
+ b.Property("WarehouseId")
+ .HasColumnType("bigint");
+
+ b.HasKey("PoLineId");
+
+ b.HasIndex("ItemId");
+
+ b.HasIndex("PoId");
+
+ b.HasIndex("UomId");
+
+ b.HasIndex("WarehouseId");
+
+ b.ToTable("po_lines", (string)null);
+ });
+
+ modelBuilder.Entity("ERPCore.Domain.Entities.PurchaseOrder", b =>
+ {
+ b.Property("PoId")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("bigint");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("PoId"));
+
+ b.Property("ApprovalRequired")
+ .HasColumnType("boolean");
+
+ b.Property("CreatedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("CreatedBy")
+ .HasColumnType("bigint");
+
+ b.Property("DocNo")
+ .IsRequired()
+ .HasMaxLength(30)
+ .HasColumnType("character varying(30)");
+
+ b.Property("RequisitionId")
+ .HasColumnType("bigint");
+
+ b.Property("RowVersion")
+ .IsConcurrencyToken()
+ .ValueGeneratedOnAddOrUpdate()
+ .HasColumnType("xid")
+ .HasColumnName("xmin");
+
+ b.Property("Status")
+ .IsRequired()
+ .HasMaxLength(20)
+ .HasColumnType("character varying(20)");
+
+ b.Property("UpdatedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("VendorId")
+ .HasColumnType("bigint");
+
+ b.HasKey("PoId");
+
+ b.HasIndex("CreatedBy");
+
+ b.HasIndex("DocNo")
+ .IsUnique();
+
+ b.HasIndex("RequisitionId");
+
+ b.HasIndex("Status");
+
+ b.HasIndex("VendorId");
+
+ b.ToTable("purchase_orders", (string)null);
+ });
+
+ modelBuilder.Entity("ERPCore.Domain.Entities.PurchaseReturn", b =>
+ {
+ b.Property("ReturnId")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("bigint");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("ReturnId"));
+
+ b.Property("CreatedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("CreatedBy")
+ .HasColumnType("bigint");
+
+ b.Property("DocNo")
+ .IsRequired()
+ .HasMaxLength(30)
+ .HasColumnType("character varying(30)");
+
+ b.Property("ReasonCodeId")
+ .HasColumnType("bigint");
+
+ b.Property("Status")
+ .IsRequired()
+ .HasMaxLength(20)
+ .HasColumnType("character varying(20)");
+
+ b.Property("VendorId")
+ .HasColumnType("bigint");
+
+ b.Property("WarehouseId")
+ .HasColumnType("bigint");
+
+ b.HasKey("ReturnId");
+
+ b.HasIndex("CreatedBy");
+
+ b.HasIndex("DocNo")
+ .IsUnique();
+
+ b.HasIndex("ReasonCodeId");
+
+ b.HasIndex("VendorId");
+
+ b.HasIndex("WarehouseId");
+
+ b.ToTable("purchase_returns", (string)null);
+ });
+
+ modelBuilder.Entity("ERPCore.Domain.Entities.PurchaseReturnLine", b =>
+ {
+ b.Property("ReturnLineId")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("bigint");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("ReturnLineId"));
+
+ b.Property("GrnLineId")
+ .HasColumnType("bigint");
+
+ b.Property("ItemId")
+ .HasColumnType("bigint");
+
+ b.Property("Qty")
+ .HasPrecision(18, 4)
+ .HasColumnType("numeric(18,4)");
+
+ b.Property("ReturnId")
+ .HasColumnType("bigint");
+
+ b.HasKey("ReturnLineId");
+
+ b.HasIndex("GrnLineId");
+
+ b.HasIndex("ItemId");
+
+ b.HasIndex("ReturnId");
+
+ b.ToTable("purchase_return_lines", (string)null);
+ });
+
+ modelBuilder.Entity("ERPCore.Domain.Entities.ReasonCode", b =>
+ {
+ b.Property("ReasonCodeId")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("bigint");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("ReasonCodeId"));
+
+ b.Property("Code")
+ .IsRequired()
+ .HasMaxLength(20)
+ .HasColumnType("character varying(20)");
+
+ b.Property("Context")
+ .IsRequired()
+ .HasMaxLength(20)
+ .HasColumnType("character varying(20)");
+
+ b.Property("Description")
+ .IsRequired()
+ .HasMaxLength(200)
+ .HasColumnType("character varying(200)");
+
+ b.HasKey("ReasonCodeId");
+
+ b.HasIndex("Context", "Code")
+ .IsUnique();
+
+ b.ToTable("reason_codes", (string)null);
+ });
+
+ modelBuilder.Entity("ERPCore.Domain.Entities.Requisition", b =>
+ {
+ b.Property("RequisitionId")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("bigint");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("RequisitionId"));
+
+ b.Property("CreatedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("DocNo")
+ .IsRequired()
+ .HasMaxLength(30)
+ .HasColumnType("character varying(30)");
+
+ b.Property("RequestedBy")
+ .HasColumnType("bigint");
+
+ b.Property("Status")
+ .IsRequired()
+ .HasMaxLength(20)
+ .HasColumnType("character varying(20)");
+
+ b.HasKey("RequisitionId");
+
+ b.HasIndex("DocNo")
+ .IsUnique();
+
+ b.HasIndex("RequestedBy");
+
+ b.HasIndex("Status");
+
+ b.ToTable("requisitions", (string)null);
+ });
+
+ modelBuilder.Entity("ERPCore.Domain.Entities.RequisitionLine", b =>
+ {
+ b.Property("ReqLineId")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("bigint");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("ReqLineId"));
+
+ b.Property("ItemId")
+ .HasColumnType("bigint");
+
+ b.Property("Qty")
+ .HasPrecision(18, 4)
+ .HasColumnType("numeric(18,4)");
+
+ b.Property("RequiredBy")
+ .HasColumnType("date");
+
+ b.Property("RequisitionId")
+ .HasColumnType("bigint");
+
+ b.HasKey("ReqLineId");
+
+ b.HasIndex("ItemId");
+
+ b.HasIndex("RequisitionId");
+
+ b.ToTable("requisition_lines", (string)null);
+ });
+
+ modelBuilder.Entity("ERPCore.Domain.Entities.Rfq", b =>
+ {
+ b.Property("RfqId")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("bigint");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("RfqId"));
+
+ b.Property("CreatedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("DocNo")
+ .IsRequired()
+ .HasMaxLength(30)
+ .HasColumnType("character varying(30)");
+
+ b.Property("RequisitionId")
+ .HasColumnType("bigint");
+
+ b.Property("Status")
+ .IsRequired()
+ .HasMaxLength(20)
+ .HasColumnType("character varying(20)");
+
+ b.HasKey("RfqId");
+
+ b.HasIndex("DocNo")
+ .IsUnique();
+
+ b.HasIndex("RequisitionId");
+
+ b.ToTable("rfqs", (string)null);
+ });
+
+ modelBuilder.Entity("ERPCore.Domain.Entities.RfqLine", b =>
+ {
+ b.Property("RfqLineId")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("bigint");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("RfqLineId"));
+
+ b.Property("ItemId")
+ .HasColumnType("bigint");
+
+ b.Property("Qty")
+ .HasPrecision(18, 4)
+ .HasColumnType("numeric(18,4)");
+
+ b.Property("RfqId")
+ .HasColumnType("bigint");
+
+ b.HasKey("RfqLineId");
+
+ b.HasIndex("ItemId");
+
+ b.HasIndex("RfqId");
+
+ b.ToTable("rfq_lines", (string)null);
+ });
+
+ modelBuilder.Entity("ERPCore.Domain.Entities.Serial", b =>
+ {
+ b.Property("SerialId")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("bigint");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("SerialId"));
+
+ b.Property("ItemId")
+ .HasColumnType("bigint");
+
+ b.Property("SerialNo")
+ .IsRequired()
+ .HasMaxLength(100)
+ .HasColumnType("character varying(100)");
+
+ b.Property("Status")
+ .IsRequired()
+ .HasMaxLength(20)
+ .HasColumnType("character varying(20)");
+
+ b.HasKey("SerialId");
+
+ b.HasIndex("ItemId", "SerialNo")
+ .IsUnique();
+
+ b.ToTable("serials", (string)null);
+ });
+
+ modelBuilder.Entity("ERPCore.Domain.Entities.StockAdjustment", b =>
+ {
+ b.Property("AdjustmentId")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("bigint");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("AdjustmentId"));
+
+ b.Property("CreatedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("CreatedBy")
+ .HasColumnType("bigint");
+
+ b.Property("DocNo")
+ .IsRequired()
+ .HasMaxLength(30)
+ .HasColumnType("character varying(30)");
+
+ b.Property("ReasonCodeId")
+ .HasColumnType("bigint");
+
+ b.Property("RowVersion")
+ .IsConcurrencyToken()
+ .ValueGeneratedOnAddOrUpdate()
+ .HasColumnType("xid")
+ .HasColumnName("xmin");
+
+ b.Property("Status")
+ .IsRequired()
+ .HasMaxLength(20)
+ .HasColumnType("character varying(20)");
+
+ b.Property("WarehouseId")
+ .HasColumnType("bigint");
+
+ b.HasKey("AdjustmentId");
+
+ b.HasIndex("CreatedBy");
+
+ b.HasIndex("DocNo")
+ .IsUnique();
+
+ b.HasIndex("ReasonCodeId");
+
+ b.HasIndex("WarehouseId");
+
+ b.ToTable("stock_adjustments", (string)null);
+ });
+
+ modelBuilder.Entity("ERPCore.Domain.Entities.StockAdjustmentLine", b =>
+ {
+ b.Property("AdjLineId")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("bigint");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("AdjLineId"));
+
+ b.Property("AdjustmentId")
+ .HasColumnType("bigint");
+
+ b.Property("BatchId")
+ .HasColumnType("bigint");
+
+ b.Property("BinId")
+ .HasColumnType("bigint");
+
+ b.Property("ItemId")
+ .HasColumnType("bigint");
+
+ b.Property("QtyDelta")
+ .HasPrecision(18, 4)
+ .HasColumnType("numeric(18,4)");
+
+ b.Property("SerialId")
+ .HasColumnType("bigint");
+
+ b.HasKey("AdjLineId");
+
+ b.HasIndex("AdjustmentId");
+
+ b.HasIndex("BatchId");
+
+ b.HasIndex("BinId");
+
+ b.HasIndex("ItemId");
+
+ b.HasIndex("SerialId");
+
+ b.ToTable("stock_adjustment_lines", (string)null);
+ });
+
+ modelBuilder.Entity("ERPCore.Domain.Entities.StockCount", b =>
+ {
+ b.Property("CountId")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("bigint");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("CountId"));
+
+ b.Property("CountType")
+ .IsRequired()
+ .HasMaxLength(20)
+ .HasColumnType("character varying(20)");
+
+ b.Property("CreatedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("CreatedBy")
+ .HasColumnType("bigint");
+
+ b.Property("DocNo")
+ .IsRequired()
+ .HasMaxLength(30)
+ .HasColumnType("character varying(30)");
+
+ b.Property("RowVersion")
+ .IsConcurrencyToken()
+ .ValueGeneratedOnAddOrUpdate()
+ .HasColumnType("xid")
+ .HasColumnName("xmin");
+
+ b.Property("Status")
+ .IsRequired()
+ .HasMaxLength(20)
+ .HasColumnType("character varying(20)");
+
+ b.Property("WarehouseId")
+ .HasColumnType("bigint");
+
+ b.HasKey("CountId");
+
+ b.HasIndex("CreatedBy");
+
+ b.HasIndex("DocNo")
+ .IsUnique();
+
+ b.HasIndex("Status");
+
+ b.HasIndex("WarehouseId");
+
+ b.ToTable("stock_counts", (string)null);
+ });
+
+ modelBuilder.Entity("ERPCore.Domain.Entities.StockCountLine", b =>
+ {
+ b.Property("CountLineId")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("bigint");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("CountLineId"));
+
+ b.Property("BinId")
+ .HasColumnType("bigint");
+
+ b.Property("CountId")
+ .HasColumnType("bigint");
+
+ b.Property("CountedQty")
+ .HasPrecision(18, 4)
+ .HasColumnType("numeric(18,4)");
+
+ b.Property("ItemId")
+ .HasColumnType("bigint");
+
+ b.Property("SystemQty")
+ .HasPrecision(18, 4)
+ .HasColumnType("numeric(18,4)");
+
+ b.Property("Variance")
+ .HasPrecision(18, 4)
+ .HasColumnType("numeric(18,4)");
+
+ b.HasKey("CountLineId");
+
+ b.HasIndex("BinId");
+
+ b.HasIndex("CountId");
+
+ b.HasIndex("ItemId");
+
+ b.ToTable("stock_count_lines", (string)null);
+ });
+
+ modelBuilder.Entity("ERPCore.Domain.Entities.StockLayer", b =>
+ {
+ b.Property("LayerId")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("bigint");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("LayerId"));
+
+ b.Property("BatchId")
+ .HasColumnType("bigint");
+
+ b.Property("GrnLineId")
+ .HasColumnType("bigint");
+
+ b.Property("ItemId")
+ .HasColumnType("bigint");
+
+ b.Property("QtyReceived")
+ .HasPrecision(18, 4)
+ .HasColumnType("numeric(18,4)");
+
+ b.Property("QtyRemaining")
+ .HasPrecision(18, 4)
+ .HasColumnType("numeric(18,4)");
+
+ b.Property("ReceiptDate")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("SerialId")
+ .HasColumnType("bigint");
+
+ b.Property("UnitCost")
+ .HasPrecision(18, 6)
+ .HasColumnType("numeric(18,6)");
+
+ b.Property("WarehouseId")
+ .HasColumnType("bigint");
+
+ b.HasKey("LayerId");
+
+ b.HasIndex("BatchId");
+
+ b.HasIndex("GrnLineId");
+
+ b.HasIndex("SerialId");
+
+ b.HasIndex("WarehouseId");
+
+ b.HasIndex("ItemId", "WarehouseId", "ReceiptDate", "LayerId");
+
+ b.ToTable("stock_layers", (string)null);
+ });
+
+ modelBuilder.Entity("ERPCore.Domain.Entities.StockLedger", b =>
+ {
+ b.Property("LedgerId")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("bigint");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("LedgerId"));
+
+ b.Property("BatchId")
+ .HasColumnType("bigint");
+
+ b.Property("BinId")
+ .HasColumnType("bigint");
+
+ b.Property("CreatedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("Direction")
+ .IsRequired()
+ .HasMaxLength(5)
+ .HasColumnType("character varying(5)");
+
+ b.Property("ItemId")
+ .HasColumnType("bigint");
+
+ b.Property("QtyBase")
+ .HasPrecision(18, 4)
+ .HasColumnType("numeric(18,4)");
+
+ b.Property("RunningBalance")
+ .HasPrecision(18, 4)
+ .HasColumnType("numeric(18,4)");
+
+ b.Property("SerialId")
+ .HasColumnType("bigint");
+
+ b.Property("SourceDocId")
+ .HasColumnType("bigint");
+
+ b.Property("SourceDocType")
+ .IsRequired()
+ .HasMaxLength(10)
+ .HasColumnType("character varying(10)");
+
+ b.Property("UnitCost")
+ .HasPrecision(18, 6)
+ .HasColumnType("numeric(18,6)");
+
+ b.Property("UserId")
+ .HasColumnType("bigint");
+
+ b.Property("Value")
+ .HasPrecision(18, 4)
+ .HasColumnType("numeric(18,4)");
+
+ b.Property("WarehouseId")
+ .HasColumnType("bigint");
+
+ b.HasKey("LedgerId");
+
+ b.HasIndex("BatchId");
+
+ b.HasIndex("BinId");
+
+ b.HasIndex("SerialId");
+
+ b.HasIndex("UserId");
+
+ b.HasIndex("WarehouseId");
+
+ b.HasIndex("SourceDocType", "SourceDocId");
+
+ b.HasIndex("ItemId", "WarehouseId", "CreatedAt");
+
+ b.HasIndex("ItemId", "WarehouseId", "LedgerId");
+
+ b.ToTable("stock_ledger", (string)null);
+ });
+
+ modelBuilder.Entity("ERPCore.Domain.Entities.StockTransfer", b =>
+ {
+ b.Property("TransferId")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("bigint");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("TransferId"));
+
+ b.Property("CreatedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("CreatedBy")
+ .HasColumnType("bigint");
+
+ b.Property("DestWarehouseId")
+ .HasColumnType("bigint");
+
+ b.Property("DocNo")
+ .IsRequired()
+ .HasMaxLength(30)
+ .HasColumnType("character varying(30)");
+
+ b.Property("RowVersion")
+ .IsConcurrencyToken()
+ .ValueGeneratedOnAddOrUpdate()
+ .HasColumnType("xid")
+ .HasColumnName("xmin");
+
+ b.Property("SrcWarehouseId")
+ .HasColumnType("bigint");
+
+ b.Property("Status")
+ .IsRequired()
+ .HasMaxLength(20)
+ .HasColumnType("character varying(20)");
+
+ b.HasKey("TransferId");
+
+ b.HasIndex("CreatedBy");
+
+ b.HasIndex("DestWarehouseId");
+
+ b.HasIndex("DocNo")
+ .IsUnique();
+
+ b.HasIndex("SrcWarehouseId");
+
+ b.HasIndex("Status");
+
+ b.ToTable("stock_transfers", (string)null);
+ });
+
+ modelBuilder.Entity("ERPCore.Domain.Entities.StockTransferLine", b =>
+ {
+ b.Property("TransferLineId")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("bigint");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("TransferLineId"));
+
+ b.Property("BatchId")
+ .HasColumnType("bigint");
+
+ b.Property("DestBinId")
+ .HasColumnType("bigint");
+
+ b.Property("ItemId")
+ .HasColumnType("bigint");
+
+ b.Property("Qty")
+ .HasPrecision(18, 4)
+ .HasColumnType("numeric(18,4)");
+
+ b.Property("QtyReceived")
+ .HasPrecision(18, 4)
+ .HasColumnType("numeric(18,4)");
+
+ b.Property("SerialId")
+ .HasColumnType("bigint");
+
+ b.Property("SrcBinId")
+ .HasColumnType("bigint");
+
+ b.Property("TransferId")
+ .HasColumnType("bigint");
+
+ b.Property("UnitCost")
+ .HasPrecision(18, 6)
+ .HasColumnType("numeric(18,6)");
+
+ b.HasKey("TransferLineId");
+
+ b.HasIndex("BatchId");
+
+ b.HasIndex("DestBinId");
+
+ b.HasIndex("ItemId");
+
+ b.HasIndex("SerialId");
+
+ b.HasIndex("SrcBinId");
+
+ b.HasIndex("TransferId");
+
+ b.ToTable("stock_transfer_lines", (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.User", b =>
+ {
+ b.Property("UserId")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("bigint");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("UserId"));
+
+ b.Property("AuthUserId")
+ .HasColumnType("uuid")
+ .HasColumnName("auth_user_id");
+
+ b.Property("DisplayName")
+ .IsRequired()
+ .HasMaxLength(200)
+ .HasColumnType("character varying(200)");
+
+ b.Property("Status")
+ .IsRequired()
+ .HasMaxLength(20)
+ .HasColumnType("character varying(20)");
+
+ b.Property("Username")
+ .IsRequired()
+ .HasMaxLength(100)
+ .HasColumnType("character varying(100)");
+
+ b.HasKey("UserId");
+
+ b.HasIndex("AuthUserId")
+ .IsUnique();
+
+ b.HasIndex("Username")
+ .IsUnique();
+
+ b.ToTable("users", (string)null);
+
+ b.HasData(
+ new
+ {
+ UserId = 1L,
+ DisplayName = "System",
+ Status = "Active",
+ Username = "system"
+ });
+ });
+
+ 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