using YLErp.DBModels; using YLErp.DBModels.Enums; namespace YLErp.Modules.SwapModule { /// /// 多次部分平仓"返回预付金"默认显示仍是初始值 bug 的回归测试(根因修复后应为全绿)。 /// --------------------------------------------------------------- /// 生产铁证 GLMS-20260701-0008(SwapTradeId=1993,dev 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.88;99,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 以锁定该契约。 /// [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 { OrigPrepay() }; var reals = new List { RealPrepay() }; var result = SwapDealService.ResolveInterestLegPositions(origs, reals); Assert.AreEqual(1, result.Count, "应保留 1 条利息腿"); Assert.AreEqual(RemainingFix, result[0].InterestPrincipalFix, "多次部分平仓后:预付金腿本金应=实时腿剩余本金 66,813.12,而非原始腿初始值 99,000(bug 症状)"); // 必须是 Clone,不能污染原始腿(原始腿要保留 99,000 供其他路径/审计) Assert.AreEqual(InitialFix, origs[0].InterestPrincipalFix, "修复必须走 Clone,绝不能就地改写 origPositions 的初始本金"); } [TestMethod] public void 首次平仓_实时腿等于原始腿_返回原始腿本身_零改动() { var origs = new List { OrigPrepay(InitialFix) }; var reals = new List { 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 { OrigPrepay(InitialFix, (int)InterestModeEnum.追加预付金) }; var reals = new List { 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 { orig }, new List { real }); Assert.AreEqual(InitialFix, result[0].InterestPrincipalFix, "非预付金腿本金不被实时腿覆盖"); Assert.AreSame(orig, result[0], "非预付金腿应原样返回,不克隆"); } [TestMethod] public void 无匹配实时腿_返回原始腿() { // real 腿 PositionId 指向别的 orig(或根本没有实时腿)→ 找不到匹配,保持原始腿 var origs = new List { OrigPrepay() }; var mismatched = new List { RealPrepay(positionId: 99999) }; var r1 = SwapDealService.ResolveInterestLegPositions(origs, mismatched); Assert.AreEqual(InitialFix, r1[0].InterestPrincipalFix, "无匹配实时腿:保持原始腿初始本金"); var r2 = SwapDealService.ResolveInterestLegPositions(origs, new List()); 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 { OrigPrepay(), underlyingLeg }; var reals = new List { 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, "且其本金已对齐实时剩余本金"); } /// /// 生产 Live Snapshot(2026-07-16 11:00,dev 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 再被平仓,剩余本金会变,本例仍应同步更新。 /// [TestMethod] public void GLMS20260701_四次部分平仓_LiveSnapshot_预付金腿应取实时腿剩余66813_12() { // 与生产一致的双轨数据:期初腿 99,000 / 实时腿 4 次平仓后 66,813.12 var origs = new List { OrigPrepay(InitialFix) }; var reals = new List { 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"); } } }