diff --git a/CHANGELOG.md b/CHANGELOG.md index b6987e77..8cc34c8d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,13 @@ CHANGELOG ========= +3.5.1-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..e9a81227 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; } @@ -77,6 +85,15 @@ public FactorsResponse( ipAddress, queriesRemaining, riskScore, shippingAddress, null, 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; + } + /** * @return The {@code Subscores} model object containing the risk factor scores. */ 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..9d797a24 --- /dev/null +++ b/src/main/java/com/maxmind/minfraud/response/Reason.java @@ -0,0 +1,84 @@ +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. + */ + 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. + */ + 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..a6174fb4 --- /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/Reason.java b/src/test/java/com/maxmind/minfraud/response/Reason.java new file mode 100644 index 00000000..5a1b706c --- /dev/null +++ b/src/test/java/com/maxmind/minfraud/response/Reason.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/RiskScoreReason.java b/src/test/java/com/maxmind/minfraud/response/RiskScoreReason.java new file mode 100644 index 00000000..821afd76 --- /dev/null +++ b/src/test/java/com/maxmind/minfraud/response/RiskScoreReason.java @@ -0,0 +1,52 @@ +package com.maxmind.minfraud.response; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +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..9e5353b5 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, + "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" + } + ] + } ] }