fix(swap): 修复公司行为事件生命周期管理问题

- 添加了股权登记日日期格式化显示功能
- 实现了公司行为类型的动态描述逻辑(配股、送股、拆分、分红)
- 新增了过滤待生效公司行为事件的操作历史机制
- 修正了操作历史中隐藏未应用公司行为事件的逻辑
- 完善了公司行为事件原因描述的构建方法
- 添加了针对不同公司行为类型的单元测试验证
This commit is contained in:
张名锐
2026-08-25 17:58:51 +08:00
parent f7766fb723
commit aa87d69999
2 changed files with 92 additions and 12 deletions
@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using Newtonsoft.Json;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using YLErp.DBModels;
@@ -20,6 +21,7 @@ namespace YLErp.Modules.SwapModule
public void RegistrationSnapshot_IsPending_AndKeepsBeforeFields()
{
var info = CreateAction(77, ConsGlobal.InstrumentType.Stock);
info.GiveShareAmount = 1m;
var before = CreateEodPosition(9, info.UnderlyingCode, 1000m, 100m);
var snapshot = SwapEodPositionService.BuildCorporateActionEventData(
@@ -37,6 +39,7 @@ namespace YLErp.Modules.SwapModule
Assert.IsFalse(snapshot.Applied);
var reason = SwapEventService.BuildCorporateActionEventReason(snapshot);
StringAssert.Contains(reason, "股权登记日:2026-08-14 发生公司行为(送股)");
StringAssert.Contains(reason, "调整前:名义本金:100000 期初标的价格:100 持仓数量:1000");
StringAssert.Contains(reason, "调整后:名义本金:0 期初标的价格:0 持仓数量:0");
}
@@ -65,6 +68,23 @@ namespace YLErp.Modules.SwapModule
Assert.IsFalse(SwapEodPositionService.IsCorporateActionInstrument(ConsGlobal.InstrumentType.TBonds));
}
[TestMethod]
public void CorporateActionReason_ShowsOneActionTypeOrActualCashDividend()
{
AssertActionDescription(
new CorporateActionEventData { ExDividendDate = RecordDate, RationedSharesAmount = 1m, GiveShareAmount = 1m, Split = 2m, GiveCashAmount = 10m, CashFlowChange = 1000m },
"发生公司行为(配股)");
AssertActionDescription(
new CorporateActionEventData { ExDividendDate = RecordDate, GiveShareAmount = 1m, Split = 2m, GiveCashAmount = 10m, CashFlowChange = 1000m },
"发生公司行为(送股)");
AssertActionDescription(
new CorporateActionEventData { ExDividendDate = RecordDate, Split = 2m, GiveCashAmount = 10m, CashFlowChange = 1000m },
"发生公司行为(拆分)");
AssertActionDescription(
new CorporateActionEventData { ExDividendDate = RecordDate, GiveCashAmount = 11m, CashFlowChange = 220000m },
"发生公司行为(产生分红:220000)");
}
[TestMethod]
public void Rerun_DoesNotCreateDuplicateCorporateActionEvent()
{
@@ -113,7 +133,7 @@ namespace YLErp.Modules.SwapModule
}
[TestMethod]
public void OperationHistory_PreservesPendingCorporateActionForAudit()
public void OperationHistory_HidesPendingCorporateActionUntilItIsApplied()
{
var info = CreateAction(80, ConsGlobal.InstrumentType.Stock);
var pendingData = SwapEodPositionService.BuildCorporateActionEventData(
@@ -133,12 +153,17 @@ namespace YLErp.Modules.SwapModule
new swap_event { id = 3, EventType = (int)SwapEventTypeEnum., EventData = "{}" }
};
// 操作历史不再隐藏登记日待生效事件;Applied=false 是事件状态,不是展示过滤条件。
Assert.AreEqual(3, events.Count);
Assert.IsTrue(SwapEventService.TryDeserializeCorporateActionEventData(events[0], out var pendingSnapshot));
Assert.IsFalse(pendingSnapshot.Applied);
Assert.IsTrue(SwapEventService.TryDeserializeCorporateActionEventData(events[1], out var appliedSnapshot));
Assert.IsTrue(appliedSnapshot.Applied);
var filter = typeof(SwapEventService).GetMethod(
"FilterOperationHistory",
BindingFlags.NonPublic | BindingFlags.Static);
Assert.IsNotNull(filter, "操作历史必须过滤登记日创建的待生效公司行为事件。");
var visibleEvents = (List<swap_event>)filter.Invoke(null, new object[] { events });
Assert.AreEqual(2, visibleEvents.Count);
Assert.IsFalse(visibleEvents.Any(x => x.id == 1));
Assert.IsTrue(visibleEvents.Any(x => x.id == 2));
Assert.IsTrue(visibleEvents.Any(x => x.id == 3));
}
[TestMethod]
@@ -283,6 +308,11 @@ namespace YLErp.Modules.SwapModule
};
}
private static void AssertActionDescription(CorporateActionEventData data, string expected)
{
StringAssert.Contains(SwapEventService.BuildCorporateActionEventReason(data), expected);
}
private static eod_swap_position CreateEodPosition(long positionId, string code, decimal quantity, decimal price)
{
return new eod_swap_position
@@ -207,8 +207,8 @@ namespace YLErp.Modules.SwapModule
return events;
}
/// <summary>
/// 获取交易操作历史。登记日创建但尚未到 EffectiveDate 的公司行为事件也保留
/// 由 EventData.Applied=false 表示“待生效”,保证审计日志完整可追溯
/// 获取交易操作历史。登记日创建的待生效公司行为仍保留在审计数据中
/// 但在 EffectiveDate 将其更新为 Applied=true 前不对操作历史展示
/// </summary>
/// <param name="tradeId">交易id</param>
/// <returns></returns>
@@ -218,7 +218,30 @@ namespace YLErp.Modules.SwapModule
.Where(x => x.SwapTradeId == tradeId)
.OrderByDescending(o => o.id)
.ToList();
return list;
return FilterOperationHistory(list);
}
/// <summary>
/// 过滤尚未生效的公司行为事件。非公司行为、已生效事件和无法识别的历史事件均保留,
/// 避免过滤条件误伤既有操作记录。
/// </summary>
private static List<swap_event> FilterOperationHistory(IEnumerable<swap_event> events)
{
if (events == null)
{
return new List<swap_event>();
}
return events
.Where(x => !IsPendingCorporateActionEvent(x))
.ToList();
}
private static bool IsPendingCorporateActionEvent(swap_event swapEvent)
{
return swapEvent?.EventType == (int)SwapEventTypeEnum.
&& TryDeserializeCorporateActionEventData(swapEvent, out var data)
&& !data.Applied;
}
/// <summary>
@@ -257,9 +280,36 @@ namespace YLErp.Modules.SwapModule
return "公司行为快照为空";
}
// 使用 InvariantCulture 固定小数格式,说明文本不随服务器区域设置变化。
// 使用 InvariantCulture 固定小数和日期格式,说明文本不随服务器区域设置变化。
string D(decimal value) => value.ToString(CultureInfo.InvariantCulture);
return $"调整前:名义本金:{D(data.BeforeNotional)} 期初标的价格:{D(data.BeforePrice)} 持仓数量:{D(data.BeforeQuantity)}"
string Date(DateTime? value) => value.HasValue
? value.Value.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture)
: "";
string ActionDescription()
{
if (data.RationedSharesAmount != 0m)
{
return "配股";
}
if (data.GiveShareAmount != 0m)
{
return "送股";
}
if (data.Split.HasValue && data.Split.Value != 1m)
{
return "拆分";
}
if (data.GiveCashAmount != 0m)
{
// return $"产生分红:{D(data.CashFlowChange)}";
return $"产生分红";
}
return "公司行为";
}
return $"股权登记日:{Date(data.ExDividendDate)} 发生公司行为({ActionDescription()}"
+ Environment.NewLine
+ $"调整前:名义本金:{D(data.BeforeNotional)} 期初标的价格:{D(data.BeforePrice)} 持仓数量:{D(data.BeforeQuantity)}"
+ Environment.NewLine
+ $"调整后:名义本金:{D(data.AfterNotional)} 期初标的价格:{D(data.AfterPrice)} 持仓数量:{D(data.AfterQuantity)}";
}