using YLErp.Abstract; using YLErp.Helpers; using YLErp.Modules.EodModule; namespace YLErp.Modules.EodModuleTests { [TestClass] public class TrsContractKafkaPushServiceTest { [TestMethod] public void Push_空日快照_发送一条空消息并使用业务日期作为Key() { var valueDate = new DateTime(2026, 8, 24); var producer = new RecordingKafkaProducer(); var service = CreateService(producer, valueDate); service.Push(valueDate); Assert.AreEqual(1, producer.Messages.Count); Assert.AreEqual("onederiv.trs.contract.v1", producer.Messages[0].Topic); Assert.AreEqual("2026-08-24", producer.Messages[0].Key); var payload = JsonHelper.Deserialize(producer.Messages[0].Message); Assert.AreEqual("2026-08-24", payload.ValueDate); Assert.AreEqual(0, payload.ContractCount); Assert.AreEqual(0, payload.Contracts.Count); } [TestMethod] public void Push_区间内每天分别调用_每个日期各发送一条快照() { var valueDates = new[] { new DateTime(2026, 8, 20), new DateTime(2026, 8, 21), new DateTime(2026, 8, 24) }; var producer = new RecordingKafkaProducer(); var service = new TestableTrsContractKafkaPushService(producer, valueDates.ToDictionary(x => x, CreateEmptySnapshot)); foreach (var valueDate in valueDates) { service.Push(valueDate); } CollectionAssert.AreEqual( new[] { "2026-08-20", "2026-08-21", "2026-08-24" }, producer.Messages.Select(x => x.Key).ToArray()); CollectionAssert.AreEqual( new[] { "2026-08-20", "2026-08-21", "2026-08-24" }, producer.Messages.Select(x => JsonHelper.Deserialize(x.Message).ValueDate).ToArray()); } [TestMethod] public void Push_首次失败后成功_停止重试且不记录最终失败() { var valueDate = new DateTime(2026, 8, 24); var producer = new RecordingKafkaProducer { FailuresBeforeSuccess = 1 }; var service = CreateService(producer, valueDate); service.Push(valueDate); Assert.AreEqual(2, producer.AttemptCount); Assert.AreEqual(1, producer.Messages.Count); Assert.AreEqual(0, service.FailureRecords.Count); } [TestMethod] public void Push_连续失败三次_记录最终失败和三次尝试() { var valueDate = new DateTime(2026, 8, 24); var producer = new RecordingKafkaProducer { FailuresBeforeSuccess = int.MaxValue }; var service = CreateService(producer, valueDate); service.Push(valueDate); Assert.AreEqual(3, producer.AttemptCount); Assert.AreEqual(0, producer.Messages.Count); Assert.AreEqual(1, service.FailureRecords.Count); Assert.AreEqual(valueDate, service.FailureRecords[0].ValueDate); Assert.AreEqual(3, service.FailureRecords[0].RetryCount); Assert.IsInstanceOfType(service.FailureRecords[0].Exception, typeof(InvalidOperationException)); } [TestMethod] public void BuildContract_字段使用日终快照和约定来源() { var valueDate = new DateTime(2026, 8, 24); var eodSwap = new eod_swap { id = 10, ValueDate = valueDate, SwapTradeId = 7, SwapTradeNo = "TRS-001", BookId = 3, ClientId = 8, NotionalValue = 1000000m, dv01 = 12.34m, InitMarginGain = 100m, InitMarginLoss = 0m }; var trade = new trade { id = 7, UnderlyingCode = "600000.SH", UnderlyingAssetName = "浦发银行", UnderlyingInstrumentType = "Stock", StartDate = new DateTime(2026, 8, 1), ExerciseDate = new DateTime(2027, 8, 1) }; var positions = new List { new() { SwapTradeId = 7, PositionId = 101, UnderlyingCode = "600000.SH", PositionType = 1 }, new() { SwapTradeId = 7, PositionId = 102, InterestMode = (int)InterestModeEnum.固定值, InterestRateDefault = 0.0123m, InterestDirection = 2 } }; var swapPositions = new Dictionary { [101] = new() { id = 101, category_tag = "互换利率" }, [102] = new() { id = 102, category_tag = "互换利率" } }; var item = TrsContractKafkaPushService.BuildContract( eodSwap, new Dictionary { [7] = trade }, positions, swapPositions); Assert.AreEqual("2026-08-24", item.TradeDate); Assert.AreEqual(3, item.BookId); Assert.AreEqual("TRS-001", item.SwapTradeNo); Assert.AreEqual(8, item.ClientId); Assert.AreEqual("600000.SH", item.UnderlyingCode); Assert.AreEqual("浦发银行", item.UnderlyingName); Assert.AreEqual("Stock", item.UnderlyingInstrumentType); Assert.AreEqual(1000000m, item.NotionalValue); Assert.AreEqual("2026-08-01", item.StartDate); Assert.AreEqual("2027-08-01", item.MaturityDate); Assert.AreEqual(12.34m, item.Dv01); Assert.AreEqual(0.0123m, item.FixedRate); Assert.AreEqual(2, item.InterestDirection); Assert.AreEqual(1, item.FloatingDirection); Assert.AreEqual(100m, item.InitMarginGain); Assert.AreEqual(0m, item.InitMarginLoss); } [TestMethod] public void BuildContract_历史类别为空_仍推送全部TRS合约() { var valueDate = new DateTime(2026, 2, 10); var eodSwap = new eod_swap { ValueDate = valueDate, SwapTradeId = 1645, SwapTradeNo = "ZSZQ-IS-202602090001" }; var positions = new List { new() { SwapTradeId = 1645, PositionId = 33306, UnderlyingCode = "220208.IB", PositionType = 1 }, new() { SwapTradeId = 1645, PositionId = 33307, InterestMode = (int)InterestModeEnum.标的期初全价, InterestRateDefault = 0.001m, InterestDirection = 1 } }; var swapPositions = new Dictionary { [33306] = new() { id = 33306, category_tag = null }, [33307] = new() { id = 33307, category_tag = null } }; var item = TrsContractKafkaPushService.BuildContract( eodSwap, new Dictionary { [1645] = new() { id = 1645, UnderlyingCode = "220208.IB" } }, positions, swapPositions); Assert.AreEqual(0m, item.FixedRate); Assert.AreEqual(1, item.InterestDirection); Assert.AreEqual(1, item.FloatingDirection); } [TestMethod] public void BuildContract_增强收益腿不参与固定利率取值() { var eodSwap = new eod_swap { ValueDate = new DateTime(2026, 8, 24), SwapTradeId = 7 }; var positions = new List { new() { SwapTradeId = 7, PositionId = 101, UnderlyingCode = "600000.SH", PositionType = 2 }, new() { SwapTradeId = 7, PositionId = 102, InterestMode = (int)InterestModeEnum.固定值, InterestRateDefault = 0.0123m, InterestDirection = 1 }, new() { SwapTradeId = 7, PositionId = 103, InterestMode = (int)InterestModeEnum.固定值, InterestRateDefault = 0.0999m, InterestDirection = 2 } }; var swapPositions = new Dictionary { [101] = new() { id = 101, category_tag = null }, [102] = new() { id = 102, category_tag = "互换利率" }, [103] = new() { id = 103, category_tag = "增强收益" } }; var item = TrsContractKafkaPushService.BuildContract( eodSwap, new Dictionary { [7] = new() { id = 7, UnderlyingCode = "600000.SH" } }, positions, swapPositions); Assert.AreEqual(0.0123m, item.FixedRate); Assert.AreEqual(1, item.InterestDirection); Assert.AreEqual(2, item.FloatingDirection); } [TestMethod] public void BuildContract_多条互换利率腿_取第一条() { var eodSwap = new eod_swap { ValueDate = new DateTime(2026, 8, 24), SwapTradeId = 7 }; var positions = new List { new() { SwapTradeId = 7, PositionId = 101, UnderlyingCode = "600000.SH", PositionType = 1 }, new() { SwapTradeId = 7, PositionId = 102, InterestMode = (int)InterestModeEnum.固定值, InterestRateDefault = 0.0123m, InterestDirection = 1 }, new() { SwapTradeId = 7, PositionId = 103, InterestMode = (int)InterestModeEnum.固定值, InterestRateDefault = 0.0456m, InterestDirection = 2 } }; var swapPositions = new Dictionary { [101] = new() { id = 101, category_tag = null }, [102] = new() { id = 102, category_tag = "互换利率" }, [103] = new() { id = 103, category_tag = "互换利率" } }; var item = TrsContractKafkaPushService.BuildContract( eodSwap, new Dictionary { [7] = new() { id = 7, UnderlyingCode = "600000.SH" } }, positions, swapPositions); Assert.AreEqual(0.0123m, item.FixedRate); Assert.AreEqual(1, item.InterestDirection); } private static TestableTrsContractKafkaPushService CreateService(RecordingKafkaProducer producer, DateTime valueDate) { return new TestableTrsContractKafkaPushService( producer, new Dictionary { [valueDate] = CreateEmptySnapshot(valueDate) }); } private static TrsContractSnapshot CreateEmptySnapshot(DateTime valueDate) { return new TrsContractSnapshot { SchemaVersion = "v1", ValueDate = valueDate.ToString("yyyy-MM-dd"), PushTime = "2026-08-24 12:00:00", ContractCount = 0, Contracts = new List() }; } private sealed class TestableTrsContractKafkaPushService : TrsContractKafkaPushService { private readonly IReadOnlyDictionary _snapshots; public List<(DateTime ValueDate, int RetryCount, Exception Exception)> FailureRecords { get; } = new(); public TestableTrsContractKafkaPushService(IKafkaProduce producer, IReadOnlyDictionary snapshots) : base(new YLContext(), producer, "onederiv.trs.contract.v1") { _snapshots = snapshots; } protected override TrsContractSnapshot BuildSnapshot(DateTime valueDate) { return _snapshots[valueDate]; } protected override void RecordFailures(DateTime valueDate, int retryCount, Exception exception) { FailureRecords.Add((valueDate, retryCount, exception)); } } private sealed class RecordingKafkaProducer : IKafkaProduce { public int FailuresBeforeSuccess { get; set; } public int AttemptCount { get; private set; } public List<(string Topic, string Key, string Message)> Messages { get; } = new(); public void Produce(string topic, string message) { throw new NotSupportedException(); } public void Produce(string topic, string key, string message) { AttemptCount++; if (AttemptCount <= FailuresBeforeSuccess) { throw new InvalidOperationException("Kafka unavailable"); } Messages.Add((topic, key, message)); } } } }