Skip to content

Commit 7ca7302

Browse files
committed
Add risk score reasons
1 parent 13c4dc5 commit 7ca7302

File tree

8 files changed

+330
-0
lines changed

8 files changed

+330
-0
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+
3.2.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
3.1.0 (2024-07-08)
513
------------------
614

src/MinFraud/Model/Factors.php

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,16 @@
99
*/
1010
class Factors extends Insights
1111
{
12+
/**
13+
* @var array<RiskScoreReason> This array contains \MaxMind\MinFraud\Model\RiskScoreReason
14+
* objects that describe risk score reasons for a given transaction
15+
* that change the risk score significantly. Risk score reasons are
16+
* usually only returned for medium to high risk transactions.
17+
* If there were no significant changes to the risk score due to
18+
* these reasons, then this array will be empty.
19+
*/
20+
public readonly array $riskScoreReasons;
21+
1222
/**
1323
* @var Subscores an object containing scores for many of the individual
1424
* risk factors that are used to calculate the overall risk
@@ -20,6 +30,14 @@ public function __construct(array $response, array $locales = ['en'])
2030
{
2131
parent::__construct($response, $locales);
2232

33+
$riskScoreReasons = [];
34+
if (isset($response['risk_score_reasons'])) {
35+
foreach ($response['risk_score_reasons'] as $reason) {
36+
$riskScoreReasons[] = new RiskScoreReason($reason);
37+
}
38+
}
39+
$this->riskScoreReasons = $riskScoreReasons;
40+
2341
$this->subscores
2442
= new Subscores($response['subscores'] ?? []);
2543
}
@@ -28,6 +46,14 @@ public function jsonSerialize(): array
2846
{
2947
$js = parent::jsonSerialize();
3048

49+
if (!empty($this->riskScoreReasons)) {
50+
$riskScoreReasons = [];
51+
foreach ($this->riskScoreReasons as $reason) {
52+
$riskScoreReasons[] = $reason->jsonSerialize();
53+
}
54+
$js['risk_score_reasons'] = $riskScoreReasons;
55+
}
56+
3157
$subscores = $this->subscores->jsonSerialize();
3258
if (!empty($subscores)) {
3359
$js['subscores'] = $subscores;

src/MinFraud/Model/Reason.php

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace MaxMind\MinFraud\Model;
6+
7+
/**
8+
* The risk score reason for the multiplier.
9+
*
10+
* This class provides both a machine-readable code and a human-readable
11+
* explanation of the reason for the risk score, see
12+
* {@link https://dev.maxmind.com/minfraud/api-documentation/responses/#schema--response--risk-score-reason--multiplier-reason}.
13+
*
14+
* Although more codes may be added in the future, the current codes are:
15+
*
16+
* * `BROWSER_LANGUAGE` - Riskiness of the browser user-agent and language associated with the request.
17+
* * `BUSINESS_ACTIVITY` - Riskiness of business activity associated with the request.
18+
* * `COUNTRY` - Riskiness of the country associated with the request.
19+
* * `CUSTOMER_ID` - Riskiness of a customer's activity.
20+
* * `EMAIL_DOMAIN` - Riskiness of email domain.
21+
* * `EMAIL_DOMAIN_NEW` - Riskiness of newly-sighted email domain.
22+
* * `EMAIL_ADDRESS_NEW` - Riskiness of newly-sighted email address.
23+
* * `EMAIL_LOCAL_PART` - Riskiness of the local part of the email address.
24+
* * `EMAIL_VELOCITY` - Velocity on email - many requests on same email over short period of time.
25+
* * `ISSUER_ID_NUMBER_COUNTRY_MISMATCH` - Riskiness of the country mismatch between IP, billing,
26+
* shipping and IIN country.
27+
* * `ISSUER_ID_NUMBER_ON_SHOP_ID` - Risk of Issuer ID Number for the shop ID.
28+
* * `ISSUER_ID_NUMBER_LAST_DIGITS_ACTIVITY` - Riskiness of many recent requests and previous
29+
* high-risk requests on the IIN and last digits of the credit card.
30+
* * `ISSUER_ID_NUMBER_SHOP_ID_VELOCITY` - Risk of recent Issuer ID Number activity for the shop ID.
31+
* * `INTRACOUNTRY_DISTANCE` - Risk of distance between IP, billing, and shipping location.
32+
* * `ANONYMOUS_IP` - Risk due to IP being an Anonymous IP.
33+
* * `IP_BILLING_POSTAL_VELOCITY` - Velocity of distinct billing postal code on IP address.
34+
* * `IP_EMAIL_VELOCITY` - Velocity of distinct email address on IP address.
35+
* * `IP_HIGH_RISK_DEVICE` - High-risk device sighted on IP address.
36+
* * `IP_ISSUER_ID_NUMBER_VELOCITY` - Velocity of distinct IIN on IP address.
37+
* * `IP_ACTIVITY` - Riskiness of IP based on minFraud network activity.
38+
* * `LANGUAGE` - Riskiness of browser language.
39+
* * `MAX_RECENT_EMAIL` - Riskiness of email address based on past minFraud risk scores on email.
40+
* * `MAX_RECENT_PHONE` - Riskiness of phone number based on past minFraud risk scores on phone.
41+
* * `MAX_RECENT_SHIP` - Riskiness of email address based on past minFraud risk scores on ship address.
42+
* * `MULTIPLE_CUSTOMER_ID_ON_EMAIL` - Riskiness of email address having many customer IDs.
43+
* * `ORDER_AMOUNT` - Riskiness of the order amount.
44+
* * `ORG_DISTANCE_RISK` - Risk of ISP and distance between billing address and IP location.
45+
* * `PHONE` - Riskiness of the phone number or related numbers.
46+
* * `CART` - Riskiness of shopping cart contents.
47+
* * `TIME_OF_DAY` - Risk due to local time of day.
48+
* * `TRANSACTION_REPORT_EMAIL` - Risk due to transaction reports on the email address.
49+
* * `TRANSACTION_REPORT_IP` - Risk due to transaction reports on the IP address.
50+
* * `TRANSACTION_REPORT_PHONE` - Risk due to transaction reports on the phone number.
51+
* * `TRANSACTION_REPORT_SHIP` - Risk due to transaction reports on the shipping address.
52+
* * `EMAIL_ACTIVITY` - Riskiness of the email address based on minFraud network activity.
53+
* * `PHONE_ACTIVITY` - Riskiness of the phone number based on minFraud network activity.
54+
* * `SHIP_ACTIVITY` - Riskiness of ship address based on minFraud network activity.
55+
*/
56+
class Reason implements \JsonSerializable
57+
{
58+
/**
59+
* @var string This value is a machine-readable code identifying the reason
60+
*/
61+
public readonly string $code;
62+
63+
/**
64+
* @var string This value provides a human-readable explanation of the reason. The description
65+
* may change at any time and should not be matched against.
66+
*/
67+
public readonly string $reason;
68+
69+
public function __construct(array $response)
70+
{
71+
$this->code = $response['code'];
72+
$this->reason = $response['reason'];
73+
}
74+
75+
public function jsonSerialize(): array
76+
{
77+
$js = [];
78+
79+
$js['code'] = $this->code;
80+
$js['reason'] = $this->reason;
81+
82+
return $js;
83+
}
84+
}
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace MaxMind\MinFraud\Model;
6+
7+
/**
8+
* The risk score multiplier and the reasons for that multiplier.
9+
*/
10+
class RiskScoreReason implements \JsonSerializable
11+
{
12+
/**
13+
* @var float|null The factor by which the risk score is increased (if the value is greater than 1)
14+
* or decreased (if the value is less than 1) for given risk reason(s).
15+
* Multipliers greater than 1.5 and less than 0.66 are considered significant
16+
* and lead to risk reason(s) being present.
17+
*/
18+
public readonly ?float $multiplier;
19+
20+
/**
21+
* @var array<Reason> This array contains \MaxMind\MinFraud\Model\Reason objects that describe
22+
* one of the reasons for the multiplier
23+
*/
24+
public readonly array $reasons;
25+
26+
public function __construct(?array $response)
27+
{
28+
if ($response === null) {
29+
$response = [];
30+
}
31+
32+
$this->multiplier = $response['multiplier'] ?? null;
33+
34+
$reasons = [];
35+
if (isset($response['reasons'])) {
36+
foreach ($response['reasons'] as $reason) {
37+
$reasons[] = new Reason($reason);
38+
}
39+
}
40+
$this->reasons = $reasons;
41+
}
42+
43+
public function jsonSerialize(): ?array
44+
{
45+
$js = [];
46+
47+
if ($this->multiplier !== null) {
48+
$js['multiplier'] = $this->multiplier;
49+
}
50+
51+
if (!empty($this->reasons)) {
52+
$reasons = [];
53+
foreach ($this->reasons as $reason) {
54+
$reasons[] = $reason->jsonSerialize();
55+
}
56+
$js['reasons'] = $reasons;
57+
}
58+
59+
return $js;
60+
}
61+
}

tests/MaxMind/Test/MinFraud/Model/FactorsTest.php

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,5 +55,20 @@ public function testFactorsProperties(): void
5555
$key
5656
);
5757
}
58+
59+
$this->assertCount(4, $factors->riskScoreReasons);
60+
$this->assertSame(
61+
$array['risk_score_reasons'][0]['multiplier'],
62+
$factors->riskScoreReasons[0]->multiplier,
63+
);
64+
$this->assertCount(1, $factors->riskScoreReasons[0]->reasons);
65+
$this->assertSame(
66+
$array['risk_score_reasons'][0]['reasons'][0]['code'],
67+
$factors->riskScoreReasons[0]->reasons[0]->code,
68+
);
69+
$this->assertSame(
70+
$array['risk_score_reasons'][0]['reasons'][0]['reason'],
71+
$factors->riskScoreReasons[0]->reasons[0]->reason,
72+
);
5873
}
5974
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace MaxMind\Test\MinFraud\Model;
6+
7+
use MaxMind\MinFraud\Model\Reason;
8+
use PHPUnit\Framework\TestCase;
9+
10+
/**
11+
* @coversNothing
12+
*
13+
* @internal
14+
*/
15+
class ReasonTest extends TestCase
16+
{
17+
public function testReason(): void
18+
{
19+
$array = [
20+
'code' => 'ANONYMOUS_IP',
21+
'reason' => 'Risk due to IP being an Anonymous IP',
22+
];
23+
$reason = new Reason($array);
24+
25+
$this->assertSame(
26+
$array['code'],
27+
$reason->code,
28+
'code'
29+
);
30+
31+
$this->assertSame(
32+
$array['reason'],
33+
$reason->reason,
34+
'reason'
35+
);
36+
37+
$this->assertSame(
38+
$array,
39+
$reason->jsonSerialize(),
40+
'correctly implements JsonSerializable'
41+
);
42+
}
43+
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace MaxMind\Test\MinFraud\Model;
6+
7+
use MaxMind\MinFraud\Model\RiskScoreReason;
8+
use PHPUnit\Framework\TestCase;
9+
10+
/**
11+
* @coversNothing
12+
*
13+
* @internal
14+
*/
15+
class RiskScoreReasonTest extends TestCase
16+
{
17+
public function testRiskScoreReason(): void
18+
{
19+
$array = [
20+
'multiplier' => 45.0,
21+
'reasons' => [
22+
[
23+
'code' => 'ANONYMOUS_IP',
24+
'reason' => 'Risk due to IP being an Anonymous IP',
25+
],
26+
],
27+
];
28+
29+
$reason = new RiskScoreReason($array);
30+
31+
$this->assertSame(
32+
$array['multiplier'],
33+
$reason->multiplier,
34+
'multiplier'
35+
);
36+
37+
$this->assertSame(
38+
\count($array['reasons']),
39+
\count($reason->reasons),
40+
'correct number of reasons'
41+
);
42+
43+
$this->assertSame(
44+
$array['reasons'][0]['code'],
45+
$reason->reasons[0]->code,
46+
'correct code'
47+
);
48+
49+
$this->assertSame(
50+
$array['reasons'][0]['reason'],
51+
$reason->reasons[0]->reason,
52+
'correct reason'
53+
);
54+
}
55+
}

tests/data/minfraud/factors-response.json

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,5 +200,43 @@
200200
"input_pointer": "/account/username_md5",
201201
"warning": "Encountered value at /account/username_md5 that does meet the required constraints"
202202
}
203+
],
204+
"risk_score_reasons": [
205+
{
206+
"multiplier": 45.0,
207+
"reasons": [
208+
{
209+
"code": "ANONYMOUS_IP",
210+
"reason": "Risk due to IP being an Anonymous IP"
211+
}
212+
]
213+
},
214+
{
215+
"multiplier": 1.8,
216+
"reasons": [
217+
{
218+
"code": "TIME_OF_DAY",
219+
"reason": "Risk due to local time of day"
220+
}
221+
]
222+
},
223+
{
224+
"multiplier": 1.6,
225+
"reasons": [
226+
{
227+
"reason": "Riskiness of newly-sighted email domain",
228+
"code": "EMAIL_DOMAIN_NEW"
229+
}
230+
]
231+
},
232+
{
233+
"multiplier": 0.34,
234+
"reasons": [
235+
{
236+
"code": "EMAIL_ADDRESS_NEW",
237+
"reason": "Riskiness of newly-sighted email address"
238+
}
239+
]
240+
}
203241
]
204242
}

0 commit comments

Comments
 (0)