using Admin.NET.Plugin.AiDOP.MaterialWarehouse.InventoryPosting.Inventory;
using Admin.NET.Plugin.AiDOP.MaterialWarehouse.InventoryPosting.Receipt;
namespace Admin.NET.Plugin.AiDOP.Tests.S5.MaterialWarehouse;
/// IQC 收货外层事务故障注入阶段(事务组合语义单测)。
public enum FakeReceiptFailPhase { None, C6, C5, C7 }
///
/// 测试用 IQC 收货外层工作单元:共享 FakeInventoryDatabase(每键 SemaphoreSlim 串行模拟 FOR UPDATE);
/// C6/C5 写缓冲,Commit 才落 backing(全有或全无);可在 C6/C5 阶段注入失败。
/// **事务组合语义单测,非 MySQL 实证(Phase 5B)。**
///
public sealed class FakeIqcReceiptUnitOfWork : IIqcReceiptUnitOfWork
{
private readonly FakeInventoryDatabase _db;
private readonly FakeReceiptFailPhase _failAt;
private readonly List _held = new();
private long _t;
private string _d;
private PurchaseReceiptWriteback _pendingC6;
private InventoryWriteSet _pendingWs;
private MobileTaskCommand _pendingTask;
private IqcCompletionMarker _pendingCompletion;
private bool _completed;
public FakeIqcReceiptUnitOfWork(FakeInventoryDatabase db, FakeReceiptFailPhase failAt)
{ _db = db; _failAt = failAt; }
public async Task BeginAndLockAsync(IReadOnlyList invCmds)
{
_t = invCmds[0].TenantId;
_d = invCmds[0].DomainCode ?? string.Empty;
var keys = new SortedSet(StringComparer.Ordinal);
foreach (var c in invCmds)
{
keys.Add("INV:" + FakeInventoryDatabase.InvK(_t, _d, c.ItemNum, c.Location));
keys.Add("DET:" + FakeInventoryDatabase.DetK(_t, _d, c.ItemNum, c.Location, c.LotSerial ?? "", c.Refs ?? ""));
}
foreach (var k in keys) { var s = _db.LockFor(k); await s.WaitAsync(); _held.Add(s); }
var ctx = new InventoryContext();
foreach (var item in invCmds.Select(c => c.ItemNum).Distinct(StringComparer.Ordinal))
if (_db.Items.Contains(FakeInventoryDatabase.ItemK(_t, _d, item))) ctx.Items.Add(item);
foreach (var loc in invCmds.Select(c => c.Location).Distinct(StringComparer.Ordinal))
if (_db.Locations.Contains(FakeInventoryDatabase.LocK(_t, _d, loc))) ctx.Locations.Add(loc);
foreach (var c in invCmds)
{
if (_db.InvMaster.TryGetValue(FakeInventoryDatabase.InvK(_t, _d, c.ItemNum, c.Location), out var b))
ctx.InvMasters[InventoryContext.InvKey(c.ItemNum, c.Location)] = Clone(b);
if (_db.LocationDetail.TryGetValue(FakeInventoryDatabase.DetK(_t, _d, c.ItemNum, c.Location, c.LotSerial ?? "", c.Refs ?? ""), out var dd))
ctx.LocationDetails[InventoryContext.DetailKey(c.ItemNum, c.Location, c.LotSerial ?? "", c.Refs ?? "")] = Clone(dd);
if (_db.ItemLocation.TryGetValue(FakeInventoryDatabase.ItemK(_t, _d, c.ItemNum), out var loc))
ctx.ItemDefaultLocation[c.ItemNum] = loc;
}
return ctx;
}
public Task BeginAsync(long tenantId, string domainCode)
{
_t = tenantId;
_d = domainCode ?? string.Empty;
return Task.CompletedTask; // 任务-only:仅开事务,无库存锁
}
public Task WritePurchaseReceiptAsync(PurchaseReceiptWriteback c6)
{
_db.CallOrder.Add("C6");
if (_failAt == FakeReceiptFailPhase.C6) throw new InvalidOperationException("inject-fail:C6");
_pendingC6 = c6;
return Task.CompletedTask;
}
public Task WriteMobileTaskAsync(MobileTaskCommand task)
{
_db.CallOrder.Add("C7");
if (_failAt == FakeReceiptFailPhase.C7) throw new InvalidOperationException("inject-fail:C7");
_pendingTask = task;
return Task.CompletedTask;
}
public Task WriteInventoryAsync(InventoryWriteSet writeSet)
{
_db.CallOrder.Add("C5");
if (_failAt == FakeReceiptFailPhase.C5) throw new InvalidOperationException("inject-fail:C5");
_pendingWs = writeSet;
return Task.CompletedTask;
}
public Task WriteCompletionAsync(IqcCompletionMarker marker)
{
_db.CallOrder.Add("COMPLETE");
_pendingCompletion = marker; // 缓冲,Commit 才落(与业务写同成同败)
return Task.CompletedTask;
}
public Task CommitAsync()
{
if (_completed) return Task.CompletedTask;
_completed = true;
if (_pendingC6 != null)
{
var key = $"{_pendingC6.TenantId}|{_pendingC6.DomainCode}|{_pendingC6.PostingId}|{_pendingC6.PurOrd}|{_pendingC6.PurLine}";
_db.ReceiptWriteback[key] = _pendingC6;
}
if (_pendingWs != null) ApplyInventory(_pendingWs);
if (_pendingTask != null)
_db.MobileTasks[$"{_pendingTask.TenantId}|{_pendingTask.DomainCode}|{_pendingTask.TaskId}"] = _pendingTask;
if (_pendingCompletion != null)
_db.Completions[$"{_pendingCompletion.TenantId}|{_pendingCompletion.DomainCode}|{_pendingCompletion.FbillNo}"] = _pendingCompletion;
ReleaseLocks();
return Task.CompletedTask;
}
public Task RollbackAsync()
{
if (_completed) return Task.CompletedTask;
_completed = true; // 丢弃 _pendingC6 / _pendingWs
ReleaseLocks();
return Task.CompletedTask;
}
public ValueTask DisposeAsync()
{
if (!_completed) { _completed = true; ReleaseLocks(); }
return default;
}
private void ApplyInventory(InventoryWriteSet ws)
{
foreach (var w in ws.InvMasterWrites)
_db.InvMaster[FakeInventoryDatabase.InvK(w.TenantId, w.DomainCode ?? "", w.ItemNum, w.Location)] = w.After;
foreach (var w in ws.ItemLocationWrites)
_db.ItemLocation[FakeInventoryDatabase.ItemK(w.TenantId, w.DomainCode ?? "", w.ItemNum)] = w.DefaultLocation;
foreach (var w in ws.LocationDetailWrites)
_db.LocationDetail[FakeInventoryDatabase.DetK(w.TenantId, w.DomainCode ?? "", w.ItemNum, w.Location, w.LotSerial ?? "", w.Refs ?? "")] = w.After;
foreach (var r in ws.TransactionRows)
_db.Transactions.Add(r);
}
private void ReleaseLocks()
{
for (var i = _held.Count - 1; i >= 0; i--) _held[i].Release();
_held.Clear();
}
private static InventoryBalance Clone(InventoryBalance b)
=> new InventoryBalance { QtyOnHand = b.QtyOnHand, AvailStatusQty = b.AvailStatusQty, Assay = b.Assay, FreezeQty = b.FreezeQty, QtyOnOrd = b.QtyOnOrd };
}
public sealed class FakeIqcReceiptUnitOfWorkFactory : IIqcReceiptUnitOfWorkFactory
{
private readonly FakeInventoryDatabase _db;
private readonly FakeReceiptFailPhase _failAt;
public FakeIqcReceiptUnitOfWorkFactory(FakeInventoryDatabase db, FakeReceiptFailPhase failAt = FakeReceiptFailPhase.None)
{ _db = db; _failAt = failAt; }
public IIqcReceiptUnitOfWork Create() => new FakeIqcReceiptUnitOfWork(_db, _failAt);
}