using System; using System.Collections.Generic; using System.Linq; using Newtonsoft.Json; using Microsoft.VisualStudio.TestTools.UnitTesting; using YLErp.DBModels; using YLErp.DBModels.Consts; namespace YLErp.Modules.SwapModule { [TestClass] public class CorporateActionEventLifecycleTest { // 8/14 登记日只创建 Applied=false 的待生效事件;8/17 真实生效日补齐 // 同一事件的调整前后快照并标记 Applied=true。 private static readonly DateTime RecordDate = new DateTime(2026, 8, 14); private static readonly DateTime EffectiveDate = new DateTime(2026, 8, 17); [TestMethod] public void RegistrationSnapshot_IsPending_AndKeepsBeforeFields() { var info = CreateAction(77, ConsGlobal.InstrumentType.Stock); var before = CreateEodPosition(9, info.UnderlyingCode, 1000m, 100m); var snapshot = SwapEodPositionService.BuildCorporateActionEventData( info, before, null, applied: false); Assert.AreEqual(77, snapshot.ExDividendInfoId); Assert.AreEqual(9L, snapshot.PositionId); Assert.AreEqual(1000m, snapshot.BeforeQuantity); Assert.AreEqual(100m, snapshot.BeforePrice); Assert.AreEqual(100000m, snapshot.BeforeNotional); Assert.AreEqual(0m, snapshot.AfterQuantity); Assert.IsFalse(snapshot.Applied); var reason = SwapEventService.BuildCorporateActionEventReason(snapshot); StringAssert.Contains(reason, "调整前:名义本金:100000 期初标的价格:100 持仓数量:1000"); StringAssert.Contains(reason, "调整后:名义本金:0 期初标的价格:0 持仓数量:0"); } [TestMethod] public void EffectiveSnapshot_ContainsAfterFields_AndSupportsStockAndFund() { var info = CreateAction(78, ConsGlobal.InstrumentType.Fund); var before = CreateEodPosition(10, info.UnderlyingCode, 1000m, 100m); var after = CreateEodPosition(10, info.UnderlyingCode, 2000m, 50m); var snapshot = SwapEodPositionService.BuildCorporateActionEventData( info, before, after, applied: true); Assert.AreEqual(1000m, snapshot.BeforeQuantity); Assert.AreEqual(100m, snapshot.BeforePrice); Assert.AreEqual(2000m, snapshot.AfterQuantity); Assert.AreEqual(50m, snapshot.AfterPrice); Assert.AreEqual(100000m, snapshot.AfterNotional); Assert.IsTrue(snapshot.Applied); Assert.IsTrue(SwapEodPositionService.IsCorporateActionInstrument(ConsGlobal.InstrumentType.Stock)); Assert.IsTrue(SwapEodPositionService.IsCorporateActionInstrument(ConsGlobal.InstrumentType.Fund)); Assert.IsFalse(SwapEodPositionService.IsCorporateActionInstrument(ConsGlobal.InstrumentType.TBonds)); } [TestMethod] public void Rerun_DoesNotCreateDuplicateCorporateActionEvent() { var info = CreateAction(79, ConsGlobal.InstrumentType.Stock); var snapshot = SwapEodPositionService.BuildCorporateActionEventData( info, CreateEodPosition(11, info.UnderlyingCode, 1000m, 100m), null, applied: false); var existing = new swap_event { SwapTradeId = 100, EventType = (int)SwapEventTypeEnum.公司行为, EventData = JsonConvert.SerializeObject(snapshot), Invalid = false }; Assert.IsFalse(SwapEodPositionService.ShouldCreateCorporateActionEvent( new[] { existing }, info, 11L)); } [TestMethod] public void LegacyEventWithoutExDividendInfoId_DoesNotBlockCurrentEvent() { var info = CreateAction(79, ConsGlobal.InstrumentType.Stock); var legacySnapshot = SwapEodPositionService.BuildCorporateActionEventData( info, CreateEodPosition(11, info.UnderlyingCode, 1000m, 100m), null, applied: false); legacySnapshot.ExDividendInfoId = 0; var legacyEvent = new swap_event { SwapTradeId = 100, EventType = (int)SwapEventTypeEnum.公司行为, EventData = JsonConvert.SerializeObject(legacySnapshot), Invalid = false }; Assert.IsTrue(SwapEodPositionService.ShouldCreateCorporateActionEvent( new[] { legacyEvent }, info, 11L)); } [TestMethod] public void OperationHistory_PreservesPendingCorporateActionForAudit() { var info = CreateAction(80, ConsGlobal.InstrumentType.Stock); var pendingData = SwapEodPositionService.BuildCorporateActionEventData( info, CreateEodPosition(12, info.UnderlyingCode, 1000m, 100m), null, applied: false); var appliedData = SwapEodPositionService.BuildCorporateActionEventData( info, CreateEodPosition(13, info.UnderlyingCode, 1000m, 100m), CreateEodPosition(13, info.UnderlyingCode, 2000m, 50m), applied: true); var events = new List { new swap_event { id = 1, EventType = (int)SwapEventTypeEnum.公司行为, EventData = JsonConvert.SerializeObject(pendingData) }, new swap_event { id = 2, EventType = (int)SwapEventTypeEnum.公司行为, EventData = JsonConvert.SerializeObject(appliedData) }, 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); } [TestMethod] public void EffectiveCorporateAction_AdjustsStockQuantityAndPrice() { var position = new swap_position { PositionId = 14, PosiDirection = 1, UnderlyingInstrumentType = ConsGlobal.InstrumentType.Stock, UnderlyingCode = "STOCK.TEST", PosiQuantity = 1000m, PosiGrossPrice = 100m, PosiNetPrice = 100m, ContractSize = 1m }; var info = CreateAction(81, ConsGlobal.InstrumentType.Stock); info.GiveShareAmount = 10m; var applied = SwapEodPositionService.ApplyCorporateActionToPosition( position, info, 100m, 0m); Assert.IsTrue(applied); Assert.AreEqual(2000m, position.PosiQuantity); Assert.AreEqual(50m, position.PosiGrossPrice); Assert.AreEqual(100000m, position.PosiNotionalValue); } [TestMethod] public void Lifecycle_RegistrationIsIdempotent_ThenEffectiveUpdatesSameEvent() { var info = CreateAction(82, ConsGlobal.InstrumentType.Stock); var before = CreateEodPosition(15, info.UnderlyingCode, 1000m, 100m); var after = CreateEodPosition(15, info.UnderlyingCode, 2000m, 50m); var service = new EventRecordingService(); var trade = new trade { id = 100 }; service.Record( trade, new[] { before }, Array.Empty(), new[] { info }, Array.Empty(), RecordDate); service.Record( trade, new[] { before }, Array.Empty(), new[] { info }, Array.Empty(), RecordDate); Assert.AreEqual(1, service.Events.Count); Assert.AreEqual(1000m, before.PosiQuantity, "登记日不能改持仓数量"); Assert.AreEqual(100m, before.PosiGrossPrice, "登记日不能改持仓价格"); var pending = JsonConvert.DeserializeObject(service.Events[0].EventData); Assert.IsFalse(pending.Applied); Assert.AreEqual(RecordDate, service.Events[0].ValueDate.Date); service.Record( trade, new[] { after }, new[] { before }, Array.Empty(), new[] { info }, EffectiveDate); Assert.AreEqual(1, service.Events.Count, "生效日应更新原事件而非新增事件"); Assert.AreEqual(1, service.UpdateCount); var applied = JsonConvert.DeserializeObject(service.Events[0].EventData); Assert.IsTrue(applied.Applied); Assert.AreEqual(1000m, applied.BeforeQuantity); Assert.AreEqual(2000m, applied.AfterQuantity); Assert.AreEqual(50m, applied.AfterPrice); Assert.AreEqual(RecordDate, service.Events[0].ValueDate.Date); } private sealed class EventRecordingService : TestableSwapEodPositionService { public List Events { get; } = new List(); public int UpdateCount { get; private set; } public EventRecordingService() : base(nameof(CorporateActionEventLifecycleTest)) { } protected override List FindCorporateActionEvents(int swapTradeId) { return Events; } protected override swap_event AddSwapEvent( DateTime tradeDate, int swapTradeId, int eventType, string data, int clientCashId, bool save, string reason) { return new swap_event { id = Events.Count + 1 }; } protected override void UpdateCorporateActionEventRecord(swap_event swapEvent) { UpdateCount++; } public void Record( trade trade, IReadOnlyCollection current, IReadOnlyCollection previous, IReadOnlyCollection registration, IReadOnlyCollection effective, DateTime settleDate) { RecordCorporateActionEvents( trade, current, previous, registration, effective, settleDate); } } private static ex_dividend_info CreateAction(int id, string instrumentType) { return new ex_dividend_info { id = id, UnderlyingCode = instrumentType == ConsGlobal.InstrumentType.Fund ? "FUND.TEST" : "STOCK.TEST", ExDividendDate = RecordDate, EffectiveDate = EffectiveDate, GiveShareAmount = 0m, GiveCashAmount = 0m, ValidStatus = true }; } private static eod_swap_position CreateEodPosition(long positionId, string code, decimal quantity, decimal price) { return new eod_swap_position { PositionId = positionId, UnderlyingCode = code, UnderlyingInstrumentType = ConsGlobal.InstrumentType.Stock, PosiQuantity = quantity, PosiGrossPrice = price, PosiNotionalValue = quantity * price, PosiNetPrice = price, ContractSize = 1m, PosiDirection = 1, PositionType = 1 }; } } }