using System.Reflection; using Admin.NET.Plugin.AiDOP.MaterialWarehouse.InventoryPosting.Entity; using SqlSugar; using Xunit; namespace Admin.NET.Plugin.AiDOP.Tests.S5.MaterialWarehouse; /// /// Phase 2 结构级验证(反射,无 DB):断言 L3 影子账实体的表名、复合唯一键维度、 /// tenant_id + domain_code 列、lot_serial/refs 空批次唯一语义(NOT NULL DEFAULT '')、条码非唯一。 /// 复合唯一键维度全部据 Schema 实核(LIVE SP 真定义)确定,此测试把「唯一键契约」固化。 /// public class AdoInventoryEntitiesStructureTests { private static string TableName(Type t) => t.GetCustomAttribute()?.TableName; private static SugarColumn Column(Type t, string prop) => t.GetProperty(prop)?.GetCustomAttribute(); private static List> UniqueKeyPropSets(Type t) => t.GetCustomAttributes() .Where(ix => ix.IsUnique) .Select(ix => new HashSet(ix.IndexFields.Keys)) .ToList(); private static bool HasUniqueKey(Type t, params string[] props) { var want = new HashSet(props); return UniqueKeyPropSets(t).Any(s => s.SetEquals(want)); } private static bool AnyUniqueContains(Type t, string prop) => UniqueKeyPropSets(t).Any(s => s.Contains(prop)); [Theory] [InlineData(typeof(AdoInventoryMaster), "ado_inventory_master")] [InlineData(typeof(AdoInventoryLocationDetail), "ado_inventory_location_detail")] [InlineData(typeof(AdoInventoryTransaction), "ado_inventory_transaction")] [InlineData(typeof(AdoBarcodeCard), "ado_barcode_card")] [InlineData(typeof(AdoBarcodeCardTransaction), "ado_barcode_card_transaction")] [InlineData(typeof(AdoIqcInventoryPosting), "ado_iqc_inventory_posting")] public void Entity_HasExpectedDopOwnedTableName(Type t, string expected) { var name = TableName(t); Assert.Equal(expected, name); // DOP-owned 前缀,且不得与旧 WMS 表同名 Assert.StartsWith("ado_", name); Assert.DoesNotContain(name, new[] { "InvMaster", "LocationDetail", "InvTransHist", "MissedPrint" }); } [Theory] [InlineData(typeof(AdoInventoryMaster))] [InlineData(typeof(AdoInventoryLocationDetail))] [InlineData(typeof(AdoInventoryTransaction))] [InlineData(typeof(AdoBarcodeCard))] [InlineData(typeof(AdoBarcodeCardTransaction))] [InlineData(typeof(AdoIqcInventoryPosting))] public void Entity_HasExplicitTenantAndDomainColumns(Type t) { Assert.Equal("tenant_id", Column(t, nameof(AdoInvEntityBase.TenantId))?.ColumnName); var domain = Column(t, nameof(AdoInvEntityBase.DomainCode)); Assert.Equal("domain_code", domain?.ColumnName); Assert.False(domain.IsNullable); // domain 非空 Assert.True(typeof(AdoInvEntityBase).IsAssignableFrom(t)); // 统一基类 } [Fact] public void InventoryMaster_UniqueKey_TenantDomainItemLocation_NoLot() { // 实核:InvMaster 自然键 = Domain+ItemNum+Location(不含批次) Assert.True(HasUniqueKey(typeof(AdoInventoryMaster), nameof(AdoInvEntityBase.TenantId), nameof(AdoInvEntityBase.DomainCode), nameof(AdoInventoryMaster.ItemNum), nameof(AdoInventoryMaster.Location))); } [Fact] public void LocationDetail_UniqueKey_SixDims_And_LotRefsNotNullDefaultEmpty() { var t = typeof(AdoInventoryLocationDetail); // 实核:六维唯一 tenant+domain+item+location+lot_serial+refs Assert.True(HasUniqueKey(t, nameof(AdoInvEntityBase.TenantId), nameof(AdoInvEntityBase.DomainCode), nameof(AdoInventoryLocationDetail.ItemNum), nameof(AdoInventoryLocationDetail.Location), nameof(AdoInventoryLocationDetail.LotSerial), nameof(AdoInventoryLocationDetail.Refs))); // ⚠️ 关键陷阱:lot_serial / refs 必须 NOT NULL DEFAULT ''(空串占位唯一,非 NULL) var lot = Column(t, nameof(AdoInventoryLocationDetail.LotSerial)); Assert.Equal("lot_serial", lot.ColumnName); Assert.False(lot.IsNullable); Assert.Equal("", lot.DefaultValue); var refs = Column(t, nameof(AdoInventoryLocationDetail.Refs)); Assert.Equal("refs", refs.ColumnName); Assert.False(refs.IsNullable); Assert.Equal("", refs.DefaultValue); } [Fact] public void InventoryTransaction_StructuralDedup_GroupSeq() { // append-only;结构层防重 = tenant+transaction_group_id+seq(不替代 Phase 4 幂等) Assert.True(HasUniqueKey(typeof(AdoInventoryTransaction), nameof(AdoInvEntityBase.TenantId), nameof(AdoInventoryTransaction.TransactionGroupId), nameof(AdoInventoryTransaction.Seq))); } [Fact] public void BarcodeCard_SourceRebackfillUnique_And_BarcodeNotUnique() { var t = typeof(AdoBarcodeCard); // 回灌幂等键 tenant+source_domain+source_rec_id Assert.True(HasUniqueKey(t, nameof(AdoInvEntityBase.TenantId), nameof(AdoBarcodeCard.SourceDomain), nameof(AdoBarcodeCard.SourceRecId))); // 实核:barcode 会重复 → 绝不可作唯一 Assert.False(AnyUniqueContains(t, nameof(AdoBarcodeCard.Barcode))); } [Fact] public void IqcInventoryPosting_IdempotencyKey_TenantDomainFbillno() { // 统一 IQC 入库事件幂等键(DB UNIQUE) Assert.True(HasUniqueKey(typeof(AdoIqcInventoryPosting), nameof(AdoInvEntityBase.TenantId), nameof(AdoInvEntityBase.DomainCode), nameof(AdoIqcInventoryPosting.FbillNo))); // 过账模式默认 SHADOW(本批次最高只允许 SHADOW) Assert.Equal("SHADOW", Column(typeof(AdoIqcInventoryPosting), nameof(AdoIqcInventoryPosting.PostingMode)).DefaultValue); } }