| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576 |
- using Admin.NET.Plugin.AiDOP.MaterialWarehouse.InventoryPosting.Inventory;
- using Admin.NET.Plugin.AiDOP.MaterialWarehouse.InventoryPosting.Receipt;
- using Xunit;
- namespace Admin.NET.Plugin.AiDOP.Tests.S5.MaterialWarehouse;
- /// <summary>
- /// Phase 6C-1:证明 G1-G9 守卫由 <see cref="InventoryTransactionService"/> 拥有(standalone 与 composed 均由 C5 返回),
- /// Orchestrator 不构造写集绕过守卫。
- /// </summary>
- public class InventoryC5OwnershipTests
- {
- private const long Tenant = 797403760988229L;
- private static InventoryTransactionCommand Out(decimal qty, string item = "IT-1", string loc = "L1")
- => new InventoryTransactionCommand
- {
- TenantId = Tenant, DomainCode = "8010", PostingId = 1, TransactionGroupId = 100, Seq = 1,
- TransType = "iss-x", ChangeQty = qty, UmConversion = 1m, ItemNum = item, Location = loc, User = "t",
- };
- private static InventoryContext CtxStocked(decimal avail)
- {
- var ctx = new InventoryContext();
- ctx.Items.Add("IT-1");
- ctx.Locations.Add("L1");
- ctx.InvMasters[InventoryContext.InvKey("IT-1", "L1")] = new InventoryBalance { QtyOnHand = avail, AvailStatusQty = avail, Assay = avail, FreezeQty = avail };
- ctx.LocationDetails[InventoryContext.DetailKey("IT-1", "L1", "", "")] = new InventoryBalance { QtyOnHand = avail, AvailStatusQty = avail, Assay = avail, FreezeQty = avail };
- return ctx;
- }
- // ① C5 standalone:库存不足 → G9 失败(不经 Orchestrator)
- [Fact]
- public async Task C5Standalone_G9_InsufficientStock_Fails()
- {
- var db = new FakeInventoryDatabase();
- db.SeedStock(Tenant, "8010", "IT-1", "L1", 50m); // avail=50
- var svc = new InventoryTransactionService(new FakeInventoryUnitOfWorkFactory(db));
- var r = await svc.PostAsync(new[] { Out(-80m) }); // 出 80 > 50
- Assert.Equal(InventoryPostingOutcome.ValidationFailure, r.Outcome);
- Assert.Equal(InventoryGuardCode.IssueExceedsAvail, r.GuardErrors[0].Code);
- Assert.Equal(50m, db.LocationDetail[FakeInventoryDatabase.DetK(Tenant, "8010", "IT-1", "L1", "", "")].AvailStatusQty); // 未变
- }
- // ② C5 composed(ExecuteWithinAsync):库存不足 → G9 失败,且 writeInventory 未被调用(不写)
- [Fact]
- public async Task C5Composed_ExecuteWithin_G9_Fails_NoWriteIssued()
- {
- var written = false;
- var r = await InventoryTransactionService.ExecuteWithinAsync(
- CtxStocked(50m), new[] { Out(-80m) },
- writeInventory: _ => { written = true; return Task.CompletedTask; });
- Assert.Equal(InventoryPostingOutcome.ValidationFailure, r.Outcome);
- Assert.Equal(InventoryGuardCode.IssueExceedsAvail, r.GuardErrors[0].Code);
- Assert.False(written); // 守卫先失败 → 未落写(不绕过)
- }
- // ③ Orchestrator 的守卫失败结果来自 C5(GuardErrors 透传自 ExecuteWithinAsync)
- [Fact]
- public async Task Orchestrator_GuardFailure_PropagatesC5GuardErrors()
- {
- var db = new FakeInventoryDatabase(); // 无 master → G2(由 C5 的 ExecuteWithinAsync 判定)
- var orch = new IqcReceiptOrchestrator(new FakeIqcReceiptUnitOfWorkFactory(db));
- var receipt = new IqcAcceptedReceipt
- {
- TenantId = Tenant, DomainCode = "8010", PostingId = 1, TransactionGroupId = 100, RctNbr = "RCT-1",
- ItemNum = "IT-1", Location = "L1", AcceptedQty = 100m, UmConversion = 1m, PurOrd = "PO-1", PurLine = 1, User = "t",
- };
- var gate = new PurchaseReceiptGateInput { HasPendingReceipt = true, AcceptedQty = 100m, HasReceivableDetails = true };
- var r = await orch.PostAsync(receipt, gate);
- Assert.Equal(IqcReceiptOutcome.ValidationFailure, r.Outcome);
- Assert.NotEmpty(r.GuardErrors); // 守卫错误来自 C5
- Assert.Equal(InventoryGuardCode.ItemMasterMissing, r.GuardErrors[0].Code);
- Assert.Equal(0, db.TotalPersisted); // C6 也回滚
- }
- }
|