using Newtonsoft.Json; using YLErp.DBModels; using YLErp.DBModels.Enums; namespace YLErp.Modules.SwapModule { /// /// SwapDealService 的可测试化子类(共享 stub)。 /// 继承 SwapDealService,override seam 把 DB/事务/外部服务替换为内存收集器。 /// 被 SwapUnwindScenarioTest / SwapIncomeScenarioTest 共用,避免重复。 /// public class TestableSwapDealService : SwapDealService { private readonly trade _trade; private readonly Dictionary _swapEvents; private readonly Dictionary> _flowEventsByEventId; /// 捕获 AddClientCash 的每次调用(金额, 操作, 日期) public List<(double amount, string action, DateTime date)> ClientCashCalls { get; } = new(); /// 捕获 SaveSwapDeal 的每次调用(unwindData, eventType, clientCashId) public List<(UnwindData data, int eventType, int clientCashId)> SaveSwapDealCalls { get; } = new(); public int SaveAllChangesCount; public int CloseReCheckCallCount; /// Fund 盤中基线测试输入;生产服务通过数据库查询同名 seam。 public swap_position RealtimeFloatPosition { get; set; } public eod_swap_position LatestFundEodPosition { get; set; } public bool HasCompletedFlowAfterLatestFundEod { get; set; } public List ActiveSwapPositions { get; set; } = new(); public List ExDividendInfos { get; } = new(); public TestableSwapDealService(trade td, Dictionary swapEvents = null, Dictionary> flowEventsByEventId = null) : base(new OptUserInfo(0, nameof(TestableSwapDealService), OptUserFrom.UnitTest)) { _trade = td; _swapEvents = swapEvents ?? new Dictionary(); _flowEventsByEventId = flowEventsByEventId ?? new Dictionary>(); } protected override trade FindTrade(int tradeId) => tradeId == _trade.id ? _trade : null; protected override List FindActiveSwapPositions(int tradeId) => ActiveSwapPositions; protected override swap_position FindRealtimeFloatPosition(UnwindData unwindData) => RealtimeFloatPosition; protected override eod_swap_position FindLatestFundEodPosition(int tradeId, long positionId, DateTime valueDate) => LatestFundEodPosition; protected override bool HasCompletedFlowAfterFundEod(int tradeId, long positionId, DateTime eodDate, DateTime valueDate) => HasCompletedFlowAfterLatestFundEod; protected override ex_dividend_info FindFundCorporateAction(string underlyingCode, DateTime valueDate) => ExDividendInfos.FirstOrDefault(x => x.ValidStatus && x.UnderlyingCode == underlyingCode && x.EffectiveDate == valueDate.Date); protected override List FindFundCorporateActions( string underlyingCode, DateTime eodDate, DateTime valueDate) => ExDividendInfos .Where(x => x.ValidStatus && x.UnderlyingCode == underlyingCode && x.EffectiveDate.HasValue && x.EffectiveDate.Value.Date > eodDate.Date && x.EffectiveDate.Value.Date <= valueDate.Date) .OrderBy(x => x.EffectiveDate) .ThenBy(x => x.id) .ToList(); protected override decimal GetFundCorporateActionClosePrice( ex_dividend_info dividendInfo, decimal fallbackPrice) => fallbackPrice; public bool RestoreEffectiveFundPositionForTest(UnwindData unwindData, DateTime valueDate) => TryRestoreAndValidateUnwindData(unwindData, valueDate); protected override int AddClientCash(trade td, double amount, string action, DateTime valueDate) { ClientCashCalls.Add((amount, action, valueDate)); return ClientCashCalls.Count; // 返回自增 id } // 整体 override SaveSwapDeal:收集入参,规避内部 new SwapEventService 连库 protected override long SaveSwapDeal(UnwindData unwindData, int eventType, int clientCashId, string eventResason = "", bool approve = false) { SaveSwapDealCalls.Add((unwindData, eventType, clientCashId)); return SaveSwapDealCalls.Count; // 返回自增 eventId } // ApproveSwapTrade 查待审核事件:从内存字典取(key=eventType) protected override swap_event FindSwapEvent(int tradeId, int eventType) { return _swapEvents.TryGetValue(eventType, out var evt) ? evt : null; } // ApproveSwapTrade 查事件关联流水:从内存字典取 protected override List FindFlowEventsByEventId(long eventId) { return _flowEventsByEventId.TryGetValue(eventId, out var list) ? list : new List(); } // ApplySwapTrade 的前置校验:计数,不实际执行 protected override void CloseReCheckSetTrade(int swapTradeId, bool isSwap, bool needCheck) { CloseReCheckCallCount++; } protected override void SaveAllChanges() { SaveAllChangesCount++; } protected override void ExecuteInTransaction(Action action) => action(); // 不包事务,直接执行 protected override void CallSaveSwapTradeClientCash(trade td, DateTime valueDate) { } // 空操作 protected override void TriggerRealtimeSwapPosition() { } // 空操作 } /// /// SwapDealService 测试的共享工厂方法(TestableSwapDealService + UnwindData 构造)。 /// 被 SwapUnwindScenarioTest / SwapIncomeScenarioTest 共用。 /// public static class SwapDealTestFactory { public const int SwapTradeId = 7700; public static readonly DateTime ValueDate = new(2026, 6, 15); public static readonly DateTime UnwindDate = new(2026, 6, 16); public static trade CreateTrade() { return new trade { id = SwapTradeId, TradeNumber = "UT-SD-001", ClientId = 888888, TradeType = "收益互换", StartDate = new DateTime(2026, 1, 5), ExerciseDate = new DateTime(2026, 6, 14), // 已到期边界(SwapIncome 判断用) TradeStatus = "确认成交", ValidState = "Valid", Notional = 1000000, StockEqvNotional = 1000000, TradeAmount = 10000 }; } /// 构造结息/平仓的 UnwindData(金额由前端算好传入,后端直接用) public static UnwindData CreateUnwindData(decimal swapRealizedPnL, decimal swapMarginRebatePnl = 0m, decimal swapMarginAmount = 0m, int closeMethod = 0, decimal closePercent = 0m, decimal closeQty = 0m, decimal closeNotionalValue = 0m, decimal positionQty = 0m) { return new UnwindData { SwapTradeId = SwapTradeId, SwapRealizedPnL = swapRealizedPnL, SwapMarginRebatePnl = swapMarginRebatePnl, SwapMarginAmount = swapMarginAmount, SwapCloseAmount = swapRealizedPnL, CloseMethod = closeMethod, ClosePercent = closePercent, CloseQty = closeQty, CloseNotionalValue = closeNotionalValue, PositionQty = positionQty, ValueDate = ValueDate, UnwindDate = UnwindDate, StartDate = new DateTime(2026, 1, 5) }; } public static void AssertDecimalEqual(decimal expected, decimal actual, decimal tolerance, string message = "") { Assert.IsTrue(Math.Abs(expected - actual) <= tolerance, $"{message} Expected: {expected}, Actual: {actual}, Diff: {expected - actual}"); } } }