- 在日期刷新基线方法中添加平仓数量重新计算逻辑 - 确保非全平仓情况下按原比例重新计算平仓数量 - 添加单元测试验证部分平仓数量按原比例重算功能 - 修复了日期刷新后平仓比例与实际平仓数量不匹配的bug
386 lines
22 KiB
C#
386 lines
22 KiB
C#
namespace YLErp.Modules.SwapModule
|
||
{
|
||
/// <summary>
|
||
/// 平仓比例口径A(占期初名义本金)产品契约测试
|
||
/// ================================================================================
|
||
/// 产品需求(不可违背):
|
||
/// ClosePercent 永远是"占期初名义本金(NotionalValue)的比例",即口径A。
|
||
/// 例:期初本金 5000 万,首次平 35%(ClosePercent=0.35),剩余 3250 万。
|
||
/// 第二次想平掉剩余的 50%,ClosePercent 应 = 1625万/5000万 = 0.325(占期初),
|
||
/// 而非 0.50(占剩余)。
|
||
///
|
||
/// 前后端协作约定:
|
||
/// - 前端(unwindSwapTrade.js):ClosePercent 始终以 NotionalValue 为分母计算/显示
|
||
/// - 前端传给后端:ClosePercent 为口径A
|
||
/// - 后端入口(SwapUnwind/ApplySwapTrade):ToRemainingClosePercent 将 A→B 供内部计算
|
||
/// - 后端落库(SaveSwapDealInternal):ToOriginalClosePercent 将 B→A 还原存储
|
||
/// - 后端 InitUnwind 默认值:CalcDefaultInitClosePercent = PosiNotionalValue/NotionalValue(口径A)
|
||
///
|
||
/// 本测试守护的回归场景(c9071a4e 曾犯的错误):
|
||
/// 1. 前端把 oriClosePercent 硬编码为 1(应为 PosiNotionalValue/NotionalValue)
|
||
/// 2. 前端把 CloseNotionalValue 分母从 NotionalValue 改为 PosiNotionalValue
|
||
/// 3. 前端把 ClosePercent 分母从 NotionalValue 改为 PosiNotionalValue
|
||
/// 4. 前端把 getInterestList 的 notionalValue/posiNotionalValue 参数去掉
|
||
/// 5. 前端把 calcCloseQtyByPercent 从 SwapCalc.calcCloseQtyByOriginalPercent 改为直接乘
|
||
/// 6. 后端 InitUnwind 默认 ClosePercent 改为 1 而非剩余比例
|
||
///
|
||
/// 与既有测试的关系:
|
||
/// - ApplySwapTradeClosePercentBugTest:测 A→B 转换函数正确性(函数级)
|
||
/// - InitUnwindDefaultClosePercentTest:测默认值函数正确性(函数级)
|
||
/// - 本测试:测完整多步场景的口径A契约(场景级),补齐"装配测试"盲区
|
||
/// ================================================================================
|
||
[TestClass]
|
||
public class ClosePercentProductContractTest
|
||
{
|
||
// GLMS-20260701-0006 真实数据
|
||
private const decimal OriginalNotional = 50_000_000m; // 期初名义本金(NotionalValue)
|
||
private const decimal FirstClosePercentA = 0.35m; // 第一次平仓35%(口径A)
|
||
private const decimal RemainingAfter1st = 32_500_000m; // 首次平35%后剩余(PosiNotionalValue)
|
||
// 第二次想平掉剩余的 50% → 平仓额=16,250,000 → ClosePercent(A)=1625万/5000万=0.325
|
||
private const decimal SecondCloseNotional = 16_250_000m;
|
||
private const decimal SecondClosePercentA = 0.325m; // 口径A:占期初
|
||
private const decimal SecondClosePercentB = 0.50m; // 口径B:占剩余
|
||
|
||
// ================================================================
|
||
// 契约1:ClosePercent = CloseNotionalValue / NotionalValue(口径A)
|
||
// 如果有人把分母改成 PosiNotionalValue,此测试会红
|
||
// ================================================================
|
||
[TestMethod]
|
||
public void CPC_001_平仓比例必须用期初名义本金为分母_而非剩余名义本金()
|
||
{
|
||
// 正确:口径A = 平仓名义本金 / 期初名义本金
|
||
decimal correctA = SecondCloseNotional / OriginalNotional;
|
||
SwapDealTestFactory.AssertDecimalEqual(SecondClosePercentA, correctA, 1e-10m,
|
||
"口径A:16,250,000 / 50,000,000 = 0.325");
|
||
|
||
// 错误:口径B = 平仓名义本金 / 剩余名义本金
|
||
decimal buggyB = SecondCloseNotional / RemainingAfter1st;
|
||
SwapDealTestFactory.AssertDecimalEqual(SecondClosePercentB, buggyB, 1e-10m,
|
||
"口径B(错误):16,250,000 / 32,500,000 = 0.50");
|
||
|
||
// 两者必须不同——如果相同说明测试场景退化(首次平仓 remaining==original)
|
||
Assert.AreNotEqual(correctA, buggyB,
|
||
"口径A(0.325) 和 口径B(0.50) 在多次部分平仓后必须不同,否则测试场景退化");
|
||
Console.WriteLine($"口径A={correctA}(正确),口径B={buggyB}(错误)");
|
||
}
|
||
|
||
// ================================================================
|
||
// 契约2:CloseNotionalValue = ClosePercent × NotionalValue(口径A)
|
||
// 如果有人把乘数改成 PosiNotionalValue,此测试会红
|
||
// ================================================================
|
||
[TestMethod]
|
||
public void CPC_002_平仓名义本金必须用期初名义本金为乘数_而非剩余名义本金()
|
||
{
|
||
// 正确:口径A → CloseNotionalValue = ClosePercent(A) × NotionalValue
|
||
decimal correctNotional = SecondClosePercentA * OriginalNotional;
|
||
SwapDealTestFactory.AssertDecimalEqual(SecondCloseNotional, correctNotional, 1e-6m,
|
||
"口径A:0.325 × 50,000,000 = 16,250,000");
|
||
|
||
// 错误:口径B → CloseNotionalValue = ClosePercent(A) × PosiNotionalValue
|
||
// 如果前端传口径A的0.325但误用 PosiNotionalValue 做乘数
|
||
decimal buggyNotional = SecondClosePercentA * RemainingAfter1st;
|
||
// 0.325 × 32,500,000 = 10,562,500 ≠ 16,250,000
|
||
Assert.AreNotEqual(SecondCloseNotional, buggyNotional,
|
||
"口径A的0.325 × 剩余本金32,500,000 = 10,562,500 ≠ 16,250,000,乘数错了");
|
||
Console.WriteLine($"正确={correctNotional},错误(用剩余)={buggyNotional}");
|
||
}
|
||
|
||
// ================================================================
|
||
// 契约3:oriClosePercent = PosiNotionalValue / NotionalValue(不能硬编码为1)
|
||
// 如果有人把 oriClosePercent 改成 1,此测试会红
|
||
// ================================================================
|
||
[TestMethod]
|
||
public void CPC_003_最多可平比例必须为剩余除以期初_不能硬编码为1()
|
||
{
|
||
// 正确:oriClosePercent = PosiNotionalValue / NotionalValue
|
||
decimal correctOri = RemainingAfter1st / OriginalNotional;
|
||
SwapDealTestFactory.AssertDecimalEqual(0.65m, correctOri, 1e-10m,
|
||
"oriClosePercent = 32,500,000 / 50,000,000 = 0.65");
|
||
|
||
// 错误:硬编码为 1(c9071a4e 的错误)
|
||
decimal buggyOri = 1m;
|
||
Assert.AreNotEqual(correctOri, buggyOri,
|
||
"多次部分平仓后 oriClosePercent 必须小于 1,硬编码 1 会允许平超过剩余持仓");
|
||
|
||
// 首次平仓时 oriClosePercent 才等于 1(remaining == original)
|
||
decimal firstTimeOri = OriginalNotional / OriginalNotional;
|
||
SwapDealTestFactory.AssertDecimalEqual(1m, firstTimeOri, 1e-10m,
|
||
"首次平仓时 oriClosePercent = 1(remaining == original)");
|
||
Console.WriteLine($"多次部分平仓后:oriClosePercent={correctOri}(≠1),首次:{firstTimeOri}(=1)");
|
||
}
|
||
|
||
// ================================================================
|
||
// 契约4:CloseQty 必须经过 A→B 转换,不能直接 PositionQty × ClosePercent(A)
|
||
// 如果有人删除 calcCloseQtyByOriginalPercent 调用改为直接乘,此测试会红
|
||
// ================================================================
|
||
[TestMethod]
|
||
public void CPC_004_平仓数量必须经过口径A到B转换_不能直接乘()
|
||
{
|
||
// 场景:期初数量 32,500,000(=剩余数量),oriClosePercent=0.65
|
||
// 用户输入 ClosePercent(A) = 0.325(想平剩余的 50%)
|
||
decimal positionQty = RemainingAfter1st; // 32,500,000
|
||
decimal oriClosePercent = RemainingAfter1st / OriginalNotional; // 0.65
|
||
|
||
// 正确:CloseQty = PositionQty × (ClosePercent(A) / oriClosePercent)
|
||
// = 32,500,000 × (0.325 / 0.65) = 32,500,000 × 0.5 = 16,250,000
|
||
decimal correctQty = positionQty * (SecondClosePercentA / oriClosePercent);
|
||
SwapDealTestFactory.AssertDecimalEqual(16_250_000m, correctQty, 1e-6m,
|
||
"正确:32,500,000 × (0.325/0.65) = 16,250,000");
|
||
|
||
// 错误:CloseQty = PositionQty × ClosePercent(A)(直接乘,不做转换)
|
||
// = 32,500,000 × 0.325 = 10,562,500 ❌
|
||
decimal buggyQty = positionQty * SecondClosePercentA;
|
||
Assert.AreNotEqual(correctQty, buggyQty,
|
||
"直接乘会得到 10,562,500 而非 16,250,000,数量算少 35%");
|
||
|
||
// JS 浮点精度守卫:32500000×(0.5/0.65) 可能 = 24999999.999999996
|
||
// SwapCalc.calcCloseQtyByOriginalPercent 用 roundHalfAwayFromZero 修复
|
||
decimal jsFloatTrap = (decimal)((double)positionQty * ((double)SecondClosePercentA / (double)oriClosePercent));
|
||
Console.WriteLine($"正确={correctQty},错误(直接乘)={buggyQty},JS浮点陷阱={jsFloatTrap}");
|
||
}
|
||
|
||
// ================================================================
|
||
// 契约5:后端 A→B→A 往返转换必须还原(多次部分平仓场景)
|
||
// 守护 SwapUnwind/ApplySwapTrade 入口的 ToRemainingClosePercent + SaveSwapDealInternal 的 ToOriginalClosePercent
|
||
// ================================================================
|
||
[TestMethod]
|
||
public void CPC_005_多次部分平仓_A到B到A往返转换必须还原原值()
|
||
{
|
||
// 第二次部分平仓:前端传 ClosePercent(A) = 0.325
|
||
decimal closePercentA = SecondClosePercentA;
|
||
|
||
// 后端入口:A → B
|
||
decimal closePercentB = SwapDealService.ToRemainingClosePercent(
|
||
closePercentA, OriginalNotional, RemainingAfter1st);
|
||
SwapDealTestFactory.AssertDecimalEqual(SecondClosePercentB, closePercentB, 1e-10m,
|
||
"A→B:0.325 × 50,000,000 / 32,500,000 = 0.50");
|
||
|
||
// 后端落库:B → A 还原
|
||
decimal restoredA = SwapDealService.ToOriginalClosePercent(
|
||
closePercentB, OriginalNotional, RemainingAfter1st);
|
||
SwapDealTestFactory.AssertDecimalEqual(closePercentA, restoredA, 1e-10m,
|
||
"B→A 还原:0.50 × 32,500,000 / 50,000,000 = 0.325(必须等于原始A)");
|
||
|
||
Console.WriteLine($"A={closePercentA} → B={closePercentB} → A'={restoredA} ✅ 往返还原");
|
||
}
|
||
|
||
// ================================================================
|
||
// 契约6:后端 InitUnwind 默认 ClosePercent = PosiNotionalValue / NotionalValue(口径A)
|
||
// 不能硬编码为 1(c9071a4e 前端硬编码1的错误在后端等价于此)
|
||
// ================================================================
|
||
[TestMethod]
|
||
public void CPC_006_InitUnwind默认值必须为剩余除以期初_不能硬编码为1()
|
||
{
|
||
// 多次部分平仓后:期初 50M,剩余 32.5M
|
||
decimal defaultValue = SwapDealService.CalcDefaultInitClosePercent(
|
||
OriginalNotional, RemainingAfter1st);
|
||
|
||
// 正确:0.65(占期初的"平剩余全部"比例)
|
||
SwapDealTestFactory.AssertDecimalEqual(0.65m, defaultValue, 1e-10m,
|
||
"CalcDefaultInitClosePercent(50M, 32.5M) = 0.65(口径A)");
|
||
|
||
// 不能是 1(硬编码错误)
|
||
Assert.AreNotEqual(1m, defaultValue,
|
||
"多次部分平仓后默认值不能为1,否则意味着'平掉原始全部'而非'平剩余全部'");
|
||
|
||
Console.WriteLine($"InitUnwind 默认 ClosePercent(A) = {defaultValue}(≠1)✅");
|
||
}
|
||
|
||
// ================================================================
|
||
// 契约7:SwapUnwind 第二次部分平仓必须正确做 A→B 转换
|
||
// 这是"装配测试"——验证后端入口确实执行了转换,而不只是函数本身正确
|
||
// ================================================================
|
||
[TestMethod]
|
||
public void CPC_007_SwapUnwind第二次部分平仓_入口必须将ClosePercent从A转为B()
|
||
{
|
||
// 模拟 GLMS-20260701-0006 第二次部分平仓
|
||
var td = new trade
|
||
{
|
||
id = 2001,
|
||
TradeNumber = "CPC-TEST-007",
|
||
TradeType = "收益互换",
|
||
TradeStatus = "确认成交",
|
||
ValidState = "Valid",
|
||
StockEqvNotional = (double)RemainingAfter1st, // 32,500,000
|
||
OriginalStockEqvNotional = (double)OriginalNotional, // 50,000,000
|
||
Notional = (double)RemainingAfter1st,
|
||
TradeAmount = (double)RemainingAfter1st
|
||
};
|
||
|
||
var service = new TestableSwapDealService(td);
|
||
|
||
// 前端传 ClosePercent = 0.325(口径A,占期初)
|
||
var unwindData = new UnwindData
|
||
{
|
||
SwapTradeId = td.id,
|
||
SwapRealizedPnL = 1000m,
|
||
SwapCloseAmount = 1000m,
|
||
CloseMethod = (int)CloseMethodEnum.部分平仓,
|
||
ClosePercent = SecondClosePercentA, // 0.325(口径A)
|
||
CloseQty = 16_250_000m,
|
||
CloseNotionalValue = SecondCloseNotional, // 16,250,000
|
||
PositionQty = RemainingAfter1st, // 32,500,000
|
||
NotionalValue = OriginalNotional, // 50,000,000(期初)
|
||
PosiNotionalValue = RemainingAfter1st, // 32,500,000(剩余)
|
||
ValueDate = new DateTime(2026, 7, 14),
|
||
UnwindDate = new DateTime(2026, 7, 15),
|
||
StartDate = new DateTime(2026, 7, 1)
|
||
};
|
||
|
||
service.SwapUnwind(unwindData);
|
||
|
||
// 验证 SwapUnwind 被调用
|
||
Assert.AreEqual(1, service.SaveSwapDealCalls.Count, "SwapUnwind 应调用 SaveSwapDeal");
|
||
|
||
// 验证传给 SaveSwapDeal 的 ClosePercent 已转为口径B
|
||
var savedData = service.SaveSwapDealCalls[0].data;
|
||
decimal expectedB = SwapDealService.ToRemainingClosePercent(
|
||
SecondClosePercentA, OriginalNotional, RemainingAfter1st);
|
||
|
||
SwapDealTestFactory.AssertDecimalEqual(expectedB, savedData.ClosePercent, 1e-10m,
|
||
"SwapUnwind 应将 ClosePercent 从口径A(0.325)转为口径B(0.50)");
|
||
|
||
// 口径B 不等于口径A(验证转换确实发生了)
|
||
Assert.AreNotEqual(SecondClosePercentA, savedData.ClosePercent,
|
||
"口径B(0.50) 不应等于口径A(0.325),否则说明转换缺失");
|
||
|
||
Console.WriteLine($"SwapUnwind: 输入A={SecondClosePercentA} → 输出B={savedData.ClosePercent} ✅");
|
||
}
|
||
|
||
// ================================================================
|
||
// 契约8:完整多步场景——3次平仓合计应等于期初本金
|
||
// 首次35%(A) → 第二次平剩余50%(A=0.325) → 第三次全平剩余(A=0.325)
|
||
// 合计 CloseNotionalValue = 17.5M + 16.25M + 16.25M = 50M = 原始本金
|
||
// ================================================================
|
||
[TestMethod]
|
||
public void CPC_008_三次部分平仓合计本金必须等于期初名义本金()
|
||
{
|
||
decimal remaining = OriginalNotional; // 50,000,000
|
||
decimal totalClosed = 0m;
|
||
|
||
// Step1: 平 35%(首次,remaining == original, A = B = 0.35)
|
||
decimal step1A = 0.35m;
|
||
decimal step1Notional = step1A * OriginalNotional; // 17,500,000
|
||
totalClosed += step1Notional;
|
||
remaining -= step1Notional; // 32,500,000
|
||
|
||
// Step2: 平剩余的 50% → A = 16,250,000 / 50,000,000 = 0.325
|
||
decimal step2Notional = 16_250_000m;
|
||
decimal step2A = step2Notional / OriginalNotional; // 0.325
|
||
// 后端 A→B 转换
|
||
decimal step2B = SwapDealService.ToRemainingClosePercent(
|
||
step2A, OriginalNotional, remaining);
|
||
SwapDealTestFactory.AssertDecimalEqual(0.50m, step2B, 1e-10m,
|
||
"Step2: A=0.325 → B=0.50(平剩余50%)");
|
||
totalClosed += step2Notional;
|
||
remaining -= step2Notional; // 16,250,000
|
||
|
||
// Step3: 全平剩余 → A = 16,250,000 / 50,000,000 = 0.325
|
||
decimal step3Notional = remaining;
|
||
decimal step3A = step3Notional / OriginalNotional; // 0.325
|
||
decimal step3B = SwapDealService.ToRemainingClosePercent(
|
||
step3A, OriginalNotional, remaining);
|
||
SwapDealTestFactory.AssertDecimalEqual(1.0m, step3B, 1e-10m,
|
||
"Step3: A=0.325 → B=1.0(全平剩余)");
|
||
totalClosed += step3Notional;
|
||
remaining -= step3Notional; // 0
|
||
|
||
// 守恒:合计 = 期初
|
||
SwapDealTestFactory.AssertDecimalEqual(OriginalNotional, totalClosed, 1e-6m,
|
||
"三次平仓合计必须 = 期初名义本金 50,000,000");
|
||
SwapDealTestFactory.AssertDecimalEqual(0m, remaining, 1e-6m,
|
||
"三次平仓后剩余必须 = 0");
|
||
|
||
Console.WriteLine($"Step1: A=0.35, Notional=17,500,000");
|
||
Console.WriteLine($"Step2: A=0.325→B=0.50, Notional=16,250,000");
|
||
Console.WriteLine($"Step3: A=0.325→B=1.00, Notional=16,250,000");
|
||
Console.WriteLine($"合计={totalClosed} = 期初{OriginalNotional} ✅");
|
||
}
|
||
|
||
// ================================================================
|
||
// 契约9:GetUnwindInterestList 必须接收 notionalValue 和 posiNotionalValue
|
||
// 前端 getInterestList 传这两个参数给后端做 A→B 转换
|
||
// 如果前端删掉这两个参数,后端 ToRemainingClosePercent 在 posiNotionalValue=0 时会跳过转换
|
||
// ================================================================
|
||
[TestMethod]
|
||
public void CPC_009_ToRemainingClosePercent_PosiNotionalValue为零时跳过转换_前端必须传值()
|
||
{
|
||
// 模拟前端不传 notionalValue/posiNotionalValue(默认0)
|
||
decimal result = SwapDealService.ToRemainingClosePercent(
|
||
SecondClosePercentA, notionalValue: 0, posiNotionalValue: 0);
|
||
|
||
// posiNotionalValue <= 0 时直接返回原值(不转换)
|
||
SwapDealTestFactory.AssertDecimalEqual(SecondClosePercentA, result, 1e-10m,
|
||
"posiNotionalValue=0 时不做转换——所以前端必须传 notionalValue/posiNotionalValue");
|
||
|
||
// 正确:前端传值后转换正常
|
||
decimal resultWithValues = SwapDealService.ToRemainingClosePercent(
|
||
SecondClosePercentA, OriginalNotional, RemainingAfter1st);
|
||
SwapDealTestFactory.AssertDecimalEqual(SecondClosePercentB, resultWithValues, 1e-10m,
|
||
"前端传值后:A=0.325 → B=0.50 ✅");
|
||
|
||
// 两者必须不同
|
||
Assert.AreNotEqual(result, resultWithValues,
|
||
"传 vs 不传 notionalValue 结果不同——前端必须传,否则利息计算用错口径");
|
||
|
||
Console.WriteLine($"不传值(默认0):{result}(未转换,错误地用A算利息)");
|
||
Console.WriteLine($"传值:{resultWithValues}(正确转换为B)");
|
||
}
|
||
|
||
// ================================================================
|
||
// 契约10:首次平仓 A==B(退化场景,不应误报)
|
||
// 首次平仓时 NotionalValue == PosiNotionalValue,A=B,转换系数=1
|
||
// 这是既有测试全绿的原因——必须用非退化场景才能捕获回归
|
||
// ================================================================
|
||
[TestMethod]
|
||
public void CPC_010_首次平仓A等于B_退化场景_不能作为唯一测试()
|
||
{
|
||
decimal closePercentA = 0.35m;
|
||
decimal firstRemaining = OriginalNotional; // 首次 remaining == original
|
||
|
||
decimal convertedB = SwapDealService.ToRemainingClosePercent(
|
||
closePercentA, OriginalNotional, firstRemaining);
|
||
|
||
// 首次平仓:A == B(转换系数 = 1)
|
||
SwapDealTestFactory.AssertDecimalEqual(closePercentA, convertedB, 1e-10m,
|
||
"首次平仓 remaining==original → A==B==0.35(退化场景)");
|
||
|
||
// 退化场景下即使不做转换结果也一样——这就是既有测试全绿的原因
|
||
decimal noConversion = closePercentA;
|
||
Assert.AreEqual(noConversion, convertedB,
|
||
"退化场景:做不做转换结果一样 → 无法发现'转换缺失'的bug");
|
||
|
||
Console.WriteLine($"⚠ 退化场景:A={closePercentA} == B={convertedB}(首次平仓,无法暴露双重转换bug)");
|
||
Console.WriteLine($"✅ 非退化场景见 CPC_005/007:A=0.325 ≠ B=0.50(多次部分平仓后才能暴露)");
|
||
}
|
||
|
||
[TestMethod]
|
||
public void CPC_011_日期刷新基线后_部分平仓数量必须按原比例重算()
|
||
{
|
||
var directory = new DirectoryInfo(AppContext.BaseDirectory);
|
||
string source = null;
|
||
while (directory != null && source == null)
|
||
{
|
||
var path = Path.Combine(directory.FullName, "YLErpWeb", "wwwroot", "Scripts", "app", "swaptrade", "unwindSwapTrade.js");
|
||
if (File.Exists(path))
|
||
{
|
||
source = File.ReadAllText(path);
|
||
}
|
||
directory = directory.Parent;
|
||
}
|
||
|
||
Assert.IsNotNull(source, "未找到 unwindSwapTrade.js");
|
||
var refreshStart = source.IndexOf("refreshUnwindBaseline() {//", StringComparison.Ordinal);
|
||
var refreshEnd = source.IndexOf("getInterestList() {//", refreshStart, StringComparison.Ordinal);
|
||
var refreshMethod = source.Substring(refreshStart, refreshEnd - refreshStart);
|
||
var oriClosePercentIndex = refreshMethod.IndexOf("thisObj.oriClosePercent =", StringComparison.Ordinal);
|
||
var recalcCloseQtyIndex = refreshMethod.IndexOf(
|
||
"thisObj.deal.CloseQty = thisObj.calcCloseQtyByPercent(thisObj.deal.ClosePercent);",
|
||
StringComparison.Ordinal);
|
||
|
||
Assert.IsTrue(recalcCloseQtyIndex > oriClosePercentIndex,
|
||
"日期刷新并更新最多可平比例后,部分平仓数量必须按保留的平仓比例重新计算。");
|
||
}
|
||
}
|
||
}
|