Files
zszq-trs/UnitTestProject/Modules/SwapModule/SwapUnwindPrepayOrigVsRealBugTdd.cs
T
hjhan d51dd8a60b test(swap): 回归测试对齐 GLMS-20260701-0008 四次部分平仓快照
生产 deal 又平仓两次(共四次),实时预付金腿 InterestPrincipalFix 由 73,260
降至 66,813.12(4 次返还 9,900+15,840+3,663+2,783.88=32,186.88;
99,000−32,186.88=66,813.12,与 dev 库实时腿完全勾稽)。

- SwapUnwindPrepayOrigVsRealBugTdd:RemainingFix 常量 73,260→66,813.12;
  头部注释同步为四次平仓;新增 GLMS20260701_四次部分平仓_LiveSnapshot 用例,
  把 4 次平仓真实数据硬编码为忠实回归(日后该 deal 再平仓需同步更新)。
- 纳入此前遗漏提交的连库诊断测试 GLMS20260701DbDiagnoseTest.cs
  (使用 DbContextFactory 取连接串,无硬编码密码,安全)。

验证:dotnet test --filter SwapUnwindPrepay → 26 passed(19+7)失败 0;
YLErpDAL / UnitTestProject 编译通过(仅既有警告)。
2026-07-16 11:09:47 +08:00

179 lines
10 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using YLErp.DBModels;
using YLErp.DBModels.Enums;
namespace YLErp.Modules.SwapModule
{
/// <summary>
/// 多次部分平仓"返回预付金"默认显示仍是初始值 bug 的回归测试(根因修复后应为全绿)。
/// ---------------------------------------------------------------
/// 生产铁证 GLMS-20260701-0008SwapTradeId=1993dev DB 192.168.2.96 / glms_yltrs_ylcms):
/// 预付金腿(InterestMode=5) 双轨记录——
/// orig 35798 (IsInitial=1, PosiDirection=0, InterestPrincipalFix=99,000) ← 期初腿,恒为初始值
/// real 35871 (IsInitial=0, PositionId=35798, InterestPrincipalFix=66,813.12) ← 实时腿,已扣减 4 次平仓
/// (9,900 + 15,840 + 3,663 + 2,783.88 = 32,186.8899,000 32,186.88 = 66,813.12,与 dev 库实时腿完全勾稽)
///
/// 根因:GetUnwindInterests 的利息腿迭代源取 origPositions(IsInitial=1),其预付金腿
/// InterestPrincipalFix 恒=99,000;而"当前剩余本金"66,813.12 存在 real 腿。GetInterests 算
/// closePrincipal = Fix × closePercent 与预付金计息基数 orginPv 都读 position.InterestPrincipalFix
/// 于是多次部分平仓后打开平仓页,"返回预付金"仍按初始 99,000 计算——完全不对。
/// 首次平仓时 orig==real,掩盖了该 bug(解释"为何只修好一次部分平仓")。
///
/// 修复:SwapDealService.ResolveInterestLegPositions —— 迭代源仍用 origPositions(保留
/// orig.id → eod_swap_position.PositionId 的日终匹配,全库 25,441 行 eod 均按 orig.id 归档,
/// 换 realPositions 会破坏 preEod 匹配导致利息重算错误),仅对预付金腿(初始5/追加6) Clone 覆盖
/// InterestPrincipalFix 为实时腿剩余本金。real 与 orig 通过 real.PositionId == orig.id 精确 1:1 关联。
///
/// 覆盖盲区说明:既有 SwapUnwindPrepayPrincipalBugTdd 的 19 个用例全部直接调 GetInterests
/// 并只喂一条 IsInitial=true 的持仓,完全绕过 GetUnwindInterests 的 orig-vs-real 选择逻辑,
/// 测不到本次 bug。本类直接单测抽出的纯函数 ResolveInterestLegPositions 以锁定该契约。
/// </summary>
[TestClass]
public class SwapUnwindPrepayOrigVsRealBugTdd
{
// 生产 GLMS-20260701-0008 精确值
private const long OrigId = 35798;
private const long RealId = 35871;
private const decimal InitialFix = 99_000m; // orig 腿初始本金
private const decimal RemainingFix = 66_813.12m; // real 腿剩余本金(已扣减 4 次平仓 9,900+15,840+3,663+2,783.88=32,186.88
private static swap_position OrigPrepay(decimal fix = InitialFix, int mode = (int)InterestModeEnum.初始预付金)
=> new swap_position
{
id = OrigId,
SwapTradeId = 1993,
PosiDirection = 0, // 利息端(收/支)
InterestMode = mode,
InterestPrincipalFix = fix,
IsInitial = true,
Invalid = false
};
private static swap_position RealPrepay(long positionId = OrigId, decimal fix = RemainingFix, int mode = (int)InterestModeEnum.初始预付金)
=> new swap_position
{
id = RealId,
SwapTradeId = 1993,
PositionId = positionId, // 指向对应 orig 的 id
PosiDirection = 0,
InterestMode = mode,
InterestPrincipalFix = fix,
IsInitial = false,
Invalid = false
};
[TestMethod]
public void 多次部分平仓后_预付金腿本金应取实时腿剩余本金_而非原始腿初始值()
{
var origs = new List<swap_position> { OrigPrepay() };
var reals = new List<swap_position> { RealPrepay() };
var result = SwapDealService.ResolveInterestLegPositions(origs, reals);
Assert.AreEqual(1, result.Count, "应保留 1 条利息腿");
Assert.AreEqual(RemainingFix, result[0].InterestPrincipalFix,
"多次部分平仓后:预付金腿本金应=实时腿剩余本金 66,813.12,而非原始腿初始值 99,000bug 症状)");
// 必须是 Clone,不能污染原始腿(原始腿要保留 99,000 供其他路径/审计)
Assert.AreEqual(InitialFix, origs[0].InterestPrincipalFix,
"修复必须走 Clone,绝不能就地改写 origPositions 的初始本金");
}
[TestMethod]
public void 首次平仓_实时腿等于原始腿_返回原始腿本身_零改动()
{
var origs = new List<swap_position> { OrigPrepay(InitialFix) };
var reals = new List<swap_position> { RealPrepay(fix: InitialFix) }; // 尚未平仓,real==orig
var result = SwapDealService.ResolveInterestLegPositions(origs, reals);
Assert.AreEqual(InitialFix, result[0].InterestPrincipalFix, "首次平仓 orig==real,本金保持初始值");
Assert.AreSame(origs[0], result[0], "orig==real 时不应克隆,直接返回原始腿本身(行为与修复前一致)");
}
[TestMethod]
public void 追加预付金腿_同样取实时腿剩余本金()
{
var origs = new List<swap_position> { OrigPrepay(InitialFix, (int)InterestModeEnum.追加预付金) };
var reals = new List<swap_position> { RealPrepay(fix: RemainingFix, mode: (int)InterestModeEnum.追加预付金) };
var result = SwapDealService.ResolveInterestLegPositions(origs, reals);
Assert.AreEqual(RemainingFix, result[0].InterestPrincipalFix,
"追加预付金(mode=6)与初始预付金(mode=5)同源修复,同样取实时腿剩余本金");
}
[TestMethod]
public void 非预付金腿_不受影响_始终保持原始腿本金()
{
// 标的期初全价(=9)等非预付金腿:即便 real 腿本金不同也不应被覆盖(其本金语义不同,不走此纠正)
var orig = OrigPrepay(InitialFix, (int)InterestModeEnum.标的期初全价);
var real = RealPrepay(fix: RemainingFix, mode: (int)InterestModeEnum.标的期初全价);
var result = SwapDealService.ResolveInterestLegPositions(
new List<swap_position> { orig }, new List<swap_position> { real });
Assert.AreEqual(InitialFix, result[0].InterestPrincipalFix, "非预付金腿本金不被实时腿覆盖");
Assert.AreSame(orig, result[0], "非预付金腿应原样返回,不克隆");
}
[TestMethod]
public void 无匹配实时腿_返回原始腿()
{
// real 腿 PositionId 指向别的 orig(或根本没有实时腿)→ 找不到匹配,保持原始腿
var origs = new List<swap_position> { OrigPrepay() };
var mismatched = new List<swap_position> { RealPrepay(positionId: 99999) };
var r1 = SwapDealService.ResolveInterestLegPositions(origs, mismatched);
Assert.AreEqual(InitialFix, r1[0].InterestPrincipalFix, "无匹配实时腿:保持原始腿初始本金");
var r2 = SwapDealService.ResolveInterestLegPositions(origs, new List<swap_position>());
Assert.AreEqual(InitialFix, r2[0].InterestPrincipalFix, "实时腿为空:保持原始腿初始本金");
var r3 = SwapDealService.ResolveInterestLegPositions(origs, null);
Assert.AreEqual(InitialFix, r3[0].InterestPrincipalFix, "实时腿为 null:应容错并保持原始腿初始本金");
}
[TestMethod]
public void 只保留利息腿_过滤掉标的腿()
{
// PosiDirection>0 的标的腿不属于利息端,应被过滤(与原实现 Where(PosiDirection==0) 一致)
var underlyingLeg = new swap_position
{
id = 40000, SwapTradeId = 1993, PosiDirection = 1,
InterestMode = (int)InterestModeEnum.标的期初全价, IsInitial = true, Invalid = false
};
var origs = new List<swap_position> { OrigPrepay(), underlyingLeg };
var reals = new List<swap_position> { RealPrepay() };
var result = SwapDealService.ResolveInterestLegPositions(origs, reals);
Assert.AreEqual(1, result.Count, "只应保留利息腿(PosiDirection==0),标的腿被过滤");
Assert.AreEqual(OrigId, result[0].id, "保留的应是预付金利息腿");
Assert.AreEqual(RemainingFix, result[0].InterestPrincipalFix, "且其本金已对齐实时剩余本金");
}
/// <summary>
/// 生产 Live Snapshot2026-07-16 11:00dev DB 192.168.2.96 / glms_yltrs_ylcms 直连核实):
/// GLMS-20260701-0008 已 4 次部分平仓。预付金腿(orig 35798 / real 35871) 实际值——
/// orig InterestPrincipalFix = 99,000(期初腿恒为初始值)
/// real InterestPrincipalFix = 66,813.12= 99,000 9,900 15,840 3,663 2,783.88
/// swap_flow_event 4 次平仓返还:9,900 / 15,840 / 3,663 / 2,783.88,合计 32,186.88。
/// 本用例把这份真实数据硬编码进来,断言修复后取实时腿剩余本金 66,813.12(非 99,000),
/// 作为该 deal 在此快照点的忠实回归;日后该 deal 再被平仓,剩余本金会变,本例仍应同步更新。
/// </summary>
[TestMethod]
public void GLMS20260701_四次部分平仓_LiveSnapshot_预付金腿应取实时腿剩余66813_12()
{
// 与生产一致的双轨数据:期初腿 99,000 / 实时腿 4 次平仓后 66,813.12
var origs = new List<swap_position> { OrigPrepay(InitialFix) };
var reals = new List<swap_position> { RealPrepay(fix: 66_813.12m) };
var result = SwapDealService.ResolveInterestLegPositions(origs, reals);
Assert.AreEqual(1, result.Count);
Assert.AreEqual(66_813.12m, result[0].InterestPrincipalFix,
"4 次部分平仓后:预付金腿本金应=实时腿剩余本金 66,813.12,而非原始腿初始值 99,000");
// 不污染原始腿
Assert.AreEqual(InitialFix, origs[0].InterestPrincipalFix, "修复必须走 Clone,不能改写 origPositions 的初始本金 99,000");
}
}
}