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);
}
}