test(swap): 补ApproveSwapTrade/ApplySwapTrade测试+3个seam(借鉴testable分支)
借鉴 origin/glms/feature/refactor-swap-event-testable 的 SwapUnwindScenarioTest 场景4/5, 补全当前分支缺失的平仓审批流程覆盖。 SwapDealService 新增3个 protected virtual seam: - FindSwapEvent(查待审核事件) - FindFlowEventsByEventId(查事件关联流水) - CloseReCheckSetTrade(前置校验包装) ApproveSwapTrade/ApplySwapTrade 重构为调seam,生产行为不变。 SwapDealSettlementTest 新增2个测试(SD_005/006,共6个): - SD_005 ApproveSwapTrade 全平仓审核: 反序列化swap_event.EventData→资金流水→TradeStatus - SD_006 ApplySwapTrade 提交审核: CloseReCheck前置校验+SwapRealizedPnL=SwapCloseAmount 融合策略: 保留当前分支SwapIncome资金流水断言独特价值(SD_001/002), 借鉴testable分支审批流程覆盖(SD_005/006),不二选一。 SwapModule 157测试全绿,无回归。
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
using Newtonsoft.Json;
|
||||
using YLErp.DBModels;
|
||||
using YLErp.DBModels.Enums;
|
||||
|
||||
@@ -32,13 +33,21 @@ namespace YLErp.Modules.SwapModule
|
||||
private sealed class StubDealService : SwapDealService
|
||||
{
|
||||
private readonly trade _trade;
|
||||
private readonly Dictionary<int, swap_event> _swapEvents;
|
||||
private readonly Dictionary<long, List<swap_flow_event>> _flowEventsByEventId;
|
||||
public List<(double amount, string action, DateTime date)> ClientCashCalls = new();
|
||||
public List<(UnwindData data, int eventType, int clientCashId)> SaveSwapDealCalls = new();
|
||||
public int SaveAllChangesCount;
|
||||
public int CloseReCheckCallCount;
|
||||
|
||||
public StubDealService(trade td) : base(new OptUserInfo(0, nameof(SwapDealSettlementTest), OptUserFrom.UnitTest))
|
||||
public StubDealService(trade td,
|
||||
Dictionary<int, swap_event> swapEvents = null,
|
||||
Dictionary<long, List<swap_flow_event>> flowEventsByEventId = null)
|
||||
: base(new OptUserInfo(0, nameof(SwapDealSettlementTest), OptUserFrom.UnitTest))
|
||||
{
|
||||
_trade = td;
|
||||
_swapEvents = swapEvents ?? new Dictionary<int, swap_event>();
|
||||
_flowEventsByEventId = flowEventsByEventId ?? new Dictionary<long, List<swap_flow_event>>();
|
||||
}
|
||||
|
||||
protected override trade FindTrade(int tradeId) => tradeId == _trade.id ? _trade : null;
|
||||
@@ -56,6 +65,24 @@ namespace YLErp.Modules.SwapModule
|
||||
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<swap_flow_event> FindFlowEventsByEventId(long eventId)
|
||||
{
|
||||
return _flowEventsByEventId.TryGetValue(eventId, out var list) ? list : new List<swap_flow_event>();
|
||||
}
|
||||
|
||||
// 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) { } // 空操作
|
||||
@@ -247,5 +274,86 @@ namespace YLErp.Modules.SwapModule
|
||||
$"TradingAmount 应=ExitDirtyPrice(1.02)×CloseQty(1000)=1020,实际={closeEvent.TradingAmount}");
|
||||
Console.WriteLine($"SD_004 通过:ExitDirtyFeePrice={closeEvent.TradingAmountFeeAvg},ExitCleanFeePrice={closeEvent.TradingAmountNetFeeAvg},TradingAmount={closeEvent.TradingAmount} ✅");
|
||||
}
|
||||
|
||||
// ================================================================
|
||||
// SD_005:ApproveSwapTrade 审核通过 —— 反序列化事件、资金流水、持仓状态
|
||||
// ================================================================
|
||||
|
||||
/// <summary>
|
||||
/// [SD_005] ApproveSwapTrade 审核通过全部平仓
|
||||
/// ------------------------------------------------------------
|
||||
/// 后端 SwapDealService.ApproveSwapTrade:从 swap_event.EventData 反序列化 UnwindData,
|
||||
/// 据此生成资金流水 + 更新持仓状态。
|
||||
/// 借鉴 testable 分支 SwapUnwindScenarioTest.Scenario4,验证:
|
||||
/// - SwapRealizedPnL 从事件反序列化正确(EventData JSON)
|
||||
/// - 资金流水金额 = -SwapRealizedPnL
|
||||
/// - 全平仓 → TradeStatus=已平仓
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public void SD_005_ApproveSwapTrade_全平仓审核_反序列化事件并记账()
|
||||
{
|
||||
var td = CreateTrade();
|
||||
// 构造待审核事件:EventData 里序列化了 UnwindData(含 SwapRealizedPnL=8000)
|
||||
var unwindData = CreateUnwindData(swapRealizedPnL: 8000m,
|
||||
closeMethod: (int)CloseMethodEnum.全部平仓, closePercent: 1m,
|
||||
closeQty: 10000m, closeNotionalValue: 1000000m);
|
||||
var swapEvent = new swap_event
|
||||
{
|
||||
id = 1, SwapTradeId = SwapTradeId,
|
||||
EventType = (int)SwapEventTypeEnum.平仓, Invalid = false,
|
||||
EventData = JsonConvert.SerializeObject(unwindData)
|
||||
};
|
||||
var flowEvents = new Dictionary<long, List<swap_flow_event>>
|
||||
{
|
||||
[1] = new List<swap_flow_event> { new swap_flow_event { id = 1, EventId = 1, PositionId = 1 } }
|
||||
};
|
||||
var service = new StubDealService(td,
|
||||
swapEvents: new Dictionary<int, swap_event> { [(int)SwapEventTypeEnum.平仓] = swapEvent },
|
||||
flowEventsByEventId: flowEvents);
|
||||
|
||||
service.ApproveSwapTrade(td, (int)SwapEventTypeEnum.平仓);
|
||||
|
||||
// 资金流水:从反序列化的 SwapRealizedPnL(8000) 记账 → -8000
|
||||
Assert.AreEqual(1, service.ClientCashCalls.Count, "全平仓无预付金时应1条资金流水");
|
||||
Assert.AreEqual(-8000.0, service.ClientCashCalls[0].amount, 0.001, "资金流水=-反序列化的SwapRealizedPnL");
|
||||
// 持仓状态
|
||||
Assert.AreEqual("已平仓", td.TradeStatus, "审核全平仓 TradeStatus=已平仓");
|
||||
Console.WriteLine($"SD_005 通过:审核反序列化 SwapRealizedPnL=8000,资金流水={service.ClientCashCalls[0].amount},TradeStatus={td.TradeStatus} ✅");
|
||||
}
|
||||
|
||||
// ================================================================
|
||||
// SD_006:ApplySwapTrade 提交审核 —— 前置校验 + 保存事件
|
||||
// ================================================================
|
||||
|
||||
/// <summary>
|
||||
/// [SD_006] ApplySwapTrade 提交审核
|
||||
/// ------------------------------------------------------------
|
||||
/// 后端 SwapDealService.ApplySwapTrade:调 CloseReCheckSetTrade 前置校验 + SaveSwapDeal(approve=true)。
|
||||
/// 借鉴 testable 分支 SwapUnwindScenarioTest.Scenario5,验证:
|
||||
/// - CloseReCheckSetTrade 被调用1次
|
||||
/// - SaveSwapDeal 以 approve=true 调用(事件类型正确)
|
||||
/// - SwapRealizedPnL = SwapCloseAmount(ApplySwapTrade 内部赋值)
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public void SD_006_ApplySwapTrade_提交审核_前置校验与保存事件()
|
||||
{
|
||||
var td = CreateTrade();
|
||||
var service = new StubDealService(td);
|
||||
// 前端提交时 SwapCloseAmount=6000(前端算好的总额),SwapRealizedPnL 初始可能为0
|
||||
var unwindData = CreateUnwindData(swapRealizedPnL: 0m);
|
||||
unwindData.SwapCloseAmount = 6000m; // 模拟前端传入的平仓总额
|
||||
|
||||
service.ApplySwapTrade(unwindData, (int)SwapEventTypeEnum.平仓);
|
||||
|
||||
// 前置校验被调用
|
||||
Assert.AreEqual(1, service.CloseReCheckCallCount, "应调用 CloseReCheckSetTrade 1次");
|
||||
// SaveSwapDeal 以 approve=true 调用
|
||||
Assert.AreEqual(1, service.SaveSwapDealCalls.Count, "应调用 SaveSwapDeal 1次");
|
||||
Assert.AreEqual((int)SwapEventTypeEnum.平仓, service.SaveSwapDealCalls[0].eventType, "事件类型=平仓");
|
||||
// SwapRealizedPnL 应被赋值为 SwapCloseAmount(ApplySwapTrade 内部 cs:1631)
|
||||
Assert.AreEqual(6000m, service.SaveSwapDealCalls[0].data.SwapRealizedPnL, 0.001m,
|
||||
"SwapRealizedPnL 应=SwapCloseAmount(6000)");
|
||||
Console.WriteLine($"SD_006 通过:CloseReCheck 调用{service.CloseReCheckCallCount}次,SwapRealizedPnL={service.SaveSwapDealCalls[0].data.SwapRealizedPnL} ✅");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -95,6 +95,26 @@ namespace YLErp.Modules.SwapModule
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>查找待审核的互换/平仓事件(生产: DbContext.swap_event 查询;测试: 返回内存对象)</summary>
|
||||
protected virtual swap_event FindSwapEvent(int tradeId, int eventType)
|
||||
{
|
||||
return DbContext.swap_event
|
||||
.Where(x => x.SwapTradeId == tradeId && !x.Invalid && x.EventType == eventType)
|
||||
.OrderByDescending(o => o.id).FirstOrDefault();
|
||||
}
|
||||
|
||||
/// <summary>查找事件关联的流水事件(生产: DbContext.swap_flow_event 查询;测试: 返回内存列表)</summary>
|
||||
protected virtual List<swap_flow_event> FindFlowEventsByEventId(long eventId)
|
||||
{
|
||||
return DbContext.swap_flow_event.Where(x => x.EventId == eventId).ToList();
|
||||
}
|
||||
|
||||
/// <summary>平仓/互换审核的前置校验与状态设置(生产: new TradeUnwindService;测试: 空操作或计数)</summary>
|
||||
protected virtual void CloseReCheckSetTrade(int swapTradeId, bool isSwap, bool needCheck)
|
||||
{
|
||||
new TradeUnwindService(this).CloseReCheck_SetTrade(swapTradeId, isSwap, needCheck);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
public SwapDealService(OptUserInfo optUser) : base(optUser)
|
||||
@@ -1578,14 +1598,13 @@ namespace YLErp.Modules.SwapModule
|
||||
/// <exception cref="Exception"></exception>
|
||||
public void ApproveSwapTrade(trade td, int eventType)
|
||||
{
|
||||
Expression<Func<swap_event, bool>> eventExpression = x => x.SwapTradeId == td.id && !x.Invalid && x.EventType == eventType;
|
||||
var swapEvent = DbContext.swap_event.Where(eventExpression).OrderByDescending(o => o.id).FirstOrDefault();
|
||||
var swapEvent = FindSwapEvent(td.id, eventType);
|
||||
if (swapEvent == null)
|
||||
{
|
||||
throw new Exception("该笔交易状态为平仓待复核,未找到相关记录,请检查该笔交易是否有效");
|
||||
}
|
||||
swapEvent.unwindData = JsonConvert.DeserializeObject<UnwindData>(swapEvent.EventData);
|
||||
var flowList = DbContext.swap_flow_event.Where(x => x.EventId == swapEvent.id).ToList();
|
||||
var flowList = FindFlowEventsByEventId(swapEvent.id);
|
||||
string action = eventType == (int)SwapEventTypeEnum.互换 ? ClientCashInCashOut.系统操作_互换 : ClientCashInCashOut.系统操作_平仓费;
|
||||
int clientCashId = AddClientCash(td, Convert.ToDouble(-swapEvent.unwindData.SwapRealizedPnL), action, swapEvent.unwindData.ValueDate);
|
||||
if (swapEvent.unwindData.SwapMarginAmount != 0)
|
||||
@@ -1605,14 +1624,14 @@ namespace YLErp.Modules.SwapModule
|
||||
}
|
||||
td.UnWindDate = swapEvent.unwindData.UnwindDate;
|
||||
if (eventType != (int)SwapEventTypeEnum.互换)
|
||||
{
|
||||
{
|
||||
td.StockEqvNotional -= Convert.ToDouble(swapEvent.unwindData.CloseNotionalValue);
|
||||
td.TradeAmount -= Convert.ToDouble(swapEvent.unwindData.CloseQty);
|
||||
}
|
||||
|
||||
td.Notional = td.TradeAmount;
|
||||
UpdateInitalPosition(flowList, swapEvent.unwindData, eventType);
|
||||
DbContext.SaveChanges();
|
||||
SaveAllChanges();
|
||||
|
||||
}
|
||||
/// <summary>
|
||||
@@ -1623,30 +1642,19 @@ namespace YLErp.Modules.SwapModule
|
||||
/// <exception cref="ServiceException"></exception>
|
||||
public void ApplySwapTrade(UnwindData unwindData, int eventType)
|
||||
{
|
||||
var td = DbContext.trade.Find(unwindData.SwapTradeId);
|
||||
var td = FindTrade(unwindData.SwapTradeId);
|
||||
if (td == null)
|
||||
{
|
||||
throw new ServiceException("未找到交易信息");
|
||||
}
|
||||
unwindData.SwapRealizedPnL = unwindData.SwapCloseAmount;
|
||||
var trans = DbContext.Database.BeginTransaction();
|
||||
string action = eventType == (int)SwapEventTypeEnum.互换 ? ClientCashInCashOut.系统操作_互换 : ClientCashInCashOut.系统操作_平仓费;
|
||||
try
|
||||
ExecuteInTransaction(() =>
|
||||
{
|
||||
new TradeUnwindService(this).CloseReCheck_SetTrade(unwindData.SwapTradeId, eventType == (int)SwapEventTypeEnum.互换, true);
|
||||
CloseReCheckSetTrade(unwindData.SwapTradeId, eventType == (int)SwapEventTypeEnum.互换, true);
|
||||
SaveSwapDeal(unwindData, eventType, 0, action, true);
|
||||
DbContext.SaveChanges();
|
||||
trans.Commit();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
trans.Rollback();
|
||||
throw;
|
||||
}
|
||||
finally
|
||||
{
|
||||
trans.Dispose();
|
||||
}
|
||||
SaveAllChanges();
|
||||
});
|
||||
}
|
||||
/// <summary>
|
||||
/// 保存平仓/互换事件
|
||||
|
||||
Reference in New Issue
Block a user