323 lines
11 KiB
C#
323 lines
11 KiB
C#
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<ServiceException>(() => 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<ServiceException>(() => 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<ServiceException>(() => 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<ServiceException>(() => 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<long, glms_risk_variable> 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
|
|
};
|
|
}
|
|
}
|
|
}
|