4 Customising Business Rules in AMDL
Business Rules AMDL lets you implement fraud and financial risk detection logic by
breaking down the logic into units of AMDL expressions. The Getting Started section shows you how to create a basic rule and add state information. This section shows you how you can customise rules. The examples in this section use transaction
and other event types (see Events for more details).
4.1 Suppressing Alerts and Tags
In some cases, it may be desirable to suppress the generation of alerts or tags for
specific events — for example, to prevent customers on a VIP list from having their
transactions held or blocked, for example a CEO or person of high net worth. You can add the @suppressAlert
to a rule in order to
prevent any alerts from being generated by AMDL rules or model risk thresholds. When a
rule with this annotation triggers, all alerts are suppressed for that event/entity
combination. The following example shows you how to prevent alerts being generated on transactions made
by VIP customers:
@suppressAlert
@eventType("transaction")
rules.noAlertsForVIPs: event.customerSegment == "V"
Automated responses to rules (e.g. holding or declining a transaction) may also be based
on tags added to an event. You can use the @suppressTag
annotation to prevent
specific tags from being added to the system response. You supply the tag, or lists of tags, for suppression as an argument to the annotation.
For example, this rule prevents VIP customers from having their transactions declined or routed via 3DS for declining deposits when the adding the action="DENY"
tag to the
output, and using the via3DS="Y"
tag to route via 3D Secure authentication.
@suppressTag(action="DENY")
@suppressTag(via3DS="Y")
@eventType("transaction")
rules.noInconveniencesForVIPs: event.customerSegment == "V"
As with @suppressAlert
, @suppressTag
suppresses tag emission from all sources,
including AMDL rules, models, risk score thresholds and aggregators. Suppressed tags
are never shown in the UI, and are removed from the outputTags
key in the JSON
response. However, the
models section of the response lists any tags assigned by models under tags.
Note that, because you define Business Rules for a specific entity type, alert and tag
suppression only work within an entity type. If you write an expression with the
@alert
annotation for one entity type, and an expression with the @suppressAlert
annotation against a different entity type, the second expression does not suppress the
alert generated by the first. This is true even if both alerts trigger on the same event.
4.2 Adding Rule Scores
You can configure Rules to output a score using the @score
annotation. Each rule supplies the score as an argument, where scores may be positive or negative. The total of all
the scores from triggered rules for a specific event is output as the 'businessrules'
model score. This score is rescaled in the same way that model scores are — a total score
of 1 is equal to 100%. For example, for an event with an MCC of 5678 and an amount of
£200, the following rules output a total score of 0.3 (30%):
@score(0.4)
rules.highTransactionValue: event.amount.baseValue > 150
@score(0.25)
rules.highRiskMCC:
[ "7999", "7995", "6051", "5912", "5933" ] ~#event.merchantCategoryCode
@score(-0.1)
rules.currencyIsGBP: event.amount.currency == "GBP"
If the 'businessrules' model score exceeds the overall risk score generated by any other model(s) for an event, it displays as the overall risk score in the Fraud Transaction Monitoring Portal for that event. These may result in risk score bars in excess of 100% or that are negative. While displayed appropriately in Fraud Transaction Monitoring System it may or may not be appropriate to assign events a risk above 100%.
var and rules Scope
The @score
annotation exists in both the var
and the rules
scope. A scope allows you to write expressions that refer to another value (or operand), or provide definitions.
When added to the var
expression, the score contributes the evaluated result of the expression to the risk score output of the 'businessrules' model. The following example scales the model down by 70% (represented as models.externalModel1.score
) and adds the value to the 'businessrules' model score.
@score
var.ruleModelScore: models.externalModel1.score * 0.7
The score can only contribute to the evaluated result to the model score if it is:
-
a single numeric value
-
either positive or negative
-
greater than 1
Thus, decimals are not allowed for the score value.
Annotating multiple var expressions with @score
adds up the score. The score also includes any scores from @score(<score>)
annotated rule expressions. The result is in an output of the total 'businesrules' model score.
When added to a rules expression, the annotation provides a static value to the expression that is returned if a specific rule is triggered. The value is added to any other rules triggered by the score annotation, and any other var @score
value, resulting in an output of the total 'businesrules' model score.
The following example adds 0.4 as the static value to the 'businesrules' model score if the event.amount.baseValue
is more than 150. The name of the business rule here is highTransactionValue
.
@score(0.4)
rules.highTransactionValue: event.amount.baseValue > 150
Defining score generating expressions in the var
scope is recommended as it enables both dynamic generation of the model at runtime and more complex logic in a single expression.
4.3 Limiting Events through Annotations
You can limit the events for evaluating a rule using the eventType
annotation.
For example, you can specify a rule that is only evaluated on the "transaction"
event type:
@eventType("transaction")
rules.transactionGreaterThan100: event.amount.baseValue > 100
You can define a rule that is evaluated for multiple event types by adding additional
eventType
annotations. For example:
@eventType("transaction")
@eventType("accountTransfer")
rules.transactionOrAccountTransferGreaterThan100:
event.amount.baseValue > 100
If you do not specify event type annotations on a rule, the rule is evaluated on every event.
However, this rule does not produce any effect when triggered. In AMDL, you use annotations to cause a rule to generate an alert or add a tag to the output when it triggers.
You use the @alert
annotation to create an alert in the Fraud Transaction Monitoring Portal if the rule triggers. An analyst
can then review the incident and add the alert to the rule.
@alert
@eventType("transaction")
@eventType("accountTransfer")
rules.transactionOrAccountTransferGreaterThan100:
event.amount.baseValue > 100
It is important which entity type this rule is written against, because the alert is raised against the entity of that type in the evaluated event.
For example, if you write this rule against the 'customer' entity type, the following event generates an incident for the entity "Customer1":
{
"eventType": "transaction",
"eventTime": "2019-05-05T18:02:55Z",
"customerId": "Customer1",
"merchantId": "Merchant2",
"amount": {
"baseValue": 150,
...
}
However, if you write this rule against the 'merchant' entity type, the incident relates to "Merchant2"
.
You can also use the @tag
annotation to add a tag to the output from the system when the
rule triggers. For instance, you could add tags to the rule as follows:
@alert
@tag("High value transaction or account transfer")
@tag(action="BLOCK")
@eventType("transaction")
@eventType("accountTransfer")
rules.transactionOrAccountTransferGreaterThan100:
event.amount.baseValue > 100
This rule produces a tag with the namespace of action
and value "BLOCK"
, and another
which displays as the text "High value transaction or account transfer"
in the
Fraud Transaction Monitoring Portal.
The general form of the @tag annotation is @tag
(<namespace>="<value>","<namespace>="<value>")
. You can add more than tag by
adding the @tag annotation multiple times. Or, you can add multiple tags using the same
annotation:
@tag(<namespace1>="<value1>","<namespace2>="<value2>"…)
To add multiple tags in the same namespace, use this syntax:
@tag(<namespace1>="<value1>","<namespace1>="<value2>"…)
If you do not provide a namespace, you can use a default namespace (_tag
). If the namespace is
unimportant, you can write @tag("<value>")
. The portal displays this tag without a
namespace.
4.4 Using State in a Rule
You can detect test transactions, which include a low value transaction to test the payment method used followed by a large transaction. To detect a low-value transaction followed by a high-value one, you need to be able to store some information about the customer's previous transaction.
To store the value of the most recent transaction each customer has made, you can write a state expression:
@eventType("transaction")
state.previousTransactionValue: event.amount.baseValue
Similar to rules, the @eventType
annotation limits the execution of this
expression to transaction events.
The entity type against which you write this expression is important, as the state is stored for the entity of this type. For the following event, if the above expression is written against the 'customer' entity type, this variable stores the value 100 in the state of the entity "Customer1":
{
"eventType": "transaction",
"eventTime": "2019-05-05T18:02:55Z",
"customerId": "Customer1",
"merchantId": "Merchant2",
"amount": {
"value": 100,
"currency": "GBP",
"baseValue": 100,
"baseCurrency": 100
},
...
}
You can then reference this state expression in a rule, provided that rule is written against the same entity type. This state expression is evaluated for each transaction event, and the transaction value stored against the relevant entity. However, this evaluation is done after the evaluation of rules and other expressions. Thus, when evaluating a rule, the value of the state expressions it references are the values as of the most recent event for that entity.
For example, this rule generates an alert if a customer makes a low-value transaction of less than 10 and their next transaction event is a high-value transaction of over 100:
@eventType("transaction")
rules.testTransaction: event.amount.baseValue > 100 &&
state.previousTransactionValue < 10
You can also write state expressions based on one event type, which are referenced in rules written against another event type. For example, you can store information about a customer when they register, and use that information in a rule.
For example, suppose a registration event contains the following data:
{
"eventId": "14b1f1e1a124c",
"eventType": "registration",
"eventTime": "2019-04-01T12:10:30Z",
"customerId": "3263827",
"customerSegment": "B",
"name": {
"firstName": "Exem"
"lastName": "Plar"
},
"email": "mrexample1@gmail.com",
"deviceData": {
"deviceId": "a85531c1-02d8-44ed-964f-0706155209c7",
"deviceType": "Android",
"OSversion": "10.0.1"
}
}
This data can be stored against the 'customer' entity type. For example, storing the customer's customer segment code determines whether they are a VIP customer when rules are executed:
@eventType("registration")
state.customerSegment: event.customerSegment
This rules ensures that a test transaction rule does not apply to VIP customers:
@eventType("transaction")
rules.testTransaction: event.amount.baseValue > 100 &&
state.previousTransactionValue < 10 &&
state.customerSegment != "V"
4.5 Applying Timestamps and Durations and Conditional State
You can apply timestamps to a rule and durations. In addition, you can apply conditional states.
Timestamps and Elapsed Time in Business Rules
In the previous example, the created rule triggers if a customer made a low-value transaction followed by a high-value one. However, that rule does not reference the time elapsed between those two transactions. With a typical test transaction, a high-value transaction is usually made shortly afterwards. A low value transaction is unlikely to be a test transaction if the following high value one takes place a week later. Times and durations (see Dates, Times and Durations) are therefore important in Business Rules.
To store the timestamp of each transaction for a customer, you can use a state expression:
@eventType("transaction")
state.previousTransactionTime: event.eventTime
You can then refer to the state expression in a rule, to determine the elapsed time since the previous transaction. This means you can modify the rule from example 2 to take into account the elapsed time since the low-value transaction. The following example is a rule triggered less than 2 hours since the previous transaction:
@eventType("transaction")
rules.testTransaction:
event.amount.baseValue > 100 &&
state.previousTransactionValue < 10 &&
event.eventTime - state.previousTransactionTime < 2h
state.previousTransactionTime
and event.eventTime
are both fields that
contain date-times, and subtracting one from the other gives a duration representing the
elapsed time between the two events.
Conditional State
In the example of Timestamps and Elapsed Time in Business Rules, you create a rule that checks the value and time of the last transaction where it triggers if a low-value transaction is followed by a high-value one within 2 hours. However, this rule could fail to detect a test transaction if a customer's card has been compromised and the card details are being used by both the criminal and the genuine cardholder.
Consider the following sequence of events:
Time |
Cardholder or criminal? |
Transaction value change |
Time since previous transaction |
Result |
---|---|---|---|---|
10:00 am |
Criminal |
Transaction Value: £ 5 |
N/A |
No alert |
10:30 am |
Cardholder |
Transaction |
30 mins |
No alert |
10:45 am |
Criminal |
Transaction Value: £ 1000 |
15 mins |
No alert |
There are no events in this sequence that match all the conditions in the rule. A better approach is to track the time since the last low-value transaction, and trigger the rule if you see a high-value transaction within 2 hours of a low-value one, regardless of whether there were any intervening transactions.
You can use a conditional assignment (see Conditional Assignment) to create state expressions that only store a value when a certain condition is true. This enables you to create an expression that stores the time of the last low-value transaction for each customer:
@eventType("transaction")
state.previousLowValueTransactionTime:
event.amount.baseValue <= 10 ?
event.eventTime
This expression stores the date and time only if the value of the transaction is less than or equal to 10. If the value is greater than 10 the state expression does not update. You can then create a version of the rule that refers to this state:
@eventType("transaction")
rules.testTransaction:
event.amount.baseValue > 100 &&
event.eventTime - state.previousLowValueTransactionTime < 2h
The rule now triggers according to the following sequence of events:
Time |
Cardholder or criminal? |
Transaction value change: |
Time since previous transaction |
Result |
---|---|---|---|---|
10:00 am |
Criminal |
Transaction Value: £ 5 |
N/A |
No alert |
10:30 am |
Cardholder |
Transaction Value: £ 90 |
30 mins |
No alert |
10:45 am |
Criminal |
Transaction Value: £ 1000 |
15 mins |
Alert |
The "N/A" annotation in the first row is because on the first transaction, the state expression has not yet been evaluated and returns a value of null
. This causes the execution of the rule to stop when it reaches the reference to
state.previousLowValueTransactionTime
, meaning that the rule fails to execute
and does not generate any output for this event. Whenever an expression refers to a
state variable that has no value for the current entity, or references an event field that is
not present in the current event, that undefined value can stop the execution of the expression.
Many state and other variable references return null
.This is usually the intended behaviour as the test transaction rule does not trigger on
the first transaction a customer makes, so when the rule execution stops this does not
cause any issues.
However, in some cases, it is necessary to prevent
references to state variables, event fields or other variables from returning null and
causing the execution of the referring expression to halt. You can
Specifying a Default Value or use the @defaultValue
annotation (see @defaultValue) when defining a single-value
state or global expressions. This ensures that, when an entity or entity type is first seen, the state
variable already has a value.
4.6 Applying Global State and More Annotations
In this example, you create a rule which identifies unusually high spending over a given time period. For example, if normal behaviour indicated an average entity spend of about £100, you could specify a static threshold to trigger a rule if a transaction has a value over 500. However, this might cause problems during times when average spend tends to be higher, such as the run up to Christmas, Black Friday, or Singles' Day in China. It might be better in this case to track the average entity spending and compare the single transaction amount to this moving threshold.
To do this, you can define a global expression that stores state from all customers' transactions as in the following example:
@rollingAverage(24h)
@eventType("transaction")
globals.averageTransactionValue: event.amount.baseValue
You use the @rollingAverage
annotation (see @rollingAverage) for calculating an average value instead of storing a value which gets overwritten
each time a transaction is processed.
You can then define a rule that generates an alert for any transaction with a value more than five times the rolling average:
@alert
@eventType("transaction")
rules.highValueTransaction:
event.amount.baseValue > 5 * globals.averageTransactionValue
4.7 Using Collections
You can also create collections in AMDL expressions. Collections are useful for storing multiple values or comparing a field in the event data against a list of possible values. For example, you might want to generate an alert for a high-value transaction at a merchant within a high-risk category, as indicated by the Merchant Category Code (MCC):
@alert
@eventType("transaction")
rules.highValueTransactionHighRiskMCC:
event.amount.baseValue > 5 * globals.averageTransactionValue &&
[ "7999", "7995", "6051", "5912", "5933" ] ~# event.merchantCategoryCode
This rule generates an alert if the transaction value exceeds 5 times the global average,
and the MCC is one of those listed in the array (using the "contains" operator, ~#
). For more details, refer to the Collections section.
To use this list of high-risk MCCs in several expressions, or for separating this definition from the business logic of the rule itself, you could store it as a static value using the "values" scope (see Static Values):
values.highRiskMCCs: [ "7999", "7995", "6051", "5912", "5933" ]
You could then reference this static array in the rule to achieve the same effect as above:
@alert
@eventType("transaction")
rules.highValueTransaction_highRiskMCC:
event.amount.baseValue > 5 * globals.averageTransactionValue &&
values.highRiskMCCs ~# event.merchantCategoryCode
Note that the values expression above must be defined for the same entity type as this rule.
You can also create expressions that store collections in an entity or global state. For
example, you can store a list of all the payment methods used with a DPAN/FPAN over the last year to trigger a rule whenever there is a large transaction
using a payment method not seen before in that time. To store a list of payment methods, you can write a state expression using the @set
annotation:
@set(365d)
@eventType("transaction")
state.paymentMethodsInLastYear: event.paymentMethod.methodId
You can then write a rule that checks whether the payment method of the current transaction event is contained in that set:
@alert
@eventType("transaction")
rules.highValueTransaction_newPaymentMethod:
event.amount.baseValue > 500 &&
state.paymentMethodsInLastYear !# event.paymentMethod.methodId
You use "does not contain" operator, !#
, where the rule triggers if the value is over
500 and the payment method is not contained in the collection of payment methods for
the current entity.
4.8 Applying Cross-Entity State References
In AMDL, each entity type has its own separate set of rules and state definitions. However, where more than one entity is present in an event, expressions defined for one entity type can refer to the state variables defined for other entities in the event. For example, if an event contains both a customer entity and a merchant entity, and the merchant has the following state definition:
@eventType("transaction")
@rollingAverage(1d)
state.avgTxnValue: event.amount.baseValue
A rule written against the customer entity type could refer to this state variable using the following syntax:
state.entities.<entity type>.<state variable>
This returns an array of the values of <state variable>
for all entities of <entity
type>
in the event, even if there is only one entity of that type.
For example, an expression defined for the customer entity type could reference the merchant state defined above:
rules.transactionExceedsMerchantAverage:
event.amount.baseValue > state.entities.merchant.avgTxnValue.single()
The expression uses the single()
method because the state.entities
reference returns a
collection, and this converts the collection into a single value.
You can test rules that use cross-entity in the AMDL unit test environment. You use the @entitytype
annotation to specify the entity for the state expressions that are assigned values in the Initial State panel (see Entities in Unit Tests).
4.9 Rule References
An AMDL Business Rule expression can reference the result of a rule as a Boolean value.
rules.rule1AndHighValue: rules.rule1 && event.amount.baseValue > 100
Rule references in rule declarations cannot have any circular references. However, there is no restriction on rule references in state update expressions, because all rules are evaluated before state is updated.
Note that if a rule does not evaluate, any reference to that rule returns undefined.
Default values can be assigned to rule outputs using the ??
operator.
For example, if rule1
in the example does not evaluate,
it can be defaulted to 'false' as shown below:
rules.rule1AndHighValue: ( rules.rule1 ?? false ) && event.amount > 100
4.10 Outputting Values
Using the @output annotation to output a value is only permitted in Business Rules AMDL expressions.
You can use the @output
annotation to output a calculated value (for example, the
value of a transient variable), either as a tag (which is visible in the Fraud Transaction Monitoring Portal Incident
Management page), or as part of the output produced by the fraud system in response to
an event.
You can use @output
in Business Rules expressions within the var
or rules
scope.
When used on a var
expression, the @output
annotation causes output to be generated
for any event for which the expression is evaluated. Thus, the output contains the value of
the var
expression for that event.
Output as a Tag
The default behaviour is to output the value as a tag, which is visible in the Fraud Transaction Monitoring Portal Incident
Management page. You can enter an optional argument, which defines the outputted tag
namespace. However, if you do not supply the argument, the namespace defaults to the name of the var
expression.
For example, the expression below adds a tag with the namespace "Daily account
position", containing the value of var.dailyPosition
for that event, to all deposit and
withdrawal events.
@eventType("deposit")
@eventType("withdrawal")
@output("Daily account position")
var.dailyPosition:
state.deposits24h.total() - state.withdrawals24h.total() +
event.amount.baseValue * ( event.eventType == "deposit" ? 1 : -1 )
You can use the @output
annotation in a rule in the following manner:
-
If the rule evaluates to true or false, the value output is true or false accordingly.
-
If the rule does not evaluate, no output tag is generated.
However, using the annotation this way can generate a large numbers of tags, which can cause performance issues and may result in new tags no longer being stored or displayed for certain entities. This is because there is a configurable limit on the number of unique tag values stored for a given entity, and each distinct value output by an expression like the one above counts as a unique tag value. Once an entity has exceeded this limit, no further tags are displayed for this entity.
'Rules Output' Mode
Alternatively, you can use the 'rules output' mode of the @output
annotation. This causes
the expression to output its value in the output data from the Fraud Transaction Monitoring System, rather than
as a tag. You can change the output mode by specifying a value for the mode argument.
Valid options are ruleoutput
and tag
(for the tag output mode described above — this is
the default).
Modifying the example above to use rule output mode gives:
@eventType("deposit")
@eventType("withdrawal")
@output(mode=ruleoutput)
var.dailyPosition:
state.deposits24h.total() - state.withdrawals24h.total() +
event.amount.baseValue * ( event.eventType == "deposit" ? 1 : -1 )
The output data is formatted as follows in the output from the Fraud Transaction Monitoring System (using the example above):
{
…"
models": [
{
"modelData": {
"dailyPosition": 568.25
}"
modelId": "businessrules",
…
},
…
],
…
}
Note that you cannot use this output mode for rules. You can only use it for transient variables.