From 7556bb3345b60cd44ee2cd9d2d2eac970d45e00e Mon Sep 17 00:00:00 2001 From: Marsel Mavletkulov Date: Thu, 29 Aug 2024 17:08:17 -0400 Subject: [PATCH] Add risk score reasons --- CHANGELOG.md | 8 ++ src/MinFraud/Model/Factors.php | 26 +++++ src/MinFraud/Model/Reason.php | 104 ++++++++++++++++++ src/MinFraud/Model/RiskScoreReason.php | 61 ++++++++++ .../Test/MinFraud/Model/FactorsTest.php | 15 +++ .../Test/MinFraud/Model/ReasonTest.php | 43 ++++++++ .../MinFraud/Model/RiskScoreReasonTest.php | 55 +++++++++ tests/data/minfraud/factors-response.json | 38 +++++++ 8 files changed, 350 insertions(+) create mode 100644 src/MinFraud/Model/Reason.php create mode 100644 src/MinFraud/Model/RiskScoreReason.php create mode 100644 tests/MaxMind/Test/MinFraud/Model/ReasonTest.php create mode 100644 tests/MaxMind/Test/MinFraud/Model/RiskScoreReasonTest.php diff --git a/CHANGELOG.md b/CHANGELOG.md index 598b575..1973860 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,14 @@ CHANGELOG ========= +3.2.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. + 3.1.0 (2024-07-08) ------------------ diff --git a/src/MinFraud/Model/Factors.php b/src/MinFraud/Model/Factors.php index b297ffd..6a0ebef 100644 --- a/src/MinFraud/Model/Factors.php +++ b/src/MinFraud/Model/Factors.php @@ -9,6 +9,16 @@ */ class Factors extends Insights { + /** + * @var array This array contains + * \MaxMind\MinFraud\Model\RiskScoreReason objects that describe + * risk score reasons for a given transaction that change the risk + * score significantly. Risk score reasons are usually only returned + * for medium to high risk transactions. If there were no significant + * changes to the risk score due to these reasons, then this array will be empty. + */ + public readonly array $riskScoreReasons; + /** * @var Subscores an object containing scores for many of the individual * risk factors that are used to calculate the overall risk @@ -20,6 +30,14 @@ public function __construct(array $response, array $locales = ['en']) { parent::__construct($response, $locales); + $riskScoreReasons = []; + if (isset($response['risk_score_reasons'])) { + foreach ($response['risk_score_reasons'] as $reason) { + $riskScoreReasons[] = new RiskScoreReason($reason); + } + } + $this->riskScoreReasons = $riskScoreReasons; + $this->subscores = new Subscores($response['subscores'] ?? []); } @@ -28,6 +46,14 @@ public function jsonSerialize(): array { $js = parent::jsonSerialize(); + if (!empty($this->riskScoreReasons)) { + $riskScoreReasons = []; + foreach ($this->riskScoreReasons as $reason) { + $riskScoreReasons[] = $reason->jsonSerialize(); + } + $js['risk_score_reasons'] = $riskScoreReasons; + } + $subscores = $this->subscores->jsonSerialize(); if (!empty($subscores)) { $js['subscores'] = $subscores; diff --git a/src/MinFraud/Model/Reason.php b/src/MinFraud/Model/Reason.php new file mode 100644 index 0000000..61442a2 --- /dev/null +++ b/src/MinFraud/Model/Reason.php @@ -0,0 +1,104 @@ +code = $response['code'] ?? null; + $this->reason = $response['reason'] ?? null; + } + + public function jsonSerialize(): array + { + $js = []; + + if ($this->code !== null) { + $js['code'] = $this->code; + } + if ($this->reason !== null) { + $js['reason'] = $this->reason; + } + + return $js; + } +} diff --git a/src/MinFraud/Model/RiskScoreReason.php b/src/MinFraud/Model/RiskScoreReason.php new file mode 100644 index 0000000..7dcf001 --- /dev/null +++ b/src/MinFraud/Model/RiskScoreReason.php @@ -0,0 +1,61 @@ + This array contains \MaxMind\MinFraud\Model\Reason objects + * that describe one of the reasons for the multiplier + */ + public readonly array $reasons; + + public function __construct(?array $response) + { + if ($response === null) { + $response = []; + } + + $this->multiplier = $response['multiplier'] ?? null; + + $reasons = []; + if (isset($response['reasons'])) { + foreach ($response['reasons'] as $reason) { + $reasons[] = new Reason($reason); + } + } + $this->reasons = $reasons; + } + + public function jsonSerialize(): ?array + { + $js = []; + + if ($this->multiplier !== null) { + $js['multiplier'] = $this->multiplier; + } + + if (!empty($this->reasons)) { + $reasons = []; + foreach ($this->reasons as $reason) { + $reasons[] = $reason->jsonSerialize(); + } + $js['reasons'] = $reasons; + } + + return $js; + } +} diff --git a/tests/MaxMind/Test/MinFraud/Model/FactorsTest.php b/tests/MaxMind/Test/MinFraud/Model/FactorsTest.php index dc133d6..c5f0619 100644 --- a/tests/MaxMind/Test/MinFraud/Model/FactorsTest.php +++ b/tests/MaxMind/Test/MinFraud/Model/FactorsTest.php @@ -55,5 +55,20 @@ public function testFactorsProperties(): void $key ); } + + $this->assertCount(4, $factors->riskScoreReasons); + $this->assertSame( + $array['risk_score_reasons'][0]['multiplier'], + $factors->riskScoreReasons[0]->multiplier, + ); + $this->assertCount(1, $factors->riskScoreReasons[0]->reasons); + $this->assertSame( + $array['risk_score_reasons'][0]['reasons'][0]['code'], + $factors->riskScoreReasons[0]->reasons[0]->code, + ); + $this->assertSame( + $array['risk_score_reasons'][0]['reasons'][0]['reason'], + $factors->riskScoreReasons[0]->reasons[0]->reason, + ); } } diff --git a/tests/MaxMind/Test/MinFraud/Model/ReasonTest.php b/tests/MaxMind/Test/MinFraud/Model/ReasonTest.php new file mode 100644 index 0000000..6bf3b4d --- /dev/null +++ b/tests/MaxMind/Test/MinFraud/Model/ReasonTest.php @@ -0,0 +1,43 @@ + 'ANONYMOUS_IP', + 'reason' => 'Risk due to IP being an Anonymous IP', + ]; + $reason = new Reason($array); + + $this->assertSame( + $array['code'], + $reason->code, + 'code' + ); + + $this->assertSame( + $array['reason'], + $reason->reason, + 'reason' + ); + + $this->assertSame( + $array, + $reason->jsonSerialize(), + 'correctly implements JsonSerializable' + ); + } +} diff --git a/tests/MaxMind/Test/MinFraud/Model/RiskScoreReasonTest.php b/tests/MaxMind/Test/MinFraud/Model/RiskScoreReasonTest.php new file mode 100644 index 0000000..5561999 --- /dev/null +++ b/tests/MaxMind/Test/MinFraud/Model/RiskScoreReasonTest.php @@ -0,0 +1,55 @@ + 45.0, + 'reasons' => [ + [ + 'code' => 'ANONYMOUS_IP', + 'reason' => 'Risk due to IP being an Anonymous IP', + ], + ], + ]; + + $reason = new RiskScoreReason($array); + + $this->assertSame( + $array['multiplier'], + $reason->multiplier, + 'multiplier' + ); + + $this->assertSame( + \count($array['reasons']), + \count($reason->reasons), + 'correct number of reasons' + ); + + $this->assertSame( + $array['reasons'][0]['code'], + $reason->reasons[0]->code, + 'correct code' + ); + + $this->assertSame( + $array['reasons'][0]['reason'], + $reason->reasons[0]->reason, + 'correct reason' + ); + } +} diff --git a/tests/data/minfraud/factors-response.json b/tests/data/minfraud/factors-response.json index 71e15c3..f6740eb 100644 --- a/tests/data/minfraud/factors-response.json +++ b/tests/data/minfraud/factors-response.json @@ -200,5 +200,43 @@ "input_pointer": "/account/username_md5", "warning": "Encountered value at /account/username_md5 that does meet the required constraints" } + ], + "risk_score_reasons": [ + { + "multiplier": 45.0, + "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" + } + ] + } ] }