diff --git a/CHANGELOG.md b/CHANGELOG.md index 8487e091..6ebb7b33 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,14 @@ CHANGELOG ========= +7.1.0-beta.1 +------------ + +* Added support for the new risk reasons outputs in minFraud Factors. The risk + reasons output codes and reasons are currently in beta and are subject to + change. We recommend that you use these beta outputs with caution and avoid + relying on them for critical applications. + 7.0.0 (2024-07-08) ------------------ diff --git a/e2e/js/package-lock.json b/e2e/js/package-lock.json index b3d5adf5..4617a8a1 100644 --- a/e2e/js/package-lock.json +++ b/e2e/js/package-lock.json @@ -1601,9 +1601,7 @@ "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", "hasInstallScript": true, "optional": true, - "os": [ - "darwin" - ], + "os": ["darwin"], "engines": { "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } diff --git a/e2e/ts/package-lock.json b/e2e/ts/package-lock.json index 6cf1f873..136501b8 100644 --- a/e2e/ts/package-lock.json +++ b/e2e/ts/package-lock.json @@ -1682,9 +1682,7 @@ "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", "hasInstallScript": true, "optional": true, - "os": [ - "darwin" - ], + "os": ["darwin"], "engines": { "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } diff --git a/fixtures/reasons.json b/fixtures/reasons.json new file mode 100644 index 00000000..17b975bc --- /dev/null +++ b/fixtures/reasons.json @@ -0,0 +1,38 @@ +[ + { + "multiplier": 45, + "reasons": [ + { + "code": "ANONYMOUS_IP", + "reason": "Risk due to IP being an Anonymous IP" + } + ] + }, + { + "multiplier": 1.8, + "reasons": [ + { + "code": "TIME_OF_DAY", + "reason": "Risk due to local time of day" + } + ] + }, + { + "multiplier": 1.6, + "reasons": [ + { + "reason": "Riskiness of newly-sighted email domain", + "code": "EMAIL_DOMAIN_NEW" + } + ] + }, + { + "multiplier": 0.34, + "reasons": [ + { + "code": "EMAIL_ADDRESS_NEW", + "reason": "Riskiness of newly-sighted email address" + } + ] + } +] diff --git a/package-lock.json b/package-lock.json index 4e3527f1..ef6b9a09 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@maxmind/minfraud-api-node", - "version": "7.0.0", + "version": "7.1.0-beta.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@maxmind/minfraud-api-node", - "version": "7.0.0", + "version": "7.1.0-beta.1", "license": "Apache-2.0", "dependencies": { "@maxmind/geoip2-node": "^5.0.0", diff --git a/package.json b/package.json index debb5d2d..048e1d2a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@maxmind/minfraud-api-node", - "version": "7.0.0", + "version": "7.1.0-beta.1", "description": "Node.js API for MaxMind minFraud Score, Insights, and Factors web services", "main": "dist/src/index.js", "homepage": "https://github.com/maxmind/minfraud-api-node", diff --git a/src/response/models/factors.ts b/src/response/models/factors.ts index a07f340b..e629853f 100644 --- a/src/response/models/factors.ts +++ b/src/response/models/factors.ts @@ -4,6 +4,12 @@ import * as webRecords from '../web-records'; import Insights from './insights'; export default class Factors extends Insights { + /** + * An array of risk score reason objects that describe a risk score + * multiplier and the reasons for that multiplier. + */ + public readonly riskScoreReasons?: records.RiskScoreReason[]; + /** * An object containing GeoIP2 and minFraud Insights information about the IP * address. @@ -13,6 +19,7 @@ export default class Factors extends Insights { public constructor(response: webRecords.FactorsResponse) { super(response); + this.riskScoreReasons = response.risk_score_reasons; this.subscores = camelizeResponse(response.subscores) as records.Subscores; } } diff --git a/src/response/records.ts b/src/response/records.ts index 8a2f1f59..0773f377 100644 --- a/src/response/records.ts +++ b/src/response/records.ts @@ -361,6 +361,106 @@ export interface Disposition { readonly ruleLabel?: string; } +/** + * The risk score reason for the multiplier. + * + * This class provides both a machine-readable code and a human-readable + * explanation of the reason for the risk score. See + * {@link https://dev.maxmind.com/minfraud/api-documentation/responses/#schema--response--risk-score-reason--multiplier-reason | the response API documentation}. + * + * Although more codes may be added in the future, the current codes are: + * + * * `BROWSER_LANGUAGE` - Riskiness of the browser user-agent and language + * associated with the request. + * * `BUSINESS_ACTIVITY` - Riskiness of business activity associated with the + * request. + * * `COUNTRY` - Riskiness of the country associated with the request. + * * `CUSTOMER_ID` - Riskiness of a customer's activity. + * * `EMAIL_DOMAIN` - Riskiness of email domain. + * * `EMAIL_DOMAIN_NEW` - Riskiness of newly-sighted email domain. + * * `EMAIL_ADDRESS_NEW` - Riskiness of newly-sighted email address. + * * `EMAIL_LOCAL_PART` - Riskiness of the local part of the email address. + * * `EMAIL_VELOCITY` - Velocity on email - many requests on same email over + * short period of time. + * * `ISSUER_ID_NUMBER_COUNTRY_MISMATCH` - Riskiness of the country mismatch + * between IP, billing, shipping and IIN country. + * * `ISSUER_ID_NUMBER_ON_SHOP_ID` - Risk of Issuer ID Number for the shop ID. + * * `ISSUER_ID_NUMBER_LAST_DIGITS_ACTIVITY` - Riskiness of many recent + * requests and previous high-risk requests on the IIN and last digits of the + * credit card. + * * `ISSUER_ID_NUMBER_SHOP_ID_VELOCITY` - Risk of recent Issuer ID Number + * activity for the shop ID. + * * `INTRACOUNTRY_DISTANCE` - Risk of distance between IP, billing, and + * shipping location. + * * `ANONYMOUS_IP` - Risk due to IP being an Anonymous IP. + * * `IP_BILLING_POSTAL_VELOCITY` - Velocity of distinct billing postal code on + * IP address. + * * `IP_EMAIL_VELOCITY` - Velocity of distinct email address on IP address. + * * `IP_HIGH_RISK_DEVICE` - High-risk device sighted on IP address. + * * `IP_ISSUER_ID_NUMBER_VELOCITY` - Velocity of distinct IIN on IP address. + * * `IP_ACTIVITY` - Riskiness of IP based on minFraud network activity. + * * `LANGUAGE` - Riskiness of browser language. + * * `MAX_RECENT_EMAIL` - Riskiness of email address based on past minFraud + * risk scores on email. + * * `MAX_RECENT_PHONE` - Riskiness of phone number based on past minFraud risk + * scores on phone. + * * `MAX_RECENT_SHIP` - Riskiness of email address based on past minFraud risk + * scores on ship address. + * * `MULTIPLE_CUSTOMER_ID_ON_EMAIL` - Riskiness of email address having many + * customer IDs. + * * `ORDER_AMOUNT` - Riskiness of the order amount. + * * `ORG_DISTANCE_RISK` - Risk of ISP and distance between billing address and + * IP location. + * * `PHONE` - Riskiness of the phone number or related numbers. + * * `CART` - Riskiness of shopping cart contents. + * * `TIME_OF_DAY` - Risk due to local time of day. + * * `TRANSACTION_REPORT_EMAIL` - Risk due to transaction reports on the email + * address. + * * `TRANSACTION_REPORT_IP` - Risk due to transaction reports on the IP + * address. + * * `TRANSACTION_REPORT_PHONE` - Risk due to transaction reports on the phone + * number. + * * `TRANSACTION_REPORT_SHIP` - Risk due to transaction reports on the + * shipping address. + * * `EMAIL_ACTIVITY` - Riskiness of the email address based on minFraud + * network activity. + * * `PHONE_ACTIVITY` - Riskiness of the phone number based on minFraud network + * activity. + * * `SHIP_ACTIVITY` - Riskiness of ship address based on minFraud network + * activity. + */ +export interface Reason { + /** + * A machine-readable code identifying the reason. + */ + code: string; + /** + * A human-readable explanation of the reason. The description may change at + * any time and should not be matched against. + */ + reason: string; +} + +/** + * The object describing the risk score multiplier and the reasons for that + * multiplier. + */ +export interface RiskScoreReason { + /** + * The factor by which the risk score is increased (if the value is greater + * than 1) or decreased (if the value is less than 1) for given risk + * reason(s). + * Multipliers greater than 1.5 and less than 0.66 are considered significant + * and lead to risk reason(s) being present. + */ + multiplier: number; + /** + * An array containing Reason objects that describe one of the reasons for + * the multiplier. + */ + reasons: Reason[]; +} + /** * This object contains scores for many of the individual risk factors that * are used to calculate the overall risk score. diff --git a/src/response/web-records.ts b/src/response/web-records.ts index 3c914dc7..845ec111 100644 --- a/src/response/web-records.ts +++ b/src/response/web-records.ts @@ -121,6 +121,16 @@ export interface PhoneWebRecord { readonly number_type?: string; } +export interface ReasonWebRecord { + code: string; + reason: string; +} + +export interface RiskScoreReasonWebRecord { + multiplier: number; + reasons: ReasonWebRecord[]; +} + export interface SubscoresWebRecord { readonly avs_result?: number; readonly billing_address?: number; @@ -172,5 +182,6 @@ export interface InsightsResponse extends ScoreResponse { } export interface FactorsResponse extends InsightsResponse { + readonly risk_score_reasons?: RiskScoreReasonWebRecord[]; readonly subscores: SubscoresWebRecord; } diff --git a/src/webServiceClient.spec.ts b/src/webServiceClient.spec.ts index 24ff6bfa..f280d936 100644 --- a/src/webServiceClient.spec.ts +++ b/src/webServiceClient.spec.ts @@ -2,6 +2,7 @@ import cloneDeep = require('lodash.clonedeep'); import nock from 'nock'; import * as models from './response/models'; import * as insights from '../fixtures/insights.json'; +import reasons from '../fixtures/reasons.json'; import * as score from '../fixtures/score.json'; import * as subscores from '../fixtures/subscores.json'; import { @@ -26,6 +27,7 @@ const client = new Client(auth.user, auth.pass); describe('WebServiceClient', () => { // eslint-disable-next-line @typescript-eslint/no-explicit-any const factors = cloneDeep(insights) as any; + factors.response.full.risk_score_reasons = cloneDeep(reasons); factors.response.full.subscores = cloneDeep(subscores); describe('factors()', () => { @@ -36,7 +38,7 @@ describe('WebServiceClient', () => { }); it('handles "full" responses', async () => { - expect.assertions(167); + expect.assertions(172); nockInstance .post(fullPath('factors'), factors.request.basic) @@ -251,6 +253,14 @@ describe('WebServiceClient', () => { ); expect(got.warnings?.[0].inputPointer).toEqual('/shipping/city'); + expect(got.riskScoreReasons).toHaveLength(4); + expect(got.riskScoreReasons?.[0].multiplier).toEqual(45); + expect(got.riskScoreReasons?.[0].reasons).toHaveLength(1); + expect(got.riskScoreReasons?.[0].reasons[0].code).toEqual('ANONYMOUS_IP'); + expect(got.riskScoreReasons?.[0].reasons[0].reason).toEqual( + 'Risk due to IP being an Anonymous IP' + ); + expect(got?.subscores?.avsResult).toEqual(0.01); expect(got?.subscores?.billingAddress).toEqual(0.02); expect(got?.subscores?.billingAddressDistanceToIpLocation).toEqual(0.03);