AdoInventoryEntitiesStructureTests.cs 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132
  1. using System.Reflection;
  2. using Admin.NET.Plugin.AiDOP.MaterialWarehouse.InventoryPosting.Entity;
  3. using SqlSugar;
  4. using Xunit;
  5. namespace Admin.NET.Plugin.AiDOP.Tests.S5.MaterialWarehouse;
  6. /// <summary>
  7. /// Phase 2 结构级验证(反射,无 DB):断言 L3 影子账实体的表名、复合唯一键维度、
  8. /// tenant_id + domain_code 列、lot_serial/refs 空批次唯一语义(NOT NULL DEFAULT '')、条码非唯一。
  9. /// 复合唯一键维度全部据 Schema 实核(LIVE SP 真定义)确定,此测试把「唯一键契约」固化。
  10. /// </summary>
  11. public class AdoInventoryEntitiesStructureTests
  12. {
  13. private static string TableName(Type t)
  14. => t.GetCustomAttribute<SugarTable>()?.TableName;
  15. private static SugarColumn Column(Type t, string prop)
  16. => t.GetProperty(prop)?.GetCustomAttribute<SugarColumn>();
  17. private static List<HashSet<string>> UniqueKeyPropSets(Type t)
  18. => t.GetCustomAttributes<SugarIndexAttribute>()
  19. .Where(ix => ix.IsUnique)
  20. .Select(ix => new HashSet<string>(ix.IndexFields.Keys))
  21. .ToList();
  22. private static bool HasUniqueKey(Type t, params string[] props)
  23. {
  24. var want = new HashSet<string>(props);
  25. return UniqueKeyPropSets(t).Any(s => s.SetEquals(want));
  26. }
  27. private static bool AnyUniqueContains(Type t, string prop)
  28. => UniqueKeyPropSets(t).Any(s => s.Contains(prop));
  29. [Theory]
  30. [InlineData(typeof(AdoInventoryMaster), "ado_inventory_master")]
  31. [InlineData(typeof(AdoInventoryLocationDetail), "ado_inventory_location_detail")]
  32. [InlineData(typeof(AdoInventoryTransaction), "ado_inventory_transaction")]
  33. [InlineData(typeof(AdoBarcodeCard), "ado_barcode_card")]
  34. [InlineData(typeof(AdoBarcodeCardTransaction), "ado_barcode_card_transaction")]
  35. [InlineData(typeof(AdoIqcInventoryPosting), "ado_iqc_inventory_posting")]
  36. public void Entity_HasExpectedDopOwnedTableName(Type t, string expected)
  37. {
  38. var name = TableName(t);
  39. Assert.Equal(expected, name);
  40. // DOP-owned 前缀,且不得与旧 WMS 表同名
  41. Assert.StartsWith("ado_", name);
  42. Assert.DoesNotContain(name, new[] { "InvMaster", "LocationDetail", "InvTransHist", "MissedPrint" });
  43. }
  44. [Theory]
  45. [InlineData(typeof(AdoInventoryMaster))]
  46. [InlineData(typeof(AdoInventoryLocationDetail))]
  47. [InlineData(typeof(AdoInventoryTransaction))]
  48. [InlineData(typeof(AdoBarcodeCard))]
  49. [InlineData(typeof(AdoBarcodeCardTransaction))]
  50. [InlineData(typeof(AdoIqcInventoryPosting))]
  51. public void Entity_HasExplicitTenantAndDomainColumns(Type t)
  52. {
  53. Assert.Equal("tenant_id", Column(t, nameof(AdoInvEntityBase.TenantId))?.ColumnName);
  54. var domain = Column(t, nameof(AdoInvEntityBase.DomainCode));
  55. Assert.Equal("domain_code", domain?.ColumnName);
  56. Assert.False(domain.IsNullable); // domain 非空
  57. Assert.True(typeof(AdoInvEntityBase).IsAssignableFrom(t)); // 统一基类
  58. }
  59. [Fact]
  60. public void InventoryMaster_UniqueKey_TenantDomainItemLocation_NoLot()
  61. {
  62. // 实核:InvMaster 自然键 = Domain+ItemNum+Location(不含批次)
  63. Assert.True(HasUniqueKey(typeof(AdoInventoryMaster),
  64. nameof(AdoInvEntityBase.TenantId), nameof(AdoInvEntityBase.DomainCode),
  65. nameof(AdoInventoryMaster.ItemNum), nameof(AdoInventoryMaster.Location)));
  66. }
  67. [Fact]
  68. public void LocationDetail_UniqueKey_SixDims_And_LotRefsNotNullDefaultEmpty()
  69. {
  70. var t = typeof(AdoInventoryLocationDetail);
  71. // 实核:六维唯一 tenant+domain+item+location+lot_serial+refs
  72. Assert.True(HasUniqueKey(t,
  73. nameof(AdoInvEntityBase.TenantId), nameof(AdoInvEntityBase.DomainCode),
  74. nameof(AdoInventoryLocationDetail.ItemNum), nameof(AdoInventoryLocationDetail.Location),
  75. nameof(AdoInventoryLocationDetail.LotSerial), nameof(AdoInventoryLocationDetail.Refs)));
  76. // ⚠️ 关键陷阱:lot_serial / refs 必须 NOT NULL DEFAULT ''(空串占位唯一,非 NULL)
  77. var lot = Column(t, nameof(AdoInventoryLocationDetail.LotSerial));
  78. Assert.Equal("lot_serial", lot.ColumnName);
  79. Assert.False(lot.IsNullable);
  80. Assert.Equal("", lot.DefaultValue);
  81. var refs = Column(t, nameof(AdoInventoryLocationDetail.Refs));
  82. Assert.Equal("refs", refs.ColumnName);
  83. Assert.False(refs.IsNullable);
  84. Assert.Equal("", refs.DefaultValue);
  85. }
  86. [Fact]
  87. public void InventoryTransaction_StructuralDedup_GroupSeq()
  88. {
  89. // append-only;结构层防重 = tenant+transaction_group_id+seq(不替代 Phase 4 幂等)
  90. Assert.True(HasUniqueKey(typeof(AdoInventoryTransaction),
  91. nameof(AdoInvEntityBase.TenantId),
  92. nameof(AdoInventoryTransaction.TransactionGroupId),
  93. nameof(AdoInventoryTransaction.Seq)));
  94. }
  95. [Fact]
  96. public void BarcodeCard_SourceRebackfillUnique_And_BarcodeNotUnique()
  97. {
  98. var t = typeof(AdoBarcodeCard);
  99. // 回灌幂等键 tenant+source_domain+source_rec_id
  100. Assert.True(HasUniqueKey(t,
  101. nameof(AdoInvEntityBase.TenantId),
  102. nameof(AdoBarcodeCard.SourceDomain),
  103. nameof(AdoBarcodeCard.SourceRecId)));
  104. // 实核:barcode 会重复 → 绝不可作唯一
  105. Assert.False(AnyUniqueContains(t, nameof(AdoBarcodeCard.Barcode)));
  106. }
  107. [Fact]
  108. public void IqcInventoryPosting_IdempotencyKey_TenantDomainFbillno()
  109. {
  110. // 统一 IQC 入库事件幂等键(DB UNIQUE)
  111. Assert.True(HasUniqueKey(typeof(AdoIqcInventoryPosting),
  112. nameof(AdoInvEntityBase.TenantId), nameof(AdoInvEntityBase.DomainCode),
  113. nameof(AdoIqcInventoryPosting.FbillNo)));
  114. // 过账模式默认 SHADOW(本批次最高只允许 SHADOW)
  115. Assert.Equal("SHADOW", Column(typeof(AdoIqcInventoryPosting), nameof(AdoIqcInventoryPosting.PostingMode)).DefaultValue);
  116. }
  117. }