using YLErp.Commons; using YLErp.DBModels; using YLErp.Modules.RiskEngine.Dto; namespace YLErp.Modules.RiskEngine { [TestClass] public class RuleConditionExpressionBuilderTest { [TestMethod] public void Build_NumericFixedValue_UsesNormalizedLiteralWithoutSuffix() { var expression = Build( new RuleCondition { VariableId = 1, Operator = "gt", ThresholdType = "Fixed", Value = "123.4500" }, Variable(1, RiskVariableDataType.Numeric, "trade.Amount")); Assert.AreEqual( "trade.Amount > 123.45", expression); Assert.IsFalse(expression.Contains("123.45m")); } [DataTestMethod] [DataRow("0", "0")] [DataRow("-0", "0")] [DataRow("123.4500", "123.45")] [DataRow("1e3", "1000")] [DataRow("1e-28", "0.0000000000000000000000000001")] public void Build_NumericFixedValue_NormalizesLikeFrontend(string input, string expectedValue) { var expression = Build( new RuleCondition { VariableId = 1, Operator = "eq", ThresholdType = "Fixed", Value = input }, Variable(1, RiskVariableDataType.Numeric, "amount")); Assert.AreEqual( $"amount == {expectedValue}", expression); } [TestMethod] public void Build_NumericFixedValue_RejectsThousandsSeparator() { Assert.ThrowsException(() => Build( new RuleCondition { VariableId = 1, Operator = "eq", ThresholdType = "Fixed", Value = "1,000" }, Variable(1, RiskVariableDataType.Numeric, "amount"))); } [DataTestMethod] [DataRow("gt", ">")] [DataRow("lt", "<")] [DataRow("gte", ">=")] [DataRow("lte", "<=")] [DataRow("eq", "==")] [DataRow("ne", "!=")] public void Build_ComparisonTokens_MapToCSharpOperators(string token, string expectedOperator) { var expression = Build( new RuleCondition { VariableId = 1, Operator = token, ThresholdType = "Fixed", Value = "0" }, Variable(1, RiskVariableDataType.Numeric, "amount")); StringAssert.Contains(expression, $" {expectedOperator} "); } [TestMethod] public void Build_NumericVariableThreshold_UsesStoredExpressions() { var expression = Build( new RuleCondition { VariableId = 1, Operator = "lte", ThresholdType = "Variable", ThresholdVariableId = 2 }, Variable(1, RiskVariableDataType.Numeric, "trade.Amount", "元"), Variable(2, RiskVariableDataType.Numeric, "limit.Amount", "元")); Assert.AreEqual( "trade.Amount <= limit.Amount", expression); } [TestMethod] public void Build_NumericVariableThreshold_RejectsDifferentUnit() { var exception = Assert.ThrowsException(() => Build( new RuleCondition { VariableId = 1, Operator = "lte", ThresholdType = "Variable", ThresholdVariableId = 2 }, Variable(1, RiskVariableDataType.Numeric, "trade.Amount", "元"), Variable(2, RiskVariableDataType.Numeric, "trade.Count", "笔"))); StringAssert.Contains(exception.Message, "单位不一致"); } [TestMethod] public void Build_NumericRangeVariableBound_RejectsDifferentUnit() { var exception = Assert.ThrowsException(() => Build( new RuleCondition { VariableId = 1, Operator = "between", LowerThresholdType = "Fixed", LowerValue = "0", UpperThresholdType = "Variable", UpperThresholdVariableId = 2, IncludeLower = true, IncludeUpper = true }, Variable(1, RiskVariableDataType.Numeric, "trade.Amount", "元"), Variable(2, RiskVariableDataType.Numeric, "trade.Count", "笔"))); StringAssert.Contains(exception.Message, "单位不一致"); } [TestMethod] public void Build_FixedDate_UsesDateConstructorTemplate() { var expression = Build( new RuleCondition { VariableId = 1, Operator = "eq", ThresholdType = "Fixed", Value = "2026-07-21" }, Variable(1, RiskVariableDataType.Date, "trade.TradeDate")); Assert.AreEqual("trade.TradeDate == new DateTime(2026, 7, 21)", expression); } [DataTestMethod] [DataRow("isTrue", "trade.IsConfirmed == true")] [DataRow("isFalse", "trade.IsConfirmed == false")] public void Build_BooleanToken_UsesBooleanLiteral(string token, string expected) { var expression = Build( new RuleCondition { VariableId = 1, Operator = token }, Variable(1, RiskVariableDataType.Boolean, "trade.IsConfirmed")); Assert.AreEqual(expected, expression); } [TestMethod] public void Build_Between_AllowsMixedFixedAndVariableBounds() { var expression = Build( new RuleCondition { VariableId = 1, Operator = "between", LowerThresholdType = "Fixed", LowerValue = "-1.25", UpperThresholdType = "Variable", UpperThresholdVariableId = 2, IncludeLower = false, IncludeUpper = true }, Variable(1, RiskVariableDataType.Numeric, "trade.Amount"), Variable(2, RiskVariableDataType.Numeric, "limit.Upper")); Assert.AreEqual( "trade.Amount > -1.25 && trade.Amount <= limit.Upper", expression); } [TestMethod] public void Build_NotBetween_NegatesCompleteRangeExpression() { var expression = Build( new RuleCondition { VariableId = 1, Operator = "notBetween", LowerThresholdType = "Fixed", LowerValue = "2026-01-01", UpperThresholdType = "Fixed", UpperValue = "2026-12-31", IncludeLower = true, IncludeUpper = false }, Variable(1, RiskVariableDataType.Date, "trade.TradeDate")); Assert.AreEqual( "!(trade.TradeDate >= new DateTime(2026, 1, 1) && trade.TradeDate < new DateTime(2026, 12, 31))", expression); } [TestMethod] public void Build_MultipleConditions_UsesExactAndSeparator() { var conditions = new[] { new RuleCondition { VariableId = 1, Operator = "gt", ThresholdType = "Fixed", Value = "0" }, new RuleCondition { VariableId = 2, Operator = "isTrue" } }; var variables = Variables( Variable(1, RiskVariableDataType.Numeric, "amount"), Variable(2, RiskVariableDataType.Boolean, "isValid")); var expression = RuleConditionExpressionBuilder.Build(conditions, variables); Assert.AreEqual( "amount > 0 && isValid == true", expression); } [TestMethod] public void Build_RejectsNonRangeConditionWithRangeFields() { var condition = new RuleCondition { VariableId = 1, Operator = "gt", ThresholdType = "Fixed", Value = "0", IncludeLower = true }; Assert.ThrowsException(() => Build( condition, Variable(1, RiskVariableDataType.Numeric, "amount"))); } [TestMethod] public void ReferencedVariableIds_IgnoreNullConditionItems() { var conditions = new RuleCondition[] { null, new RuleCondition { VariableId = 7 } }; CollectionAssert.AreEqual( new long[] { 7 }, RuleConditionExpressionBuilder.GetReferencedVariableIds(conditions).ToArray()); } [TestMethod] public void ReferencedVariableIds_IncludeAllFourFields() { var conditions = new[] { new RuleCondition { VariableId = 1, ThresholdVariableId = 2, LowerThresholdVariableId = 3, UpperThresholdVariableId = 4 } }; CollectionAssert.AreEquivalent( new long[] { 1, 2, 3, 4 }, RuleConditionExpressionBuilder.GetReferencedVariableIds(conditions).ToArray()); } [TestMethod] public void IsVariableReferenced_MatchesAllFieldsWithoutIdPrefixCollision() { const string json = "[{\"VariableId\":10,\"ThresholdVariableId\":20,\"LowerThresholdVariableId\":30,\"UpperThresholdVariableId\":40}]"; Assert.IsTrue(RuleConditionExpressionBuilder.IsVariableReferenced(json, 10)); Assert.IsTrue(RuleConditionExpressionBuilder.IsVariableReferenced(json, 20)); Assert.IsTrue(RuleConditionExpressionBuilder.IsVariableReferenced(json, 30)); Assert.IsTrue(RuleConditionExpressionBuilder.IsVariableReferenced(json, 40)); Assert.IsFalse(RuleConditionExpressionBuilder.IsVariableReferenced(json, 1)); Assert.IsFalse(RuleConditionExpressionBuilder.IsVariableReferenced(json, 4)); } private static string Build(RuleCondition condition, params glms_risk_variable[] variables) { return RuleConditionExpressionBuilder.Build(new[] { condition }, Variables(variables)); } private static IReadOnlyDictionary Variables(params glms_risk_variable[] variables) { return variables.ToDictionary(variable => (long)variable.id); } private static glms_risk_variable Variable(long id, RiskVariableDataType dataType, string expression, string unit = null) { return new glms_risk_variable { id = (int)id, VariableName = $"V{id}", DataType = dataType, VariableExpr = expression, Unit = unit }; } } }