| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141 |
- using Admin.NET.Plugin.AiDOP.MaterialWarehouse.InventoryPosting.Inventory;
- using Xunit;
- namespace Admin.NET.Plugin.AiDOP.Tests.S5.MaterialWarehouse;
- /// <summary>
- /// C5 InventoryTransactionService 编排测试(Phase 5A/5C):写集路由 + 守卫 0 写 + 故障注入全回滚(事务语义单测)。
- /// 单事务/锁边界经 UnitOfWork;故障注入为事务语义单测,非 MySQL rollback 实证(Phase 5B)。
- /// </summary>
- public class InventoryTransactionServiceTests
- {
- private const long Tenant = 797403760988229L;
- private static InventoryTransactionCommand Cmd(string tt, decimal qty, string item = "IT-1", string loc = "L1", string lot = "", string refs = "")
- => new InventoryTransactionCommand
- {
- TenantId = Tenant, DomainCode = "8010",
- PostingId = 1, TransactionGroupId = 100, Seq = 1,
- TransType = tt, ChangeQty = qty, UmConversion = 1m,
- ItemNum = item, Location = loc, LotSerial = lot, Refs = refs, User = "t",
- };
- // ── InventoryContext(供 BuildWriteSet 纯路由测试)──
- private static InventoryContext EmptyCtxWithMaster(string item = "IT-1", string loc = "L1")
- {
- var ctx = new InventoryContext();
- ctx.Items.Add(item);
- ctx.Locations.Add(loc);
- return ctx;
- }
- private static InventoryContext StockedCtx(string item = "IT-1", string loc = "L1", decimal stock = 500m)
- {
- var ctx = new InventoryContext();
- ctx.Items.Add(item);
- ctx.Locations.Add(loc);
- ctx.InvMasters[InventoryContext.InvKey(item, loc)] = new InventoryBalance { QtyOnHand = stock, AvailStatusQty = stock, Assay = stock, FreezeQty = stock };
- ctx.LocationDetails[InventoryContext.DetailKey(item, loc, "", "")] = new InventoryBalance { QtyOnHand = stock, AvailStatusQty = stock, Assay = stock, FreezeQty = stock };
- ctx.ItemDefaultLocation[item] = loc;
- return ctx;
- }
- // ── FakeInventoryDatabase(供 PostAsync 编排测试)──
- private static FakeInventoryDatabase DbWithMaster()
- {
- var db = new FakeInventoryDatabase();
- db.SeedMaster(Tenant, "8010", "IT-1", "L1");
- return db;
- }
- private static InventoryTransactionService Svc(FakeInventoryDatabase db, FakeInventoryFailPhase fail = FakeInventoryFailPhase.None)
- => new InventoryTransactionService(new FakeInventoryUnitOfWorkFactory(db, fail));
- // ═══ 路由:INSERT vs UPDATE(BuildWriteSet 纯计算)═══
- [Fact]
- public void BuildWriteSet_InboundToEmpty_RoutesInsert()
- {
- var ws = InventoryTransactionService.BuildWriteSet(new[] { Cmd("rct-po-ins", 100m) }, EmptyCtxWithMaster());
- Assert.True(Assert.Single(ws.InvMasterWrites).InsertIfMissing);
- Assert.True(Assert.Single(ws.LocationDetailWrites).InsertIfMissing);
- Assert.Single(ws.ItemLocationWrites);
- Assert.Single(ws.TransactionRows);
- }
- [Fact]
- public void BuildWriteSet_OutboundToExisting_RoutesUpdateOnly()
- {
- var ws = InventoryTransactionService.BuildWriteSet(new[] { Cmd("iss-x", -50m) }, StockedCtx());
- Assert.False(Assert.Single(ws.InvMasterWrites).InsertIfMissing);
- Assert.False(Assert.Single(ws.LocationDetailWrites).InsertIfMissing);
- Assert.Empty(ws.ItemLocationWrites);
- }
- [Fact]
- public void BuildWriteSet_RctPoIns_AfterBalances_MatchLive()
- {
- var ws = InventoryTransactionService.BuildWriteSet(new[] { Cmd("rct-po-ins", 40m) }, StockedCtx(stock: 100m));
- var inv = Assert.Single(ws.InvMasterWrites);
- Assert.Equal(140m, inv.After.QtyOnHand);
- Assert.Equal(100m, inv.After.AvailStatusQty);
- Assert.Equal(140m, inv.After.Assay);
- }
- [Fact]
- public void BuildWriteSet_NonLot_LotSerialEmpty()
- => Assert.Equal("", Assert.Single(
- InventoryTransactionService.BuildWriteSet(new[] { Cmd("rct-po-ins", 10m) }, EmptyCtxWithMaster()).LocationDetailWrites).LotSerial);
- [Fact]
- public void BuildWriteSet_Lot_PreservesLotSerial()
- => Assert.Equal("LOT-A", Assert.Single(
- InventoryTransactionService.BuildWriteSet(new[] { Cmd("rct-po-ins", 10m, lot: "LOT-A") }, EmptyCtxWithMaster()).LocationDetailWrites).LotSerial);
- // ═══ PostAsync 编排 ═══
- [Fact]
- public async Task Post_GuardFailure_ZeroWrites()
- {
- var db = new FakeInventoryDatabase(); // 无 master → G2
- var r = await Svc(db).PostAsync(new[] { Cmd("rct-po-ins", 10m) });
- Assert.Equal(InventoryPostingOutcome.ValidationFailure, r.Outcome);
- Assert.Equal(0, r.WrittenRows);
- Assert.Equal(0, db.TotalPersisted);
- Assert.NotEmpty(r.GuardErrors);
- }
- [Fact]
- public async Task Post_Success_PersistsAllFour()
- {
- var db = DbWithMaster();
- var r = await Svc(db).PostAsync(new[] { Cmd("rct-po-ins", 100m) });
- Assert.Equal(InventoryPostingOutcome.Success, r.Outcome);
- Assert.Equal("保存成功", r.LegacyReturnMsg);
- Assert.Equal(1, db.InvMaster.Count);
- Assert.Equal(1, db.LocationDetail.Count);
- Assert.Equal(1, db.ItemLocation.Count);
- Assert.Equal(1, db.Transactions.Count);
- }
- [Theory]
- [InlineData(FakeInventoryFailPhase.InvMaster)]
- [InlineData(FakeInventoryFailPhase.ItemLocation)]
- [InlineData(FakeInventoryFailPhase.LocationDetail)]
- [InlineData(FakeInventoryFailPhase.Transaction)]
- public async Task Post_FailureInjection_AllRollBack(FakeInventoryFailPhase phase)
- {
- var db = DbWithMaster();
- var r = await Svc(db, phase).PostAsync(new[] { Cmd("rct-po-ins", 100m) });
- Assert.Equal(InventoryPostingOutcome.Error, r.Outcome);
- Assert.Equal(0, r.WrittenRows);
- Assert.Equal(0, db.TotalPersisted);
- Assert.StartsWith("保存失败,", r.LegacyReturnMsg);
- }
- [Fact]
- public async Task Post_EmptyCommands_SuccessZero()
- {
- var r = await Svc(new FakeInventoryDatabase()).PostAsync(System.Array.Empty<InventoryTransactionCommand>());
- Assert.Equal(InventoryPostingOutcome.Success, r.Outcome);
- Assert.Equal(0, r.WrittenRows);
- }
- }
|