using Newtonsoft.Json;
using YLErp.DBModels;
using YLErp.DBModels.Enums;
using YLErp.Modules.SwapModule.Margin;
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++;
}
// R4 按标签分流释放:纯内存测试不连库,stub 为全现金(与既有断言语义一致)
protected override UnwindTagSplit ReleaseMarginByFundTag(trade td, DateTime valueDate, List interestEvents,
decimal marginAmount, decimal marginRebate)
=> new UnwindTagSplit { CashMargin = Convert.ToDouble(marginAmount), CashRebate = Convert.ToDouble(marginRebate) };
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}");
}
}
}