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:

Copy
@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.

Copy
@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%):

Copy
@score(0.4)
rules.highTransactionValue: event.amount.baseValue > 150
Copy
@score(0.25)
rules.highRiskMCC:
[ "7999", "7995", "6051", "5912", "5933" ] ~#event.merchantCategoryCode
Copy
@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.

Copy
@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.

Copy
@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:

Copy
@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:

Copy
@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.

Copy
@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":

Copy
{
"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:

Copy
@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:

Copy
@tag(<namespace1>="<value1>","<namespace2>="<value2>"…)

To add multiple tags in the same namespace, use this syntax:

Copy
@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:

Copy
@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":

Copy
{
"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:

Copy
@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:

Copy
{
"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:

Copy
@eventType("registration")
state.customerSegment: event.customerSegment

This rules ensures that a test transaction rule does not apply to VIP customers:

Copy
@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:

Copy
@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:

Copy
@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

Previous Transaction Value: N/A

N/A

No alert

10:30 am

Cardholder

Transaction
Value: £ 90

Previous Transaction Value: £5

30 mins

No alert

10:45 am

Criminal

Transaction Value: £ 1000

Previous Transaction Value: £100

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:

Copy
@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:

Copy
@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

Previous Transaction Value: N/A

N/A

No alert

10:30 am

Cardholder

Transaction Value: £ 90

Previous Transaction Value: £5

30 mins

No alert

10:45 am

Criminal

Transaction Value: £ 1000

Previous Transaction Value: £100

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:

Copy
@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:

Copy
@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):

Copy
@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):

Copy
values.highRiskMCCs: [ "7999", "7995", "6051", "5912", "5933" ]

You could then reference this static array in the rule to achieve the same effect as above:

Copy
@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:

Copy
@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:

Copy
@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:

Copy
@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:

Copy
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:

Copy
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.

Copy
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:

Copy
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.

Copy
@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:

Copy
@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):

Copy
{
"
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.