From 80a2af53f7c57b3e0c06ecb60944c32797411cae Mon Sep 17 00:00:00 2001 From: Marsel Mavletkulov Date: Tue, 27 Aug 2024 13:49:34 -0400 Subject: [PATCH] Add risk score reasons --- CHANGELOG.md | 7 ++ .../minfraud/response/FactorsResponse.java | 48 ++++++++- .../com/maxmind/minfraud/response/Reason.java | 98 +++++++++++++++++++ .../minfraud/response/RiskScoreReason.java | 51 ++++++++++ .../response/FactorsResponseTest.java | 26 +++++ .../maxmind/minfraud/response/ReasonTest.java | 30 ++++++ .../response/RiskScoreReasonTest.java | 51 ++++++++++ .../resources/test-data/factors-response.json | 38 +++++++ 8 files changed, 348 insertions(+), 1 deletion(-) create mode 100644 src/main/java/com/maxmind/minfraud/response/Reason.java create mode 100644 src/main/java/com/maxmind/minfraud/response/RiskScoreReason.java create mode 100644 src/test/java/com/maxmind/minfraud/response/ReasonTest.java create mode 100644 src/test/java/com/maxmind/minfraud/response/RiskScoreReasonTest.java diff --git a/CHANGELOG.md b/CHANGELOG.md index b6987e77..02680971 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,13 @@ CHANGELOG ========= +3.6.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.5.0 (2024-07-08) ------------------ diff --git a/src/main/java/com/maxmind/minfraud/response/FactorsResponse.java b/src/main/java/com/maxmind/minfraud/response/FactorsResponse.java index 94f3ff8c..303407f6 100644 --- a/src/main/java/com/maxmind/minfraud/response/FactorsResponse.java +++ b/src/main/java/com/maxmind/minfraud/response/FactorsResponse.java @@ -9,6 +9,7 @@ */ public final class FactorsResponse extends InsightsResponse { + private final List riskScoreReasons; private final Subscores subscores; @@ -26,6 +27,11 @@ public final class FactorsResponse extends InsightsResponse { * @param queriesRemaining The number of queries remaining. * @param riskScore The risk score. * @param shippingAddress The {@code ShippingAddress} model object. + * @param riskScoreReasons A list containing 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 list will be empty. * @param subscores The {@code Subscores} model object. * @param warnings A list containing warning objects. */ @@ -43,12 +49,14 @@ public FactorsResponse( @JsonProperty("risk_score") Double riskScore, @JsonProperty("shipping_address") ShippingAddress shippingAddress, @JsonProperty("shipping_phone") Phone shippingPhone, + @JsonProperty("risk_score_reasons") List riskScoreReasons, @JsonProperty("subscores") Subscores subscores, @JsonProperty("warnings") List warnings ) { super(billingAddress, billingPhone, creditCard, device, disposition, email, fundsRemaining, id, ipAddress, queriesRemaining, riskScore, shippingAddress, shippingPhone, warnings); + this.riskScoreReasons = riskScoreReasons; this.subscores = subscores; } @@ -74,7 +82,45 @@ public FactorsResponse( List warnings ) { this(billingAddress, null, creditCard, device, disposition, email, fundsRemaining, id, - ipAddress, queriesRemaining, riskScore, shippingAddress, null, subscores, warnings); + ipAddress, queriesRemaining, riskScore, shippingAddress, null, null, subscores, + warnings); + } + + /** + * Constructor for backwards compatibility. This will be removed in the next + * major release. + * + * @deprecated use other constructor. + */ + @Deprecated + public FactorsResponse( + BillingAddress billingAddress, + CreditCard creditCard, + Device device, + Disposition disposition, + Email email, + Double fundsRemaining, + UUID id, + IpAddress ipAddress, + Integer queriesRemaining, + Double riskScore, + ShippingAddress shippingAddress, + List riskScoreReasons, + Subscores subscores, + List warnings + ) { + this(billingAddress, null, creditCard, device, disposition, email, fundsRemaining, id, + ipAddress, queriesRemaining, riskScore, shippingAddress, null, riskScoreReasons, + subscores, warnings); + } + + /** + * @return A list containing objects that describe risk score reasons + * for a given transaction that change the risk score significantly. + */ + @JsonProperty("risk_score_reasons") + public List getRiskScoreReasons() { + return riskScoreReasons; } /** diff --git a/src/main/java/com/maxmind/minfraud/response/Reason.java b/src/main/java/com/maxmind/minfraud/response/Reason.java new file mode 100644 index 00000000..eab36fa3 --- /dev/null +++ b/src/main/java/com/maxmind/minfraud/response/Reason.java @@ -0,0 +1,98 @@ +package com.maxmind.minfraud.response; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.maxmind.minfraud.AbstractModel; + +/** + * This class represents a risk score reason for the multiplier. + */ +public final class Reason extends AbstractModel { + private final String code; + private final String reason; + + /** + * Constructor for {@code Reason}. + * + * @param code The code. + * @param reason The reason. + */ + public Reason( + @JsonProperty("code") String code, + @JsonProperty("reason") String reason + ) { + this.code = code; + this.reason = reason; + } + + /** + * This field provides a machine-readable code identifying the reason. + * 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.
  • + *
+ * + * @return The code. + */ + @JsonProperty("code") + public String getCode() { + return this.code; + } + + /** + * @return The human-readable explanation of the reason. + * The description may change at any time and should not be matched against. + */ + @JsonProperty("reason") + public String getReason() { + return this.reason; + } +} diff --git a/src/main/java/com/maxmind/minfraud/response/RiskScoreReason.java b/src/main/java/com/maxmind/minfraud/response/RiskScoreReason.java new file mode 100644 index 00000000..75729b70 --- /dev/null +++ b/src/main/java/com/maxmind/minfraud/response/RiskScoreReason.java @@ -0,0 +1,51 @@ +package com.maxmind.minfraud.response; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.maxmind.minfraud.AbstractModel; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * This class represents a risk score multiplier and reasons for that multiplier. + */ +public final class RiskScoreReason extends AbstractModel { + private final Double multiplier; + private final List reasons; + + /** + * Constructor for {@code RiskScoreReason}. + * + * @param multiplier The multiplier. + * @param reasons The reasons. + */ + public RiskScoreReason( + @JsonProperty("multiplier") Double multiplier, + @JsonProperty("reasons") List reasons + ) { + this.multiplier = multiplier; + this.reasons = + Collections.unmodifiableList(reasons == null ? new ArrayList<>() : reasons); + } + + /** + * @return 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. + */ + @JsonProperty("multiplier") + public Double getMultiplier() { + return multiplier; + } + + /** + * @return An unmodifiable list containing objects that describe + * one of the reasons for the multiplier. + * This will be an empty list if there are no reasons. + */ + @JsonProperty("reasons") + public List getReasons() { + return reasons; + } +} diff --git a/src/test/java/com/maxmind/minfraud/response/FactorsResponseTest.java b/src/test/java/com/maxmind/minfraud/response/FactorsResponseTest.java index cd382782..9b42857a 100644 --- a/src/test/java/com/maxmind/minfraud/response/FactorsResponseTest.java +++ b/src/test/java/com/maxmind/minfraud/response/FactorsResponseTest.java @@ -50,6 +50,17 @@ public void testFactors() throws Exception { .put("queries_remaining", 123) .put("id", id) .put("risk_score", 0.01) + .startArrayField("risk_score_reasons") + .startObject() + .put("multiplier", 45) + .startArrayField("reasons") + .startObject() + .put("code", "ANONYMOUS_IP") + .put("reason", "Risk due to IP being an Anonymous IP") + .end() + .end() + .end() + .end() .end() .finish() ); @@ -147,5 +158,20 @@ public void testFactors() throws Exception { factors.getRiskScore(), "correct risk score" ); + assertEquals( + Double.valueOf(45), + factors.getRiskScoreReasons().get(0).getMultiplier(), + "risk multiplier" + ); + assertEquals( + "ANONYMOUS_IP", + factors.getRiskScoreReasons().get(0).getReasons().get(0).getCode(), + "risk reason code" + ); + assertEquals( + "Risk due to IP being an Anonymous IP", + factors.getRiskScoreReasons().get(0).getReasons().get(0).getReason(), + "risk reason" + ); } } diff --git a/src/test/java/com/maxmind/minfraud/response/ReasonTest.java b/src/test/java/com/maxmind/minfraud/response/ReasonTest.java new file mode 100644 index 00000000..5a1b706c --- /dev/null +++ b/src/test/java/com/maxmind/minfraud/response/ReasonTest.java @@ -0,0 +1,30 @@ +package com.maxmind.minfraud.response; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import com.fasterxml.jackson.jr.ob.JSON; +import org.junit.jupiter.api.Test; + +public class ReasonTest extends AbstractOutputTest { + + @Test + public void testReason() throws Exception { + String code = "ANONYMOUS_IP"; + String msg = "Risk due to IP being an Anonymous IP"; + + Reason reason = this.deserialize( + Reason.class, + JSON.std + .composeString() + .startObject() + .put("code", code) + .put("reason", msg) + .end() + .finish() + ); + + assertEquals(code, reason.getCode(), "code"); + assertEquals(msg, reason.getReason(), "reason"); + } + +} diff --git a/src/test/java/com/maxmind/minfraud/response/RiskScoreReasonTest.java b/src/test/java/com/maxmind/minfraud/response/RiskScoreReasonTest.java new file mode 100644 index 00000000..895bdfc0 --- /dev/null +++ b/src/test/java/com/maxmind/minfraud/response/RiskScoreReasonTest.java @@ -0,0 +1,51 @@ +package com.maxmind.minfraud.response; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import com.fasterxml.jackson.jr.ob.JSON; +import org.junit.jupiter.api.Test; + +public class RiskScoreReasonTest extends AbstractOutputTest { + + @Test + public void testRiskScoreReason() throws Exception { + RiskScoreReason reason = this.deserialize( + RiskScoreReason.class, + JSON.std + .composeString() + .startObject() + .put("multiplier", 45) + .startArrayField("reasons") + .startObject() + .put("code", "ANONYMOUS_IP") + .put("reason", "Risk due to IP being an Anonymous IP") + .end() + .end() + .end() + .finish() + ); + + assertEquals(Double.valueOf(45), reason.getMultiplier(), "multiplier"); + assertEquals( + "ANONYMOUS_IP", + reason.getReasons().get(0).getCode(), + "risk reason code" + ); + assertEquals( + "Risk due to IP being an Anonymous IP", + reason.getReasons().get(0).getReason(), + "risk reason" + ); + } + + @Test + public void testEmptyObject() throws Exception { + RiskScoreReason reason = this.deserialize( + RiskScoreReason.class, + "{}" + ); + + assertNotNull(reason.getReasons()); + } +} diff --git a/src/test/resources/test-data/factors-response.json b/src/test/resources/test-data/factors-response.json index 53a98b77..7e4d096d 100644 --- a/src/test/resources/test-data/factors-response.json +++ b/src/test/resources/test-data/factors-response.json @@ -191,5 +191,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" + } + ] + } ] }