Skip to content

Commit 2964f71

Browse files
authored
Merge pull request #1415 from maxmind/nlogan/risk-reasons-beta
Add risk reasons
2 parents 89919a4 + 1f76fb1 commit 2964f71

File tree

10 files changed

+180
-10
lines changed

10 files changed

+180
-10
lines changed

CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,14 @@
11
CHANGELOG
22
=========
33

4+
7.1.0-beta.1
5+
------------
6+
7+
* Added support for the new risk reasons outputs in minFraud Factors. The risk
8+
reasons output codes and reasons are currently in beta and are subject to
9+
change. We recommend that you use these beta outputs with caution and avoid
10+
relying on them for critical applications.
11+
412
7.0.0 (2024-07-08)
513
------------------
614

e2e/js/package-lock.json

Lines changed: 1 addition & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

e2e/ts/package-lock.json

Lines changed: 1 addition & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

fixtures/reasons.json

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
[
2+
{
3+
"multiplier": 45,
4+
"reasons": [
5+
{
6+
"code": "ANONYMOUS_IP",
7+
"reason": "Risk due to IP being an Anonymous IP"
8+
}
9+
]
10+
},
11+
{
12+
"multiplier": 1.8,
13+
"reasons": [
14+
{
15+
"code": "TIME_OF_DAY",
16+
"reason": "Risk due to local time of day"
17+
}
18+
]
19+
},
20+
{
21+
"multiplier": 1.6,
22+
"reasons": [
23+
{
24+
"reason": "Riskiness of newly-sighted email domain",
25+
"code": "EMAIL_DOMAIN_NEW"
26+
}
27+
]
28+
},
29+
{
30+
"multiplier": 0.34,
31+
"reasons": [
32+
{
33+
"code": "EMAIL_ADDRESS_NEW",
34+
"reason": "Riskiness of newly-sighted email address"
35+
}
36+
]
37+
}
38+
]

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@maxmind/minfraud-api-node",
3-
"version": "7.0.0",
3+
"version": "7.1.0-beta.1",
44
"description": "Node.js API for MaxMind minFraud Score, Insights, and Factors web services",
55
"main": "dist/src/index.js",
66
"homepage": "https://github.com/maxmind/minfraud-api-node",

src/response/models/factors.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,12 @@ import * as webRecords from '../web-records';
44
import Insights from './insights';
55

66
export default class Factors extends Insights {
7+
/**
8+
* An array of risk score reason objects that describe a risk score
9+
* multiplier and the reasons for that multiplier.
10+
*/
11+
public readonly riskScoreReasons?: records.RiskScoreReason[];
12+
713
/**
814
* An object containing GeoIP2 and minFraud Insights information about the IP
915
* address.
@@ -13,6 +19,7 @@ export default class Factors extends Insights {
1319
public constructor(response: webRecords.FactorsResponse) {
1420
super(response);
1521

22+
this.riskScoreReasons = response.risk_score_reasons;
1623
this.subscores = camelizeResponse(response.subscores) as records.Subscores;
1724
}
1825
}

src/response/records.ts

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -361,6 +361,106 @@ export interface Disposition {
361361
readonly ruleLabel?: string;
362362
}
363363

364+
/**
365+
* The risk score reason for the multiplier.
366+
*
367+
* This class provides both a machine-readable code and a human-readable
368+
* explanation of the reason for the risk score. See
369+
* {@link https://dev.maxmind.com/minfraud/api-documentation/responses/#schema--response--risk-score-reason--multiplier-reason | the response API documentation}.
370+
*
371+
* Although more codes may be added in the future, the current codes are:
372+
*
373+
* * `BROWSER_LANGUAGE` - Riskiness of the browser user-agent and language
374+
* associated with the request.
375+
* * `BUSINESS_ACTIVITY` - Riskiness of business activity associated with the
376+
* request.
377+
* * `COUNTRY` - Riskiness of the country associated with the request.
378+
* * `CUSTOMER_ID` - Riskiness of a customer's activity.
379+
* * `EMAIL_DOMAIN` - Riskiness of email domain.
380+
* * `EMAIL_DOMAIN_NEW` - Riskiness of newly-sighted email domain.
381+
* * `EMAIL_ADDRESS_NEW` - Riskiness of newly-sighted email address.
382+
* * `EMAIL_LOCAL_PART` - Riskiness of the local part of the email address.
383+
* * `EMAIL_VELOCITY` - Velocity on email - many requests on same email over
384+
* short period of time.
385+
* * `ISSUER_ID_NUMBER_COUNTRY_MISMATCH` - Riskiness of the country mismatch
386+
* between IP, billing, shipping and IIN country.
387+
* * `ISSUER_ID_NUMBER_ON_SHOP_ID` - Risk of Issuer ID Number for the shop ID.
388+
* * `ISSUER_ID_NUMBER_LAST_DIGITS_ACTIVITY` - Riskiness of many recent
389+
* requests and previous high-risk requests on the IIN and last digits of the
390+
* credit card.
391+
* * `ISSUER_ID_NUMBER_SHOP_ID_VELOCITY` - Risk of recent Issuer ID Number
392+
* activity for the shop ID.
393+
* * `INTRACOUNTRY_DISTANCE` - Risk of distance between IP, billing, and
394+
* shipping location.
395+
* * `ANONYMOUS_IP` - Risk due to IP being an Anonymous IP.
396+
* * `IP_BILLING_POSTAL_VELOCITY` - Velocity of distinct billing postal code on
397+
* IP address.
398+
* * `IP_EMAIL_VELOCITY` - Velocity of distinct email address on IP address.
399+
* * `IP_HIGH_RISK_DEVICE` - High-risk device sighted on IP address.
400+
* * `IP_ISSUER_ID_NUMBER_VELOCITY` - Velocity of distinct IIN on IP address.
401+
* * `IP_ACTIVITY` - Riskiness of IP based on minFraud network activity.
402+
* * `LANGUAGE` - Riskiness of browser language.
403+
* * `MAX_RECENT_EMAIL` - Riskiness of email address based on past minFraud
404+
* risk scores on email.
405+
* * `MAX_RECENT_PHONE` - Riskiness of phone number based on past minFraud risk
406+
* scores on phone.
407+
* * `MAX_RECENT_SHIP` - Riskiness of email address based on past minFraud risk
408+
* scores on ship address.
409+
* * `MULTIPLE_CUSTOMER_ID_ON_EMAIL` - Riskiness of email address having many
410+
* customer IDs.
411+
* * `ORDER_AMOUNT` - Riskiness of the order amount.
412+
* * `ORG_DISTANCE_RISK` - Risk of ISP and distance between billing address and
413+
* IP location.
414+
* * `PHONE` - Riskiness of the phone number or related numbers.
415+
* * `CART` - Riskiness of shopping cart contents.
416+
* * `TIME_OF_DAY` - Risk due to local time of day.
417+
* * `TRANSACTION_REPORT_EMAIL` - Risk due to transaction reports on the email
418+
* address.
419+
* * `TRANSACTION_REPORT_IP` - Risk due to transaction reports on the IP
420+
* address.
421+
* * `TRANSACTION_REPORT_PHONE` - Risk due to transaction reports on the phone
422+
* number.
423+
* * `TRANSACTION_REPORT_SHIP` - Risk due to transaction reports on the
424+
* shipping address.
425+
* * `EMAIL_ACTIVITY` - Riskiness of the email address based on minFraud
426+
* network activity.
427+
* * `PHONE_ACTIVITY` - Riskiness of the phone number based on minFraud network
428+
* activity.
429+
* * `SHIP_ACTIVITY` - Riskiness of ship address based on minFraud network
430+
* activity.
431+
*/
432+
export interface Reason {
433+
/**
434+
* A machine-readable code identifying the reason.
435+
*/
436+
code: string;
437+
/**
438+
* A human-readable explanation of the reason. The description may change at
439+
* any time and should not be matched against.
440+
*/
441+
reason: string;
442+
}
443+
444+
/**
445+
* The object describing the risk score multiplier and the reasons for that
446+
* multiplier.
447+
*/
448+
export interface RiskScoreReason {
449+
/**
450+
* The factor by which the risk score is increased (if the value is greater
451+
* than 1) or decreased (if the value is less than 1) for given risk
452+
* reason(s).
453+
* Multipliers greater than 1.5 and less than 0.66 are considered significant
454+
* and lead to risk reason(s) being present.
455+
*/
456+
multiplier: number;
457+
/**
458+
* An array containing Reason objects that describe one of the reasons for
459+
* the multiplier.
460+
*/
461+
reasons: Reason[];
462+
}
463+
364464
/**
365465
* This object contains scores for many of the individual risk factors that
366466
* are used to calculate the overall risk score.

src/response/web-records.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,16 @@ export interface PhoneWebRecord {
121121
readonly number_type?: string;
122122
}
123123

124+
export interface ReasonWebRecord {
125+
code: string;
126+
reason: string;
127+
}
128+
129+
export interface RiskScoreReasonWebRecord {
130+
multiplier: number;
131+
reasons: ReasonWebRecord[];
132+
}
133+
124134
export interface SubscoresWebRecord {
125135
readonly avs_result?: number;
126136
readonly billing_address?: number;
@@ -172,5 +182,6 @@ export interface InsightsResponse extends ScoreResponse {
172182
}
173183

174184
export interface FactorsResponse extends InsightsResponse {
185+
readonly risk_score_reasons?: RiskScoreReasonWebRecord[];
175186
readonly subscores: SubscoresWebRecord;
176187
}

src/webServiceClient.spec.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import cloneDeep = require('lodash.clonedeep');
22
import nock from 'nock';
33
import * as models from './response/models';
44
import * as insights from '../fixtures/insights.json';
5+
import reasons from '../fixtures/reasons.json';
56
import * as score from '../fixtures/score.json';
67
import * as subscores from '../fixtures/subscores.json';
78
import {
@@ -26,6 +27,7 @@ const client = new Client(auth.user, auth.pass);
2627
describe('WebServiceClient', () => {
2728
// eslint-disable-next-line @typescript-eslint/no-explicit-any
2829
const factors = cloneDeep(insights) as any;
30+
factors.response.full.risk_score_reasons = cloneDeep(reasons);
2931
factors.response.full.subscores = cloneDeep(subscores);
3032

3133
describe('factors()', () => {
@@ -36,7 +38,7 @@ describe('WebServiceClient', () => {
3638
});
3739

3840
it('handles "full" responses', async () => {
39-
expect.assertions(167);
41+
expect.assertions(172);
4042

4143
nockInstance
4244
.post(fullPath('factors'), factors.request.basic)
@@ -251,6 +253,14 @@ describe('WebServiceClient', () => {
251253
);
252254
expect(got.warnings?.[0].inputPointer).toEqual('/shipping/city');
253255

256+
expect(got.riskScoreReasons).toHaveLength(4);
257+
expect(got.riskScoreReasons?.[0].multiplier).toEqual(45);
258+
expect(got.riskScoreReasons?.[0].reasons).toHaveLength(1);
259+
expect(got.riskScoreReasons?.[0].reasons[0].code).toEqual('ANONYMOUS_IP');
260+
expect(got.riskScoreReasons?.[0].reasons[0].reason).toEqual(
261+
'Risk due to IP being an Anonymous IP'
262+
);
263+
254264
expect(got?.subscores?.avsResult).toEqual(0.01);
255265
expect(got?.subscores?.billingAddress).toEqual(0.02);
256266
expect(got?.subscores?.billingAddressDistanceToIpLocation).toEqual(0.03);

0 commit comments

Comments
 (0)