├── .mailmap ├── .gitignore ├── src ├── test │ ├── java │ │ └── com │ │ │ └── maxmind │ │ │ └── minfraud │ │ │ ├── request │ │ │ ├── BillingTest.java │ │ │ ├── AccountTest.java │ │ │ ├── ShippingTest.java │ │ │ ├── ShoppingCartTest.java │ │ │ ├── PaymentTest.java │ │ │ ├── DeviceTest.java │ │ │ ├── EventTest.java │ │ │ ├── CustomInputsTest.java │ │ │ ├── OrderTest.java │ │ │ ├── AbstractLocationTest.java │ │ │ ├── TransactionTest.java │ │ │ ├── TransactionReportTest.java │ │ │ └── CreditCardTest.java │ │ │ ├── exception │ │ │ ├── HttpExceptionTest.java │ │ │ └── InvalidRequestExceptionTest.java │ │ │ └── response │ │ │ ├── GeoIp2LocationTest.java │ │ │ ├── ReasonTest.java │ │ │ ├── DispositionTest.java │ │ │ ├── WarningTest.java │ │ │ ├── IssuerTest.java │ │ │ ├── PhoneTest.java │ │ │ ├── DeviceTest.java │ │ │ ├── CreditCardTest.java │ │ │ ├── BillingAddressTest.java │ │ │ ├── RiskScoreReasonTest.java │ │ │ ├── AbstractOutputTest.java │ │ │ ├── ScoreResponseTest.java │ │ │ ├── ShippingAddressTest.java │ │ │ ├── EmailTest.java │ │ │ ├── IpAddressTest.java │ │ │ ├── FactorsResponseTest.java │ │ │ ├── InsightsResponseTest.java │ │ │ ├── EmailDomainVisitTest.java │ │ │ └── EmailDomainTest.java │ └── resources │ │ └── test-data │ │ ├── score-response.json │ │ ├── full-request.json │ │ ├── full-request-email-md5.json │ │ ├── insights-response.json │ │ └── factors-response.json ├── main │ └── java │ │ ├── com │ │ └── maxmind │ │ │ └── minfraud │ │ │ ├── exception │ │ │ ├── InsufficientFundsException.java │ │ │ ├── AuthenticationException.java │ │ │ ├── PermissionRequiredException.java │ │ │ ├── MinFraudException.java │ │ │ ├── HttpException.java │ │ │ └── InvalidRequestException.java │ │ │ ├── JsonSerializable.java │ │ │ ├── response │ │ │ ├── IpAddressInterface.java │ │ │ ├── ScoreIpAddress.java │ │ │ ├── Issuer.java │ │ │ ├── EmailDomainVisit.java │ │ │ ├── RiskScoreReason.java │ │ │ ├── Email.java │ │ │ ├── EmailDomain.java │ │ │ ├── Phone.java │ │ │ ├── Disposition.java │ │ │ ├── BillingAddress.java │ │ │ ├── IpRiskReason.java │ │ │ ├── Device.java │ │ │ ├── ShippingAddress.java │ │ │ ├── CreditCard.java │ │ │ ├── Warning.java │ │ │ ├── ScoreResponse.java │ │ │ └── GeoIp2Location.java │ │ │ ├── request │ │ │ ├── Billing.java │ │ │ ├── Shipping.java │ │ │ ├── Account.java │ │ │ ├── ShoppingCartItem.java │ │ │ ├── CustomInputs.java │ │ │ ├── Device.java │ │ │ ├── Order.java │ │ │ └── Event.java │ │ │ ├── AbstractModel.java │ │ │ └── Mapper.java │ │ └── module-info.java └── assembly │ └── bin.xml ├── .github ├── dependabot.yml └── workflows │ ├── zizmor.yml │ ├── test.yml │ └── codeql-analysis.yml ├── dev-bin └── release.sh └── README.dev.md /.mailmap: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.class 2 | *~ 3 | *.iml 4 | *.jar 5 | *.war 6 | *.ear 7 | *.sw? 8 | *.classpath 9 | .claude 10 | .gh-pages 11 | .idea 12 | .pmd 13 | .project 14 | .settings 15 | doc 16 | hs_err*.log 17 | pom.xml.versionsBackup 18 | target 19 | reports 20 | Main.java 21 | Test.java 22 | -------------------------------------------------------------------------------- /src/test/java/com/maxmind/minfraud/request/BillingTest.java: -------------------------------------------------------------------------------- 1 | package com.maxmind.minfraud.request; 2 | 3 | 4 | import com.maxmind.minfraud.request.Billing.Builder; 5 | 6 | public class BillingTest extends AbstractLocationTest { 7 | 8 | Builder builder() { 9 | return new Builder(); 10 | } 11 | } -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: maven 4 | directory: / 5 | schedule: 6 | interval: daily 7 | time: '14:00' 8 | open-pull-requests-limit: 10 9 | groups: 10 | jackson: 11 | patterns: 12 | - com.fasterxml.jackson* 13 | cooldown: 14 | default-days: 7 15 | - package-ecosystem: github-actions 16 | directory: / 17 | schedule: 18 | interval: daily 19 | time: '14:00' 20 | cooldown: 21 | default-days: 7 22 | -------------------------------------------------------------------------------- /src/main/java/com/maxmind/minfraud/exception/InsufficientFundsException.java: -------------------------------------------------------------------------------- 1 | package com.maxmind.minfraud.exception; 2 | 3 | /** 4 | * This exception is thrown when your account does not have sufficient funds to complete the 5 | * request. 6 | */ 7 | public final class InsufficientFundsException extends MinFraudException { 8 | 9 | /** 10 | * @param message A message explaining the cause of the error. 11 | */ 12 | public InsufficientFundsException(String message) { 13 | super(message); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /.github/workflows/zizmor.yml: -------------------------------------------------------------------------------- 1 | name: GitHub Actions Security Analysis with zizmor 2 | 3 | on: 4 | push: 5 | branches: ["main"] 6 | pull_request: 7 | branches: ["**"] 8 | 9 | permissions: {} 10 | 11 | jobs: 12 | zizmor: 13 | runs-on: ubuntu-latest 14 | permissions: 15 | security-events: write 16 | steps: 17 | - name: Checkout repository 18 | uses: actions/checkout@v6 19 | with: 20 | persist-credentials: false 21 | 22 | - name: Run zizmor 23 | uses: zizmorcore/zizmor-action@e639db99335bc9038abc0e066dfcd72e23d26fb4 # v0.3.0 24 | -------------------------------------------------------------------------------- /src/main/java/com/maxmind/minfraud/JsonSerializable.java: -------------------------------------------------------------------------------- 1 | package com.maxmind.minfraud; 2 | 3 | import java.io.IOException; 4 | 5 | /** 6 | * Interface for classes that can be serialized to JSON. 7 | * Provides default implementation for toJson() method. 8 | */ 9 | public interface JsonSerializable { 10 | 11 | /** 12 | * @return JSON representation of this object. 13 | * @throws IOException if there is an error serializing the object to JSON. 14 | */ 15 | default String toJson() throws IOException { 16 | return Mapper.get().writeValueAsString(this); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/test/java/com/maxmind/minfraud/exception/HttpExceptionTest.java: -------------------------------------------------------------------------------- 1 | package com.maxmind.minfraud.exception; 2 | 3 | import static org.junit.jupiter.api.Assertions.assertEquals; 4 | 5 | import java.net.URI; 6 | import org.junit.jupiter.api.Test; 7 | 8 | public class HttpExceptionTest { 9 | 10 | @Test 11 | public void testHttpException() throws Exception { 12 | var uri = new URI("https://www.maxmind.com/"); 13 | var e = new HttpException("message", 200, uri); 14 | assertEquals(200, e.httpStatus(), "correct status"); 15 | assertEquals(uri, e.uri(), "correct URL"); 16 | } 17 | } -------------------------------------------------------------------------------- /src/main/java/com/maxmind/minfraud/response/IpAddressInterface.java: -------------------------------------------------------------------------------- 1 | package com.maxmind.minfraud.response; 2 | 3 | /** 4 | * Interface for IP address risk models. 5 | */ 6 | public interface IpAddressInterface { 7 | /** 8 | * @return The risk associated with the IP address. 9 | */ 10 | Double risk(); 11 | 12 | /** 13 | * @return The risk associated with the IP address. 14 | * @deprecated Use {@link #risk()} instead. This method will be removed in 5.0.0. 15 | */ 16 | @Deprecated(since = "4.0.0", forRemoval = true) 17 | default Double getRisk() { 18 | return risk(); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/main/java/module-info.java: -------------------------------------------------------------------------------- 1 | /** 2 | * This module provides an API for the minFraud Score, Insights, and Factors web services. 3 | */ 4 | module com.maxmind.minfraud { 5 | requires com.fasterxml.jackson.annotation; 6 | requires com.fasterxml.jackson.core; 7 | requires com.fasterxml.jackson.databind; 8 | requires com.fasterxml.jackson.datatype.jsr310; 9 | requires transitive com.maxmind.geoip2; 10 | requires java.net.http; 11 | 12 | exports com.maxmind.minfraud; 13 | exports com.maxmind.minfraud.exception; 14 | exports com.maxmind.minfraud.request; 15 | exports com.maxmind.minfraud.response; 16 | } 17 | -------------------------------------------------------------------------------- /src/test/java/com/maxmind/minfraud/request/AccountTest.java: -------------------------------------------------------------------------------- 1 | package com.maxmind.minfraud.request; 2 | 3 | import static org.junit.jupiter.api.Assertions.assertEquals; 4 | 5 | import com.maxmind.minfraud.request.Account.Builder; 6 | import org.junit.jupiter.api.Test; 7 | 8 | public class AccountTest { 9 | 10 | @Test 11 | public void testUserId() { 12 | var account = new Builder().userId("usr").build(); 13 | assertEquals("usr", account.userId()); 14 | } 15 | 16 | @Test 17 | public void testUsername() { 18 | var account = new Builder().username("username").build(); 19 | assertEquals("14c4b06b824ec593239362517f538b29", account.usernameMd5()); 20 | } 21 | } -------------------------------------------------------------------------------- /src/test/java/com/maxmind/minfraud/request/ShippingTest.java: -------------------------------------------------------------------------------- 1 | package com.maxmind.minfraud.request; 2 | 3 | import static org.junit.jupiter.api.Assertions.assertEquals; 4 | 5 | import com.maxmind.minfraud.request.Shipping.Builder; 6 | import com.maxmind.minfraud.request.Shipping.DeliverySpeed; 7 | import org.junit.jupiter.api.Test; 8 | 9 | public class ShippingTest extends AbstractLocationTest { 10 | 11 | Builder builder() { 12 | return new Builder(); 13 | } 14 | 15 | @Test 16 | public void testDeliverySpeed() { 17 | var loc = this.builder().deliverySpeed(DeliverySpeed.EXPEDITED).build(); 18 | assertEquals(DeliverySpeed.EXPEDITED, loc.deliverySpeed()); 19 | } 20 | } -------------------------------------------------------------------------------- /src/main/java/com/maxmind/minfraud/exception/AuthenticationException.java: -------------------------------------------------------------------------------- 1 | package com.maxmind.minfraud.exception; 2 | 3 | /** 4 | * This exception is thrown when there is an error authenticating. 5 | */ 6 | public final class AuthenticationException extends MinFraudException { 7 | /** 8 | * @param message A message explaining the cause of the error. 9 | */ 10 | public AuthenticationException(String message) { 11 | super(message); 12 | } 13 | 14 | /** 15 | * @param message A message explaining the cause of the error. 16 | * @param e The cause of the exception. 17 | */ 18 | public AuthenticationException(String message, Throwable e) { 19 | super(message, e); 20 | } 21 | } -------------------------------------------------------------------------------- /src/main/java/com/maxmind/minfraud/exception/PermissionRequiredException.java: -------------------------------------------------------------------------------- 1 | package com.maxmind.minfraud.exception; 2 | 3 | /** 4 | * This exception is thrown when permission is required to use the service. 5 | */ 6 | public final class PermissionRequiredException extends MinFraudException { 7 | /** 8 | * @param message A message explaining the cause of the error. 9 | */ 10 | public PermissionRequiredException(String message) { 11 | super(message); 12 | } 13 | 14 | /** 15 | * @param message A message explaining the cause of the error. 16 | * @param e The cause of the exception. 17 | */ 18 | public PermissionRequiredException(String message, Throwable e) { 19 | super(message, e); 20 | } 21 | } -------------------------------------------------------------------------------- /src/test/java/com/maxmind/minfraud/exception/InvalidRequestExceptionTest.java: -------------------------------------------------------------------------------- 1 | package com.maxmind.minfraud.exception; 2 | 3 | import static org.junit.jupiter.api.Assertions.assertEquals; 4 | 5 | import java.net.URI; 6 | import org.junit.jupiter.api.Test; 7 | 8 | public class InvalidRequestExceptionTest { 9 | 10 | @Test 11 | public void testInvalidRequestException() throws Exception { 12 | var uri = new URI("https://www.maxmind.com/"); 13 | var code = "INVALID_INPUT"; 14 | var status = 400; 15 | var e = new InvalidRequestException("message", code, status, uri, null); 16 | assertEquals(code, e.code(), "correct code"); 17 | assertEquals(status, e.httpStatus(), "correct status"); 18 | assertEquals(uri, e.uri(), "correct URL"); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/main/java/com/maxmind/minfraud/request/Billing.java: -------------------------------------------------------------------------------- 1 | package com.maxmind.minfraud.request; 2 | 3 | /** 4 | * The billing information for the transaction. 5 | */ 6 | public final class Billing extends AbstractLocation { 7 | private Billing(Billing.Builder builder) { 8 | super(builder); 9 | } 10 | 11 | /** 12 | * {@code Builder} creates instances of {@code Billing} from values set by the builder's 13 | * methods. 14 | */ 15 | public static final class Builder extends AbstractLocation.Builder { 16 | /** 17 | * @return An instance of {@code Billing} created from the fields set on this builder. 18 | */ 19 | @Override 20 | public Billing build() { 21 | return new Billing(this); 22 | } 23 | } 24 | } -------------------------------------------------------------------------------- /src/test/resources/test-data/score-response.json: -------------------------------------------------------------------------------- 1 | { 2 | "risk_score": 0.01, 3 | "id": "27d26476-e2bc-11e4-92b8-962e705b4af5", 4 | "funds_remaining": 10.00, 5 | "queries_remaining": 1000, 6 | "disposition": { 7 | "action": "reject", 8 | "reason": "custom_rule", 9 | "rule_label": "the label" 10 | }, 11 | "ip_address": { 12 | "risk": 99 13 | }, 14 | "warnings": [ 15 | { 16 | "code": "INPUT_INVALID", 17 | "input_pointer": "/account/user_id", 18 | "warning": "Encountered value at \/account\/user_id that does meet the required constraints" 19 | }, 20 | { 21 | "code": "INPUT_INVALID", 22 | "input_pointer": "/account/username_md5", 23 | "warning": "Encountered value at \/account\/username_md5 that does meet the required constraints" 24 | } 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Run tests 2 | on: [push, pull_request] 3 | permissions: {} 4 | jobs: 5 | test: 6 | runs-on: ${{ matrix.os }} 7 | strategy: 8 | fail-fast: false 9 | matrix: 10 | distribution: ['zulu'] 11 | os: [ubuntu-latest, windows-latest, macos-latest] 12 | version: [ 17, 21, 24 ] 13 | steps: 14 | - uses: actions/checkout@v6 15 | with: 16 | submodules: true 17 | persist-credentials: false 18 | - uses: actions/setup-java@v5 19 | with: 20 | distribution: ${{ matrix.distribution }} 21 | java-version: ${{ matrix.version }} 22 | - run: mvn test -B 23 | # This is after the test run to work around 24 | # https://issues.apache.org/jira/projects/MJAVADOC/issues/MJAVADOC-736 25 | - run: mvn javadoc:javadoc 26 | -------------------------------------------------------------------------------- /src/main/java/com/maxmind/minfraud/AbstractModel.java: -------------------------------------------------------------------------------- 1 | package com.maxmind.minfraud; 2 | 3 | import java.io.IOException; 4 | 5 | /** 6 | * This {@code AbstractModel} is the base class for all model classes. 7 | */ 8 | public abstract class AbstractModel { 9 | /** 10 | * @return JSON representation of this object. 11 | * @throws IOException if there is an error serializing the object to JSON. 12 | */ 13 | public final String toJson() throws IOException { 14 | return Mapper.get().writeValueAsString(this); 15 | } 16 | 17 | @Override 18 | public String toString() { 19 | // This exception should never happen. If it does happen, we did 20 | // something wrong. 21 | try { 22 | return getClass().getName() + " [ " + toJson() + " ]"; 23 | } catch (IOException e) { 24 | throw new RuntimeException(e); 25 | } 26 | } 27 | } -------------------------------------------------------------------------------- /src/test/java/com/maxmind/minfraud/response/GeoIp2LocationTest.java: -------------------------------------------------------------------------------- 1 | package com.maxmind.minfraud.response; 2 | 3 | import static org.junit.jupiter.api.Assertions.assertEquals; 4 | 5 | import com.fasterxml.jackson.jr.ob.JSON; 6 | import org.junit.jupiter.api.Test; 7 | 8 | public class GeoIp2LocationTest extends AbstractOutputTest { 9 | 10 | @Test 11 | public void testGetLocalTime() throws Exception { 12 | String time = "2015-04-19T12:59:23-01:00"; 13 | GeoIp2Location location = this.deserialize( 14 | GeoIp2Location.class, 15 | JSON.std 16 | .composeString() 17 | .startObject() 18 | .put("local_time", time) 19 | .end() 20 | .finish() 21 | ); 22 | 23 | assertEquals(time, location.localTime()); 24 | assertEquals(time, location.getLocalDateTime().toString()); 25 | } 26 | } -------------------------------------------------------------------------------- /src/test/java/com/maxmind/minfraud/response/ReasonTest.java: -------------------------------------------------------------------------------- 1 | package com.maxmind.minfraud.response; 2 | 3 | import static org.junit.jupiter.api.Assertions.assertEquals; 4 | 5 | import com.fasterxml.jackson.jr.ob.JSON; 6 | import org.junit.jupiter.api.Test; 7 | 8 | public class ReasonTest extends AbstractOutputTest { 9 | 10 | @Test 11 | public void testReason() throws Exception { 12 | String code = "ANONYMOUS_IP"; 13 | String msg = "Risk due to IP being an Anonymous IP"; 14 | 15 | Reason reason = this.deserialize( 16 | Reason.class, 17 | JSON.std 18 | .composeString() 19 | .startObject() 20 | .put("code", code) 21 | .put("reason", msg) 22 | .end() 23 | .finish() 24 | ); 25 | 26 | assertEquals(code, reason.code(), "code"); 27 | assertEquals(msg, reason.reason(), "reason"); 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /src/main/java/com/maxmind/minfraud/response/ScoreIpAddress.java: -------------------------------------------------------------------------------- 1 | package com.maxmind.minfraud.response; 2 | 3 | import com.fasterxml.jackson.annotation.JsonProperty; 4 | import com.maxmind.minfraud.JsonSerializable; 5 | 6 | /** 7 | * This class contains the IP address risk. 8 | * 9 | * @param risk The risk associated with the IP address. 10 | */ 11 | public record ScoreIpAddress( 12 | @JsonProperty("risk") 13 | Double risk 14 | ) implements IpAddressInterface, JsonSerializable { 15 | 16 | /** 17 | * Constructs an instance of {@code ScoreIpAddress} with no data. 18 | */ 19 | public ScoreIpAddress() { 20 | this(null); 21 | } 22 | 23 | /** 24 | * @return The risk associated with the IP address. 25 | * @deprecated Use {@link #risk()} instead. This method will be removed in 5.0.0. 26 | */ 27 | @Deprecated(since = "4.0.0", forRemoval = true) 28 | @JsonProperty("risk") 29 | public Double getRisk() { 30 | return risk(); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/test/java/com/maxmind/minfraud/response/DispositionTest.java: -------------------------------------------------------------------------------- 1 | package com.maxmind.minfraud.response; 2 | 3 | import static org.junit.jupiter.api.Assertions.assertEquals; 4 | 5 | import com.fasterxml.jackson.jr.ob.JSON; 6 | import org.junit.jupiter.api.Test; 7 | 8 | public class DispositionTest extends AbstractOutputTest { 9 | 10 | @Test 11 | public void testDisposition() throws Exception { 12 | Disposition disposition = this.deserialize( 13 | Disposition.class, 14 | JSON.std 15 | .composeString() 16 | .startObject() 17 | .put("action", "accept") 18 | .put("reason", "default") 19 | .put("rule_label", "the label") 20 | .end() 21 | .finish() 22 | ); 23 | 24 | assertEquals("accept", disposition.action()); 25 | assertEquals("default", disposition.reason()); 26 | assertEquals("the label", disposition.ruleLabel()); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/assembly/bin.xml: -------------------------------------------------------------------------------- 1 | 2 | with-dependencies 3 | 4 | zip 5 | 6 | 7 | 8 | false 9 | runtime 10 | lib 11 | 12 | 13 | 14 | 15 | ${project.basedir} 16 | / 17 | 18 | README* 19 | LICENSE* 20 | CHANGELOG* 21 | 22 | 23 | 24 | ${project.basedir}/target 25 | lib 26 | 27 | *.jar 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /src/main/java/com/maxmind/minfraud/exception/MinFraudException.java: -------------------------------------------------------------------------------- 1 | package com.maxmind.minfraud.exception; 2 | 3 | /** 4 | * This class represents a non-specific error with the web service. Generally this will be thrown if 5 | * the web service responds with an expected status but unexpected content. 6 | *

7 | * It also serves as the base class for {@code AuthenticationException}, 8 | * {@code InsufficientFundsException}, and {@code InvalidRequestException}. 9 | */ 10 | public sealed class MinFraudException extends Exception 11 | permits AuthenticationException, InsufficientFundsException, InvalidRequestException, 12 | PermissionRequiredException { 13 | 14 | /** 15 | * @param message A message explaining the cause of the error. 16 | */ 17 | public MinFraudException(String message) { 18 | super(message); 19 | } 20 | 21 | /** 22 | * @param message A message explaining the cause of the error. 23 | * @param e The cause of the exception. 24 | */ 25 | public MinFraudException(String message, Throwable e) { 26 | super(message, e); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/test/java/com/maxmind/minfraud/response/WarningTest.java: -------------------------------------------------------------------------------- 1 | package com.maxmind.minfraud.response; 2 | 3 | import static org.junit.jupiter.api.Assertions.assertEquals; 4 | 5 | import com.fasterxml.jackson.jr.ob.JSON; 6 | import org.junit.jupiter.api.Test; 7 | 8 | public class WarningTest extends AbstractOutputTest { 9 | 10 | @Test 11 | public void testWarning() throws Exception { 12 | String code = "INVALID_INPUT"; 13 | String msg = "Input invalid"; 14 | String pointer = "/first/second"; 15 | 16 | Warning warning = this.deserialize( 17 | Warning.class, 18 | JSON.std 19 | .composeString() 20 | .startObject() 21 | .put("code", code) 22 | .put("warning", msg) 23 | .put("input_pointer", pointer) 24 | .end() 25 | .finish() 26 | ); 27 | 28 | assertEquals(code, warning.code(), "code"); 29 | assertEquals(msg, warning.warning(), "warning message"); 30 | assertEquals(pointer, warning.inputPointer(), "input_pointer"); 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /src/test/java/com/maxmind/minfraud/response/IssuerTest.java: -------------------------------------------------------------------------------- 1 | package com.maxmind.minfraud.response; 2 | 3 | import static org.junit.jupiter.api.Assertions.assertEquals; 4 | import static org.junit.jupiter.api.Assertions.assertTrue; 5 | 6 | import com.fasterxml.jackson.jr.ob.JSON; 7 | import org.junit.jupiter.api.Test; 8 | 9 | public class IssuerTest extends AbstractOutputTest { 10 | 11 | @Test 12 | public void testIssuer() throws Exception { 13 | String phone = "132-342-2131"; 14 | 15 | Issuer issuer = this.deserialize( 16 | Issuer.class, 17 | JSON.std 18 | .composeString() 19 | .startObject() 20 | .put("name", "Bank") 21 | .put("matches_provided_name", true) 22 | .put("phone_number", phone) 23 | .put("matches_provided_phone_number", true) 24 | .end() 25 | .finish() 26 | ); 27 | 28 | assertEquals("Bank", issuer.name(), "bank name"); 29 | assertTrue(issuer.matchesProvidedName(), "provided name matches"); 30 | assertEquals(phone, issuer.phoneNumber(), "phone"); 31 | assertTrue(issuer.matchesProvidedPhoneNumber(), "provided phone matches"); 32 | } 33 | } -------------------------------------------------------------------------------- /src/test/java/com/maxmind/minfraud/response/PhoneTest.java: -------------------------------------------------------------------------------- 1 | package com.maxmind.minfraud.response; 2 | 3 | import static org.junit.jupiter.api.Assertions.assertEquals; 4 | import static org.junit.jupiter.api.Assertions.assertFalse; 5 | import static org.junit.jupiter.api.Assertions.assertTrue; 6 | 7 | import com.fasterxml.jackson.jr.ob.JSON; 8 | import org.junit.jupiter.api.Test; 9 | 10 | public class PhoneTest extends AbstractOutputTest { 11 | 12 | @Test 13 | public void testPhone() throws Exception { 14 | Phone phone = this.deserialize( 15 | Phone.class, 16 | JSON.std 17 | .composeString() 18 | .startObject() 19 | .put("country", "US") 20 | .put("is_voip", true) 21 | .put("matches_postal", false) 22 | .put("network_operator", "Operator") 23 | .put("number_type", "fixed") 24 | .end() 25 | .finish() 26 | ); 27 | 28 | assertEquals("US", phone.country()); 29 | assertTrue(phone.isVoip()); 30 | assertFalse(phone.matchesPostal()); 31 | assertEquals("Operator", phone.networkOperator()); 32 | assertEquals("fixed", phone.numberType()); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/test/java/com/maxmind/minfraud/request/ShoppingCartTest.java: -------------------------------------------------------------------------------- 1 | package com.maxmind.minfraud.request; 2 | 3 | import static org.junit.jupiter.api.Assertions.assertEquals; 4 | 5 | import com.maxmind.minfraud.request.ShoppingCartItem.Builder; 6 | import java.math.BigDecimal; 7 | import org.junit.jupiter.api.Test; 8 | 9 | public class ShoppingCartTest { 10 | 11 | @Test 12 | public void testCategory() { 13 | var item = new Builder().category("cat1").build(); 14 | assertEquals("cat1", item.category()); 15 | } 16 | 17 | @Test 18 | public void testItemId() { 19 | var item = new Builder().itemId("id5").build(); 20 | assertEquals("id5", item.itemId()); 21 | } 22 | 23 | @Test 24 | public void testQuantity() { 25 | var item = new Builder().quantity(100).build(); 26 | assertEquals(Integer.valueOf(100), item.quantity()); 27 | } 28 | 29 | @Test 30 | public void testPrice() { 31 | var item = new Builder().price(BigDecimal.TEN).build(); 32 | assertEquals(BigDecimal.TEN, item.price()); 33 | } 34 | 35 | @Test 36 | public void testDoublePrice() { 37 | var item = new Builder().price(10.3).build(); 38 | assertEquals(BigDecimal.valueOf(10.3), item.price()); 39 | } 40 | } -------------------------------------------------------------------------------- /src/test/java/com/maxmind/minfraud/response/DeviceTest.java: -------------------------------------------------------------------------------- 1 | package com.maxmind.minfraud.response; 2 | 3 | import static org.junit.jupiter.api.Assertions.assertEquals; 4 | 5 | import com.fasterxml.jackson.jr.ob.JSON; 6 | import java.util.UUID; 7 | import org.junit.jupiter.api.Test; 8 | 9 | public class DeviceTest extends AbstractOutputTest { 10 | 11 | @Test 12 | public void testDevice() throws Exception { 13 | Device device = this.deserialize( 14 | Device.class, 15 | JSON.std 16 | .composeString() 17 | .startObject() 18 | .put("confidence", 99) 19 | .put("id", "C8D3BE1A-BE26-11E5-8C50-1B575C37265F") 20 | .put("last_seen", "2016-06-08T14:16:38Z") 21 | .put("local_time", "2018-04-05T15:21:01-07:00") 22 | .end() 23 | .finish() 24 | ); 25 | 26 | assertEquals(99.0, device.confidence(), 1e-15); 27 | assertEquals(UUID.fromString("C8D3BE1A-BE26-11E5-8C50-1B575C37265F"), device.id()); 28 | assertEquals("2016-06-08T14:16:38Z", device.lastSeen()); 29 | assertEquals("2016-06-08T14:16:38Z", device.getLastSeenDateTime().toString()); 30 | assertEquals("2018-04-05T15:21:01-07:00", device.localTime()); 31 | assertEquals("2018-04-05T15:21:01-07:00", device.getLocalDateTime().toString()); 32 | 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/main/java/com/maxmind/minfraud/Mapper.java: -------------------------------------------------------------------------------- 1 | package com.maxmind.minfraud; 2 | 3 | import com.fasterxml.jackson.annotation.JsonInclude; 4 | import com.fasterxml.jackson.databind.DeserializationFeature; 5 | import com.fasterxml.jackson.databind.MapperFeature; 6 | import com.fasterxml.jackson.databind.ObjectMapper; 7 | import com.fasterxml.jackson.databind.SerializationFeature; 8 | import com.fasterxml.jackson.databind.json.JsonMapper; 9 | import com.fasterxml.jackson.databind.util.StdDateFormat; 10 | import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; 11 | 12 | class Mapper { 13 | private static final ObjectMapper mapper = JsonMapper.builder() 14 | .addModule(new JavaTimeModule()) 15 | .defaultDateFormat(new StdDateFormat().withColonInTimeZone(true)) 16 | .enable(SerializationFeature.WRITE_ENUMS_USING_TO_STRING) 17 | .enable(DeserializationFeature.READ_ENUMS_USING_TO_STRING) 18 | .enable(DeserializationFeature.READ_UNKNOWN_ENUM_VALUES_AS_NULL) 19 | .disable(MapperFeature.CAN_OVERRIDE_ACCESS_MODIFIERS) 20 | .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES) 21 | .disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS) 22 | .serializationInclusion(JsonInclude.Include.NON_NULL) 23 | .serializationInclusion(JsonInclude.Include.NON_EMPTY) 24 | .build(); 25 | 26 | public static ObjectMapper get() { 27 | return mapper; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/test/java/com/maxmind/minfraud/response/CreditCardTest.java: -------------------------------------------------------------------------------- 1 | package com.maxmind.minfraud.response; 2 | 3 | import static org.junit.jupiter.api.Assertions.assertEquals; 4 | import static org.junit.jupiter.api.Assertions.assertTrue; 5 | 6 | import com.fasterxml.jackson.jr.ob.JSON; 7 | import org.junit.jupiter.api.Test; 8 | 9 | public class CreditCardTest extends AbstractOutputTest { 10 | 11 | @Test 12 | public void testCreditCard() throws Exception { 13 | CreditCard cc = this.deserialize( 14 | CreditCard.class, 15 | JSON.std 16 | .composeString() 17 | .startObject() 18 | .startObjectField("issuer") 19 | .put("name", "Bank") 20 | .end() 21 | .put("brand", "Visa") 22 | .put("country", "US") 23 | .put("is_business", true) 24 | .put("is_issued_in_billing_address_country", true) 25 | .put("is_prepaid", true) 26 | .put("is_virtual", true) 27 | .put("type", "credit") 28 | .end() 29 | .finish() 30 | ); 31 | 32 | assertEquals("Bank", cc.issuer().name()); 33 | assertEquals("US", cc.country()); 34 | assertEquals("Visa", cc.brand()); 35 | assertEquals("credit", cc.type()); 36 | assertTrue(cc.isBusiness()); 37 | assertTrue(cc.isPrepaid()); 38 | assertTrue(cc.isVirtual()); 39 | assertTrue(cc.isIssuedInBillingAddressCountry()); 40 | } 41 | 42 | } 43 | -------------------------------------------------------------------------------- /src/test/java/com/maxmind/minfraud/request/PaymentTest.java: -------------------------------------------------------------------------------- 1 | package com.maxmind.minfraud.request; 2 | 3 | import static org.junit.jupiter.api.Assertions.assertEquals; 4 | import static org.junit.jupiter.api.Assertions.assertTrue; 5 | 6 | import com.maxmind.minfraud.request.Payment.Builder; 7 | import com.maxmind.minfraud.request.Payment.Method; 8 | import com.maxmind.minfraud.request.Payment.Processor; 9 | import org.junit.jupiter.api.Test; 10 | 11 | public class PaymentTest { 12 | 13 | @Test 14 | public void testMethod() { 15 | var payment = new Builder().method(Method.CARD).build(); 16 | assertEquals(Method.CARD, payment.method()); 17 | 18 | payment = new Builder().method(Method.DIGITAL_WALLET).build(); 19 | assertEquals(Method.DIGITAL_WALLET, payment.method()); 20 | 21 | payment = new Builder().method(Method.BUY_NOW_PAY_LATER).build(); 22 | assertEquals(Method.BUY_NOW_PAY_LATER, payment.method()); 23 | } 24 | 25 | @Test 26 | public void testProcessor() { 27 | var payment = new Builder().processor(Processor.ADYEN).build(); 28 | assertEquals(Processor.ADYEN, payment.processor()); 29 | } 30 | 31 | @Test 32 | public void testWasAuthorized() { 33 | var payment = new Builder().wasAuthorized(true).build(); 34 | assertTrue(payment.wasAuthorized()); 35 | } 36 | 37 | @Test 38 | public void testDeclineCode() { 39 | var payment = new Builder().declineCode("declined").build(); 40 | assertEquals("declined", payment.declineCode()); 41 | } 42 | } -------------------------------------------------------------------------------- /src/test/java/com/maxmind/minfraud/response/BillingAddressTest.java: -------------------------------------------------------------------------------- 1 | package com.maxmind.minfraud.response; 2 | 3 | import static org.junit.jupiter.api.Assertions.assertEquals; 4 | import static org.junit.jupiter.api.Assertions.assertTrue; 5 | 6 | import com.fasterxml.jackson.jr.ob.JSON; 7 | import org.junit.jupiter.api.Test; 8 | 9 | public class BillingAddressTest extends AbstractOutputTest { 10 | private static final double DELTA = 1e-15; 11 | 12 | @Test 13 | public void testBillingAddress() throws Exception { 14 | BillingAddress address = this.deserialize(BillingAddress.class, 15 | JSON.std 16 | .composeString() 17 | .startObject() 18 | .put("is_in_ip_country", true) 19 | .put("latitude", 43.1) 20 | .put("longitude", 32.1) 21 | .put("distance_to_ip_location", 100) 22 | .put("is_postal_in_city", true) 23 | .end() 24 | .finish() 25 | ); 26 | 27 | assertTrue(address.isInIpCountry(), "correct isInIpCountry"); 28 | assertTrue(address.isPostalInCity(), "correct isPostalInCity"); 29 | assertEquals( 30 | 100, 31 | address.distanceToIpLocation().longValue(), 32 | "correct distanceToIpLocation" 33 | ); 34 | assertEquals( 35 | 32.1, 36 | address.longitude(), 37 | DELTA, 38 | "correct longitude" 39 | ); 40 | assertEquals( 41 | 43.1, 42 | address.latitude(), 43 | DELTA, 44 | "correct latitude" 45 | ); 46 | } 47 | } -------------------------------------------------------------------------------- /src/main/java/com/maxmind/minfraud/exception/HttpException.java: -------------------------------------------------------------------------------- 1 | package com.maxmind.minfraud.exception; 2 | 3 | import java.io.IOException; 4 | import java.net.URI; 5 | 6 | /** 7 | * This class represents an HTTP transport error. This is not an error returned by the web service 8 | * itself. As such, it is a IOException instead of a MinFraudException. 9 | */ 10 | public final class HttpException extends IOException { 11 | private final int httpStatus; 12 | private final URI uri; 13 | 14 | /** 15 | * @param message A message describing the reason why the exception was thrown. 16 | * @param httpStatus The HTTP status of the response that caused the exception. 17 | * @param uri The URI queried. 18 | */ 19 | public HttpException(String message, int httpStatus, URI uri) { 20 | super(message); 21 | this.httpStatus = httpStatus; 22 | this.uri = uri; 23 | } 24 | 25 | /** 26 | * @param message A message describing the reason why the exception was thrown. 27 | * @param httpStatus The HTTP status of the response that caused the exception. 28 | * @param uri The URI queried. 29 | * @param cause The cause of the exception. 30 | */ 31 | public HttpException(String message, int httpStatus, URI uri, 32 | Throwable cause) { 33 | super(message, cause); 34 | this.httpStatus = httpStatus; 35 | this.uri = uri; 36 | } 37 | 38 | /** 39 | * @return the HTTP status of the query that caused the exception. 40 | */ 41 | public int httpStatus() { 42 | return httpStatus; 43 | } 44 | 45 | /** 46 | * @return the URI queried. 47 | */ 48 | public URI uri() { 49 | return this.uri; 50 | } 51 | 52 | } -------------------------------------------------------------------------------- /src/test/java/com/maxmind/minfraud/response/RiskScoreReasonTest.java: -------------------------------------------------------------------------------- 1 | package com.maxmind.minfraud.response; 2 | 3 | import static org.junit.jupiter.api.Assertions.assertEquals; 4 | import static org.junit.jupiter.api.Assertions.assertNotNull; 5 | 6 | import com.fasterxml.jackson.jr.ob.JSON; 7 | import org.junit.jupiter.api.Test; 8 | 9 | public class RiskScoreReasonTest extends AbstractOutputTest { 10 | 11 | @Test 12 | public void testRiskScoreReason() throws Exception { 13 | RiskScoreReason reason = this.deserialize( 14 | RiskScoreReason.class, 15 | JSON.std 16 | .composeString() 17 | .startObject() 18 | .put("multiplier", 45) 19 | .startArrayField("reasons") 20 | .startObject() 21 | .put("code", "ANONYMOUS_IP") 22 | .put("reason", "Risk due to IP being an Anonymous IP") 23 | .end() 24 | .end() 25 | .end() 26 | .finish() 27 | ); 28 | 29 | assertEquals(Double.valueOf(45), reason.multiplier(), "multiplier"); 30 | assertEquals(1, reason.reasons().size()); 31 | assertEquals( 32 | "ANONYMOUS_IP", 33 | reason.reasons().get(0).code(), 34 | "risk reason code" 35 | ); 36 | assertEquals( 37 | "Risk due to IP being an Anonymous IP", 38 | reason.reasons().get(0).reason(), 39 | "risk reason" 40 | ); 41 | } 42 | 43 | @Test 44 | public void testEmptyObject() throws Exception { 45 | RiskScoreReason reason = this.deserialize( 46 | RiskScoreReason.class, 47 | "{}" 48 | ); 49 | 50 | assertNotNull(reason.reasons()); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/test/java/com/maxmind/minfraud/response/AbstractOutputTest.java: -------------------------------------------------------------------------------- 1 | package com.maxmind.minfraud.response; 2 | 3 | import com.fasterxml.jackson.annotation.JsonInclude; 4 | import com.fasterxml.jackson.databind.DeserializationFeature; 5 | import com.fasterxml.jackson.databind.InjectableValues; 6 | import com.fasterxml.jackson.databind.InjectableValues.Std; 7 | import com.fasterxml.jackson.databind.MapperFeature; 8 | import com.fasterxml.jackson.databind.ObjectMapper; 9 | import com.fasterxml.jackson.databind.SerializationFeature; 10 | import com.fasterxml.jackson.databind.json.JsonMapper; 11 | import com.fasterxml.jackson.databind.util.StdDateFormat; 12 | import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; 13 | import java.io.IOException; 14 | import java.util.List; 15 | 16 | public abstract class AbstractOutputTest { 17 | 18 | T deserialize(Class cls, String json) throws IOException { 19 | var mapper = JsonMapper.builder() 20 | .addModule(new JavaTimeModule()) 21 | .defaultDateFormat(new StdDateFormat().withColonInTimeZone(true)) 22 | .enable(SerializationFeature.WRITE_ENUMS_USING_TO_STRING) 23 | .enable(DeserializationFeature.READ_ENUMS_USING_TO_STRING) 24 | .enable(DeserializationFeature.READ_UNKNOWN_ENUM_VALUES_AS_NULL) 25 | .disable(MapperFeature.CAN_OVERRIDE_ACCESS_MODIFIERS) 26 | .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES) 27 | .disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS) 28 | .serializationInclusion(JsonInclude.Include.NON_NULL) 29 | .serializationInclusion(JsonInclude.Include.NON_EMPTY) 30 | .build(); 31 | var inject = new Std().addValue( 32 | "locales", List.of("en")); 33 | return mapper.readerFor(cls).with(inject).readValue(json); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | name: "Code scanning - action" 2 | 3 | on: 4 | push: 5 | branches-ignore: 6 | - 'dependabot/**' 7 | pull_request: 8 | schedule: 9 | - cron: '0 4 * * 0' 10 | 11 | jobs: 12 | CodeQL-Build: 13 | 14 | runs-on: ubuntu-latest 15 | 16 | permissions: 17 | security-events: write 18 | 19 | steps: 20 | - name: Checkout repository 21 | uses: actions/checkout@v6 22 | with: 23 | # We must fetch at least the immediate parents so that if this is 24 | # a pull request then we can checkout the head. 25 | fetch-depth: 2 26 | persist-credentials: false 27 | 28 | # If this run was triggered by a pull request event, then checkout 29 | # the head of the pull request instead of the merge commit. 30 | - run: git checkout HEAD^2 31 | if: ${{ github.event_name == 'pull_request' }} 32 | 33 | # Initializes the CodeQL tools for scanning. 34 | - name: Initialize CodeQL 35 | uses: github/codeql-action/init@v4 36 | # Override language selection by uncommenting this and choosing your languages 37 | # with: 38 | # languages: go, javascript, csharp, python, cpp, java 39 | 40 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 41 | # If this step fails, then you should remove it and run the build manually (see below) 42 | - name: Autobuild 43 | uses: github/codeql-action/autobuild@v4 44 | 45 | # ℹ️ Command-line programs to run using the OS shell. 46 | # 📚 https://git.io/JvXDl 47 | 48 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 49 | # and modify them (or add more) to build your code if your project 50 | # uses a compiled language 51 | 52 | #- run: | 53 | # make bootstrap 54 | # make release 55 | 56 | - name: Perform CodeQL Analysis 57 | uses: github/codeql-action/analyze@v4 58 | -------------------------------------------------------------------------------- /src/test/java/com/maxmind/minfraud/response/ScoreResponseTest.java: -------------------------------------------------------------------------------- 1 | package com.maxmind.minfraud.response; 2 | 3 | import static org.junit.jupiter.api.Assertions.assertEquals; 4 | 5 | import com.fasterxml.jackson.jr.ob.JSON; 6 | import java.util.UUID; 7 | import org.junit.jupiter.api.Test; 8 | 9 | public class ScoreResponseTest extends AbstractOutputTest { 10 | 11 | @Test 12 | public void testScore() throws Exception { 13 | String id = "b643d445-18b2-4b9d-bad4-c9c4366e402a"; 14 | ScoreResponse score = this.deserialize( 15 | ScoreResponse.class, 16 | JSON.std 17 | .composeString() 18 | .startObject() 19 | .put("funds_remaining", 1.20) 20 | .put("id", id) 21 | .put("queries_remaining", 123) 22 | .put("risk_score", 0.01) 23 | .startObjectField("disposition") 24 | .put("action", "manual_review") 25 | .end() 26 | .startObjectField("ip_address") 27 | .put("risk", 0.02) 28 | .end() 29 | .startArrayField("warnings") 30 | .startObject() 31 | .put("code", "INVALID_INPUT") 32 | .end() 33 | .end() 34 | .end() 35 | .finish() 36 | ); 37 | 38 | assertEquals(UUID.fromString(id), score.id(), "correct ID"); 39 | assertEquals(Double.valueOf(1.20), score.fundsRemaining(), "correct funds remaining"); 40 | assertEquals(Integer.valueOf(123), score.queriesRemaining(), "queries remaining"); 41 | assertEquals(Double.valueOf(0.01), score.riskScore(), "risk score"); 42 | assertEquals("manual_review", score.disposition().action(), "disposition"); 43 | assertEquals(Double.valueOf(0.02), score.ipAddress().risk(), "IP risk"); 44 | assertEquals("INVALID_INPUT", score.warnings().get(0).code(), "warning code"); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/test/java/com/maxmind/minfraud/request/DeviceTest.java: -------------------------------------------------------------------------------- 1 | package com.maxmind.minfraud.request; 2 | 3 | import static org.junit.jupiter.api.Assertions.assertEquals; 4 | import static org.junit.jupiter.api.Assertions.assertNull; 5 | 6 | import com.maxmind.minfraud.request.Device.Builder; 7 | import java.net.InetAddress; 8 | import java.net.UnknownHostException; 9 | import org.junit.jupiter.api.Test; 10 | 11 | public class DeviceTest { 12 | 13 | private final InetAddress ip; 14 | 15 | public DeviceTest() throws UnknownHostException { 16 | ip = InetAddress.getByName("1.1.1.1"); 17 | } 18 | 19 | @Test 20 | public void testConstructorWithoutIP() { 21 | var device = new Builder().build(); 22 | assertNull(device.ipAddress()); 23 | } 24 | 25 | @Test 26 | public void testIpAddressThroughConstructor() { 27 | var device = new Builder(ip).build(); 28 | assertEquals(ip, device.ipAddress()); 29 | } 30 | 31 | @Test 32 | public void testIpAddress() { 33 | var device = new Builder().ipAddress(ip).build(); 34 | assertEquals(ip, device.ipAddress()); 35 | } 36 | 37 | @Test 38 | public void testUserAgent() { 39 | var ua = "Mozila 5"; 40 | var device = new Builder(ip).userAgent(ua).build(); 41 | assertEquals(ua, device.userAgent()); 42 | } 43 | 44 | @Test 45 | public void testAcceptLanguage() { 46 | var al = "en-US"; 47 | var device = new Builder(ip).acceptLanguage(al).build(); 48 | assertEquals(al, device.acceptLanguage()); 49 | } 50 | 51 | @Test 52 | public void testSessionAge() { 53 | var hour = 3600d; 54 | var device = new Builder(ip).sessionAge(hour).build(); 55 | assertEquals(hour, device.sessionAge()); 56 | } 57 | 58 | @Test 59 | public void testSessionId() { 60 | var id = "foobar"; 61 | var device = new Builder(ip).sessionId(id).build(); 62 | assertEquals(id, device.sessionId()); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/test/java/com/maxmind/minfraud/response/ShippingAddressTest.java: -------------------------------------------------------------------------------- 1 | package com.maxmind.minfraud.response; 2 | 3 | import static org.junit.jupiter.api.Assertions.assertEquals; 4 | import static org.junit.jupiter.api.Assertions.assertFalse; 5 | import static org.junit.jupiter.api.Assertions.assertTrue; 6 | 7 | import com.fasterxml.jackson.jr.ob.JSON; 8 | import org.junit.jupiter.api.Test; 9 | 10 | public class ShippingAddressTest extends AbstractOutputTest { 11 | private static final double DELTA = 1e-15; 12 | 13 | @Test 14 | public void testShippingAddress() throws Exception { 15 | ShippingAddress address = this.deserialize( 16 | ShippingAddress.class, 17 | JSON.std 18 | .composeString() 19 | .startObject() 20 | .put("is_in_ip_country", true) 21 | .put("latitude", 43.1) 22 | .put("longitude", 32.1) 23 | .put("distance_to_ip_location", 100) 24 | .put("is_postal_in_city", true) 25 | .put("is_high_risk", false) 26 | .put("distance_to_billing_address", 200) 27 | .end() 28 | .finish() 29 | ); 30 | 31 | assertTrue(address.isInIpCountry(), "correct isInIpCountry"); 32 | assertTrue(address.isPostalInCity(), "correct isPostalInCity"); 33 | assertEquals( 34 | 100, 35 | address.distanceToIpLocation().longValue(), 36 | "correct distanceToIpLocation" 37 | ); 38 | assertEquals( 39 | 32.1, 40 | address.longitude(), 41 | DELTA, 42 | "correct longitude" 43 | ); 44 | assertEquals( 45 | 43.1, 46 | address.latitude(), 47 | DELTA, 48 | "correct latitude" 49 | ); 50 | 51 | assertFalse(address.isHighRisk(), "is high risk"); 52 | assertEquals( 53 | Integer.valueOf(200), 54 | address.distanceToBillingAddress(), 55 | "distance to billing address" 56 | ); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/main/java/com/maxmind/minfraud/response/Issuer.java: -------------------------------------------------------------------------------- 1 | package com.maxmind.minfraud.response; 2 | 3 | import com.fasterxml.jackson.annotation.JsonProperty; 4 | import com.maxmind.minfraud.JsonSerializable; 5 | 6 | /** 7 | * This class contains minFraud response data related to the credit card issuer. 8 | * 9 | * @param matchesProvidedName This is true if the name matches the name provided. 10 | * @param matchesProvidedPhoneNumber This is true if the phone number matches the one provided. 11 | * @param name The name of the bank which issued the credit card. 12 | * @param phoneNumber The phone number of the bank which issued the credit card. In 13 | * some cases the phone number we return may be out of date. 14 | */ 15 | public record Issuer( 16 | @JsonProperty("matches_provided_name") 17 | Boolean matchesProvidedName, 18 | 19 | @JsonProperty("matches_provided_phone_number") 20 | Boolean matchesProvidedPhoneNumber, 21 | 22 | @JsonProperty("name") 23 | String name, 24 | 25 | @JsonProperty("phone_number") 26 | String phoneNumber 27 | ) implements JsonSerializable { 28 | 29 | /** 30 | * Constructs an instance of {@code Issuer} with no data. 31 | */ 32 | public Issuer() { 33 | this(null, null, null, null); 34 | } 35 | 36 | /** 37 | * @return The name of the bank which issued the credit card. 38 | * @deprecated Use {@link #name()} instead. This method will be removed in 5.0.0. 39 | */ 40 | @Deprecated(since = "4.0.0", forRemoval = true) 41 | @JsonProperty("name") 42 | public String getName() { 43 | return name(); 44 | } 45 | 46 | /** 47 | * @return The phone number of the bank which issued the credit card. In some cases the phone 48 | * number we return may be out of date. 49 | * @deprecated Use {@link #phoneNumber()} instead. This method will be removed in 5.0.0. 50 | */ 51 | @Deprecated(since = "4.0.0", forRemoval = true) 52 | @JsonProperty("phone_number") 53 | public String getPhoneNumber() { 54 | return phoneNumber(); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/main/java/com/maxmind/minfraud/exception/InvalidRequestException.java: -------------------------------------------------------------------------------- 1 | package com.maxmind.minfraud.exception; 2 | 3 | import java.net.URI; 4 | 5 | /** 6 | * This class represents a non-specific error returned by MaxMind's minFraud web service. This 7 | * occurs when the web service is up and responding to requests, but the request sent was invalid in 8 | * some way. 9 | */ 10 | public final class InvalidRequestException extends MinFraudException { 11 | private final String code; 12 | private final int httpStatus; 13 | private final URI uri; 14 | 15 | /** 16 | * @param message A message explaining the cause of the error. 17 | * @param code The error code returned by the web service. 18 | * @param uri The URL queried. 19 | */ 20 | public InvalidRequestException(String message, String code, URI uri) { 21 | super(message); 22 | this.uri = uri; 23 | this.code = code; 24 | this.httpStatus = 0; 25 | } 26 | 27 | /** 28 | * @param message A message explaining the cause of the error. 29 | * @param code The error code returned by the web service. 30 | * @param httpStatus The HTTP status of the response. 31 | * @param uri The URL queried. 32 | * @param e The cause of the exception. 33 | */ 34 | public InvalidRequestException(String message, String code, int httpStatus, 35 | URI uri, Throwable e) { 36 | super(message, e); 37 | this.code = code; 38 | this.uri = uri; 39 | this.httpStatus = httpStatus; 40 | } 41 | 42 | /** 43 | * @return The error code returned by the MaxMind web service. 44 | */ 45 | public final String code() { 46 | return code; 47 | } 48 | 49 | /** 50 | * @return The integer HTTP status returned by the MaxMind web service. Will be 0 if it was not 51 | * set at throw time. 52 | */ 53 | public final int httpStatus() { 54 | return httpStatus; 55 | } 56 | 57 | /** 58 | * @return the URI queried. 59 | */ 60 | public URI uri() { 61 | return this.uri; 62 | } 63 | 64 | } 65 | -------------------------------------------------------------------------------- /src/main/java/com/maxmind/minfraud/request/Shipping.java: -------------------------------------------------------------------------------- 1 | package com.maxmind.minfraud.request; 2 | 3 | import com.fasterxml.jackson.annotation.JsonProperty; 4 | 5 | /** 6 | * The shipping information for the transaction. 7 | */ 8 | public final class Shipping extends AbstractLocation { 9 | private final DeliverySpeed deliverySpeed; 10 | 11 | private Shipping(Shipping.Builder builder) { 12 | super(builder); 13 | deliverySpeed = builder.deliverySpeed; 14 | } 15 | 16 | /** 17 | * {@code Builder} creates instances of {@code Shipping} from values set by the builder's 18 | * methods. 19 | */ 20 | public static final class Builder extends AbstractLocation.Builder { 21 | DeliverySpeed deliverySpeed; 22 | 23 | /** 24 | * @param speed The shipping delivery speed for the order. 25 | * @return The builder object. 26 | */ 27 | public Shipping.Builder deliverySpeed(DeliverySpeed speed) { 28 | deliverySpeed = speed; 29 | return this; 30 | } 31 | 32 | /** 33 | * @return An instance of {@code Shipping} created from the fields set on this builder. 34 | */ 35 | @Override 36 | public Shipping build() { 37 | return new Shipping(this); 38 | } 39 | } 40 | 41 | /** 42 | * @return The shipping delivery speed for the order. 43 | */ 44 | @JsonProperty("delivery_speed") 45 | public DeliverySpeed deliverySpeed() { 46 | return deliverySpeed; 47 | } 48 | 49 | /** 50 | * Enumerated delivery speeds. 51 | */ 52 | public enum DeliverySpeed { 53 | /** 54 | * Same day 55 | */ 56 | SAME_DAY, 57 | /** 58 | * Overnight 59 | */ 60 | OVERNIGHT, 61 | /** 62 | * Expedited 63 | */ 64 | EXPEDITED, 65 | /** 66 | * Standard 67 | */ 68 | STANDARD; 69 | 70 | /** 71 | * @return a string representation of the object. 72 | */ 73 | public String toString() { 74 | return this.name().toLowerCase(); 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/test/java/com/maxmind/minfraud/response/EmailTest.java: -------------------------------------------------------------------------------- 1 | package com.maxmind.minfraud.response; 2 | 3 | import static org.junit.jupiter.api.Assertions.assertEquals; 4 | import static org.junit.jupiter.api.Assertions.assertFalse; 5 | import static org.junit.jupiter.api.Assertions.assertNotNull; 6 | import static org.junit.jupiter.api.Assertions.assertNull; 7 | import static org.junit.jupiter.api.Assertions.assertTrue; 8 | 9 | import com.fasterxml.jackson.jr.ob.JSON; 10 | import java.time.LocalDate; 11 | import org.junit.jupiter.api.Test; 12 | 13 | public class EmailTest extends AbstractOutputTest { 14 | 15 | @Test 16 | public void testEmail() throws Exception { 17 | var email = this.deserialize( 18 | Email.class, 19 | JSON.std 20 | .composeString() 21 | .startObject() 22 | .startObjectField("domain") 23 | .put("first_seen", "2014-02-03") 24 | .end() 25 | .put("is_disposable", false) 26 | .put("is_free", false) 27 | .put("is_high_risk", true) 28 | .put("first_seen", "2017-01-02") 29 | .end() 30 | .finish() 31 | ); 32 | 33 | assertEquals(LocalDate.parse("2014-02-03"), email.domain().firstSeen()); 34 | assertFalse(email.isDisposable()); 35 | assertFalse(email.isFree()); 36 | assertTrue(email.isHighRisk()); 37 | assertEquals("2017-01-02", email.firstSeen()); 38 | assertEquals(LocalDate.parse("2017-01-02"), email.getFirstSeenDate()); 39 | } 40 | 41 | @Test 42 | public void testEmailWithoutFirstSeen() throws Exception { 43 | var email = this.deserialize( 44 | Email.class, 45 | JSON.std 46 | .composeString() 47 | .startObject() 48 | .put("is_free", false) 49 | .put("is_high_risk", true) 50 | .end() 51 | .finish() 52 | ); 53 | 54 | assertNotNull(email.domain()); 55 | assertNull(email.isDisposable()); 56 | assertFalse(email.isFree()); 57 | assertTrue(email.isHighRisk()); 58 | assertNull(email.firstSeen()); 59 | assertNull(email.getFirstSeenDate()); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/test/java/com/maxmind/minfraud/request/EventTest.java: -------------------------------------------------------------------------------- 1 | package com.maxmind.minfraud.request; 2 | 3 | import static org.junit.jupiter.api.Assertions.assertEquals; 4 | 5 | import com.maxmind.minfraud.request.Event.Builder; 6 | import com.maxmind.minfraud.request.Event.Party; 7 | import com.maxmind.minfraud.request.Event.Type; 8 | import java.time.ZonedDateTime; 9 | import java.util.Date; 10 | import org.junit.jupiter.api.Test; 11 | 12 | public class EventTest { 13 | 14 | @Test 15 | public void testParty() { 16 | var event = new Builder().party(Party.AGENT).build(); 17 | assertEquals(Party.AGENT, event.party()); 18 | 19 | event = new Builder().party(Party.CUSTOMER).build(); 20 | assertEquals(Party.CUSTOMER, event.party()); 21 | } 22 | 23 | @Test 24 | public void testTransactionId() { 25 | var event = new Builder().transactionId("t12").build(); 26 | assertEquals("t12", event.transactionId()); 27 | } 28 | 29 | @Test 30 | public void testShopId() { 31 | var event = new Builder().shopId("s12").build(); 32 | assertEquals("s12", event.shopId()); 33 | } 34 | 35 | @Test 36 | public void testTimeWithDate() { 37 | var date = new Date(); 38 | var event = new Builder().time(date).build(); 39 | assertEquals(date, event.time()); 40 | } 41 | 42 | @Test 43 | public void testTimeWithZonedDateTime() { 44 | var date = ZonedDateTime.now(); 45 | var event = new Builder().time(date).build(); 46 | assertEquals(date, event.dateTime()); 47 | } 48 | 49 | @Test 50 | public void testType() { 51 | var event = new Builder().type(Type.ACCOUNT_CREATION).build(); 52 | assertEquals(Type.ACCOUNT_CREATION, event.type()); 53 | 54 | event = new Builder().type(Type.PAYOUT_CHANGE).build(); 55 | assertEquals(Type.PAYOUT_CHANGE, event.type()); 56 | 57 | event = new Builder().type(Type.CREDIT_APPLICATION).build(); 58 | assertEquals(Type.CREDIT_APPLICATION, event.type()); 59 | 60 | event = new Builder().type(Type.FUND_TRANSFER).build(); 61 | assertEquals(Type.FUND_TRANSFER, event.type()); 62 | 63 | event = new Builder().type(Type.SIM_SWAP).build(); 64 | assertEquals(Type.SIM_SWAP, event.type()); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/main/java/com/maxmind/minfraud/response/EmailDomainVisit.java: -------------------------------------------------------------------------------- 1 | package com.maxmind.minfraud.response; 2 | 3 | import com.fasterxml.jackson.annotation.JsonProperty; 4 | import com.maxmind.minfraud.JsonSerializable; 5 | import java.time.LocalDate; 6 | 7 | /** 8 | * This class contains information about an automated visit to the email domain. 9 | * 10 | * @param hasRedirect Whether the domain redirects to another URL. This field is only present if 11 | * the value is true. 12 | * @param lastVisitedOn The date when the automated visit was last completed. 13 | * @param status The status of the domain based on the automated visit. Possible values are: 14 | * live, dns_error, network_error, http_error, parked, pre_development. 15 | */ 16 | public record EmailDomainVisit( 17 | @JsonProperty("has_redirect") 18 | Boolean hasRedirect, 19 | 20 | @JsonProperty("last_visited_on") 21 | LocalDate lastVisitedOn, 22 | 23 | @JsonProperty("status") 24 | Status status 25 | ) implements JsonSerializable { 26 | 27 | /** 28 | * The status of an email domain based on an automated visit. 29 | */ 30 | public enum Status { 31 | /** 32 | * The domain is live and responding normally. 33 | */ 34 | LIVE, 35 | 36 | /** 37 | * A DNS error occurred when attempting to visit the domain. 38 | */ 39 | DNS_ERROR, 40 | 41 | /** 42 | * A network error occurred when attempting to visit the domain. 43 | */ 44 | NETWORK_ERROR, 45 | 46 | /** 47 | * An HTTP error occurred when attempting to visit the domain. 48 | */ 49 | HTTP_ERROR, 50 | 51 | /** 52 | * The domain is parked. 53 | */ 54 | PARKED, 55 | 56 | /** 57 | * The domain is in pre-development. 58 | */ 59 | PRE_DEVELOPMENT; 60 | 61 | /** 62 | * @return a string representation of the status in lowercase with underscores. 63 | */ 64 | @Override 65 | public String toString() { 66 | return name().toLowerCase(); 67 | } 68 | } 69 | 70 | /** 71 | * Constructs an instance of {@code EmailDomainVisit} with no data. 72 | */ 73 | public EmailDomainVisit() { 74 | this(null, null, null); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/main/java/com/maxmind/minfraud/response/RiskScoreReason.java: -------------------------------------------------------------------------------- 1 | package com.maxmind.minfraud.response; 2 | 3 | import com.fasterxml.jackson.annotation.JsonProperty; 4 | import com.maxmind.minfraud.JsonSerializable; 5 | import java.util.List; 6 | 7 | /** 8 | * This class represents a risk score multiplier and reasons for that multiplier. 9 | * 10 | * @param multiplier The factor by which the risk score is increased (if the value is greater than 11 | * 1) or decreased (if the value is less than 1) for given risk reason(s). 12 | * Multipliers greater than 1.5 and less than 0.66 are considered significant and 13 | * lead to risk reason(s) being present. 14 | * @param reasons An unmodifiable list containing objects that describe one of the reasons for 15 | * the multiplier. This will be an empty list if there are no reasons. 16 | */ 17 | public record RiskScoreReason( 18 | @JsonProperty("multiplier") 19 | Double multiplier, 20 | 21 | @JsonProperty("reasons") 22 | List reasons 23 | ) implements JsonSerializable { 24 | 25 | /** 26 | * Compact canonical constructor that ensures immutability. 27 | */ 28 | public RiskScoreReason { 29 | reasons = reasons != null ? List.copyOf(reasons) : List.of(); 30 | } 31 | 32 | /** 33 | * @return The factor by which the risk score is increased (if the value is greater than 1) 34 | * or decreased (if the value is less than 1) for given risk reason(s). 35 | * Multipliers greater than 1.5 and less than 0.66 are considered significant 36 | * and lead to risk reason(s) being present. 37 | * @deprecated Use {@link #multiplier()} instead. This method will be removed in 5.0.0. 38 | */ 39 | @Deprecated(since = "4.0.0", forRemoval = true) 40 | @JsonProperty("multiplier") 41 | public Double getMultiplier() { 42 | return multiplier(); 43 | } 44 | 45 | /** 46 | * @return An unmodifiable list containing objects that describe one of the reasons for 47 | * the multiplier. This will be an empty list if there are no reasons. 48 | * @deprecated Use {@link #reasons()} instead. This method will be removed in 5.0.0. 49 | */ 50 | @Deprecated(since = "4.0.0", forRemoval = true) 51 | @JsonProperty("reasons") 52 | public List getReasons() { 53 | return reasons(); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/test/java/com/maxmind/minfraud/request/CustomInputsTest.java: -------------------------------------------------------------------------------- 1 | package com.maxmind.minfraud.request; 2 | 3 | import static org.junit.jupiter.api.Assertions.assertEquals; 4 | import static org.junit.jupiter.api.Assertions.assertThrows; 5 | 6 | import java.util.Map; 7 | import org.junit.jupiter.api.Test; 8 | 9 | public class CustomInputsTest { 10 | @Test 11 | public void TestPuttingTypes() { 12 | Map inputs = new CustomInputs.Builder() 13 | .put("string_input_1", "test string") 14 | .put("int_input", 19) 15 | .put("long_input", 12L) 16 | .put("float_input", 3.2f) 17 | .put("double_input", 32.123d) 18 | .put("bool_input", true) 19 | .build().inputs(); 20 | 21 | assertEquals("test string", inputs.get("string_input_1")); 22 | assertEquals(19, inputs.get("int_input")); 23 | assertEquals(12L, inputs.get("long_input")); 24 | assertEquals(3.2f, inputs.get("float_input")); 25 | assertEquals(32.123d, inputs.get("double_input")); 26 | assertEquals(true, inputs.get("bool_input")); 27 | } 28 | 29 | @Test 30 | public void testInvalidKey() { 31 | assertThrows( 32 | IllegalArgumentException.class, 33 | () -> new CustomInputs.Builder().put("InvalidKey", 1) 34 | ); 35 | } 36 | 37 | @Test 38 | public void testStringThatIsTooLong() { 39 | assertThrows( 40 | IllegalArgumentException.class, 41 | () -> new CustomInputs.Builder().put("string", 42 | new String(new char[256]).replace('\0', 'x')) 43 | ); 44 | } 45 | 46 | @Test 47 | public void testStringWithNewLine() { 48 | assertThrows( 49 | IllegalArgumentException.class, 50 | () -> new CustomInputs.Builder().put("string", "test\n") 51 | ); 52 | } 53 | 54 | @Test 55 | public void testTooLargeLong() { 56 | assertThrows( 57 | IllegalArgumentException.class, 58 | () -> new CustomInputs.Builder().put("long", 10_000_000_000_000L) 59 | ); 60 | } 61 | 62 | @Test 63 | public void testTooSmallLong() { 64 | assertThrows( 65 | IllegalArgumentException.class, 66 | () -> new CustomInputs.Builder().put("long", -10_000_000_000_000L) 67 | ); 68 | } 69 | 70 | @Test 71 | public void testTooLargeDouble() { 72 | assertThrows( 73 | IllegalArgumentException.class, 74 | () -> new CustomInputs.Builder().put("double", 1e13) 75 | ); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/main/java/com/maxmind/minfraud/response/Email.java: -------------------------------------------------------------------------------- 1 | package com.maxmind.minfraud.response; 2 | 3 | import com.fasterxml.jackson.annotation.JsonIgnore; 4 | import com.fasterxml.jackson.annotation.JsonProperty; 5 | import com.maxmind.minfraud.JsonSerializable; 6 | import java.time.LocalDate; 7 | 8 | /** 9 | * This class contains minFraud response data related to the email address. 10 | * 11 | * @param domain The {@code EmailDomain} model object. 12 | * @param isDisposable Whether it is a disposable email. 13 | * @param isFree Whether it is a free email. 14 | * @param isHighRisk Whether it is a high risk email. 15 | * @param firstSeen A date string (e.g. 2017-04-24) to identify the date an email address was 16 | * first seen by MaxMind. This is expressed using the ISO 8601 date format. 17 | */ 18 | public record Email( 19 | @JsonProperty("domain") 20 | EmailDomain domain, 21 | 22 | @JsonProperty("is_disposable") 23 | Boolean isDisposable, 24 | 25 | @JsonProperty("is_free") 26 | Boolean isFree, 27 | 28 | @JsonProperty("is_high_risk") 29 | Boolean isHighRisk, 30 | 31 | @JsonProperty("first_seen") 32 | String firstSeen 33 | ) implements JsonSerializable { 34 | 35 | /** 36 | * Compact canonical constructor that sets defaults for null values. 37 | */ 38 | public Email { 39 | domain = domain != null ? domain : new EmailDomain(); 40 | } 41 | 42 | /** 43 | * Constructs an instance of {@code Email} with no data. 44 | */ 45 | public Email() { 46 | this(null, null, null, null, null); 47 | } 48 | 49 | /** 50 | * @return The {@code EmailDomain} model object. 51 | * @deprecated Use {@link #domain()} instead. This method will be removed in 5.0.0. 52 | */ 53 | @Deprecated(since = "4.0.0", forRemoval = true) 54 | @JsonProperty("domain") 55 | public EmailDomain getDomain() { 56 | return domain(); 57 | } 58 | 59 | /** 60 | * @return A date string (e.g. 2017-04-24) to identify the date an email address was first seen 61 | * by MaxMind. This is expressed using the ISO 8601 date format. 62 | * @deprecated Use {@link #firstSeen()} instead. This method will be removed in 5.0.0. 63 | */ 64 | @Deprecated(since = "4.0.0", forRemoval = true) 65 | @JsonProperty("first_seen") 66 | public String getFirstSeen() { 67 | return firstSeen(); 68 | } 69 | 70 | /** 71 | * @return A date to identify the date an email address was first seen by MaxMind. 72 | */ 73 | @JsonIgnore 74 | public LocalDate getFirstSeenDate() { 75 | if (firstSeen == null) { 76 | return null; 77 | } 78 | return LocalDate.parse(firstSeen); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/test/resources/test-data/full-request.json: -------------------------------------------------------------------------------- 1 | { 2 | "event": { 3 | "party": "customer", 4 | "transaction_id": "txn3134133", 5 | "shop_id": "s2123", 6 | "time": "2012-04-12T23:20:50.52Z", 7 | "type": "purchase" 8 | }, 9 | "account": { 10 | "user_id": "3132", 11 | "username_md5": "570a90bfbf8c7eab5dc5d4e26832d5b1" 12 | }, 13 | "email": { 14 | "address": "test@maxmind.com", 15 | "domain": "maxmind.com" 16 | }, 17 | "billing": { 18 | "first_name": "First", 19 | "last_name": "Last", 20 | "company": "Company", 21 | "address": "101 Address Rd.", 22 | "address_2": "Unit 5", 23 | "city": "City of Thorns", 24 | "region": "CT", 25 | "country": "US", 26 | "postal": "06510", 27 | "phone_number": "123-456-7890", 28 | "phone_country_code": "1" 29 | }, 30 | "shipping": { 31 | "first_name": "ShipFirst", 32 | "last_name": "ShipLast", 33 | "company": "ShipCo", 34 | "address": "322 Ship Addr. Ln.", 35 | "address_2": "St. 43", 36 | "city": "Nowhere", 37 | "region": "OK", 38 | "country": "US", 39 | "postal": "73003", 40 | "phone_number": "123-456-0000", 41 | "phone_country_code": "1", 42 | "delivery_speed": "same_day" 43 | }, 44 | "payment": { 45 | "method": "card", 46 | "processor": "stripe", 47 | "was_authorized": false, 48 | "decline_code": "invalid number" 49 | }, 50 | "credit_card": { 51 | "issuer_id_number": "411111", 52 | "last_digits": "7643", 53 | "bank_name": "Bank of No Hope", 54 | "bank_phone_country_code": "1", 55 | "bank_phone_number": "123-456-1234", 56 | "avs_result": "Y", 57 | "cvv_result": "N", 58 | "token": "123456abc1234", 59 | "was_3d_secure_successful": true 60 | }, 61 | "custom_inputs": { 62 | "float_input": 12.1, 63 | "integer_input": 3123, 64 | "string_input": "This is a string input.", 65 | "boolean_input": true 66 | }, 67 | "order": { 68 | "amount": 323.21, 69 | "currency": "USD", 70 | "discount_code": "FIRST", 71 | "affiliate_id": "af12", 72 | "subaffiliate_id": "saf42", 73 | "is_gift": true, 74 | "has_gift_message": false, 75 | "referrer_uri": "http://www.amazon.com/" 76 | }, 77 | "shopping_cart": [ 78 | { 79 | "category": "pets", 80 | "item_id": "ad23232", 81 | "quantity": 2, 82 | "price": 20.43 83 | }, 84 | { 85 | "category": "beauty", 86 | "item_id": "bst112", 87 | "quantity": 1, 88 | "price": 100.0 89 | } 90 | ], 91 | "device": { 92 | "ip_address": "152.216.7.110", 93 | "user_agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2272.89 Safari/537.36", 94 | "session_age": 3600.5, 95 | "session_id": "foobar", 96 | "accept_language": "en-US,en;q=0.8" 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /src/test/resources/test-data/full-request-email-md5.json: -------------------------------------------------------------------------------- 1 | { 2 | "event": { 3 | "party": "customer", 4 | "transaction_id": "txn3134133", 5 | "shop_id": "s2123", 6 | "time": "2012-04-12T23:20:50.52Z", 7 | "type": "purchase" 8 | }, 9 | "account": { 10 | "user_id": "3132", 11 | "username_md5": "570a90bfbf8c7eab5dc5d4e26832d5b1" 12 | }, 13 | "email": { 14 | "address": "977577b140bfb7c516e4746204fbdb01", 15 | "domain": "maxmind.com" 16 | }, 17 | "billing": { 18 | "first_name": "First", 19 | "last_name": "Last", 20 | "company": "Company", 21 | "address": "101 Address Rd.", 22 | "address_2": "Unit 5", 23 | "city": "City of Thorns", 24 | "region": "CT", 25 | "country": "US", 26 | "postal": "06510", 27 | "phone_number": "123-456-7890", 28 | "phone_country_code": "1" 29 | }, 30 | "shipping": { 31 | "first_name": "ShipFirst", 32 | "last_name": "ShipLast", 33 | "company": "ShipCo", 34 | "address": "322 Ship Addr. Ln.", 35 | "address_2": "St. 43", 36 | "city": "Nowhere", 37 | "region": "OK", 38 | "country": "US", 39 | "postal": "73003", 40 | "phone_number": "123-456-0000", 41 | "phone_country_code": "1", 42 | "delivery_speed": "same_day" 43 | }, 44 | "payment": { 45 | "method": "card", 46 | "processor": "stripe", 47 | "was_authorized": false, 48 | "decline_code": "invalid number" 49 | }, 50 | "credit_card": { 51 | "issuer_id_number": "411111", 52 | "last_digits": "7643", 53 | "bank_name": "Bank of No Hope", 54 | "bank_phone_country_code": "1", 55 | "bank_phone_number": "123-456-1234", 56 | "avs_result": "Y", 57 | "cvv_result": "N", 58 | "token": "123456abc1234", 59 | "was_3d_secure_successful": true 60 | }, 61 | "custom_inputs": { 62 | "float_input": 12.1, 63 | "integer_input": 3123, 64 | "string_input": "This is a string input.", 65 | "boolean_input": true 66 | }, 67 | "order": { 68 | "amount": 323.21, 69 | "currency": "USD", 70 | "discount_code": "FIRST", 71 | "affiliate_id": "af12", 72 | "subaffiliate_id": "saf42", 73 | "is_gift": true, 74 | "has_gift_message": false, 75 | "referrer_uri": "http://www.amazon.com/" 76 | }, 77 | "shopping_cart": [ 78 | { 79 | "category": "pets", 80 | "item_id": "ad23232", 81 | "quantity": 2, 82 | "price": 20.43 83 | }, 84 | { 85 | "category": "beauty", 86 | "item_id": "bst112", 87 | "quantity": 1, 88 | "price": 100.0 89 | } 90 | ], 91 | "device": { 92 | "ip_address": "152.216.7.110", 93 | "user_agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2272.89 Safari/537.36", 94 | "session_age": 3600.5, 95 | "session_id": "foobar", 96 | "accept_language": "en-US,en;q=0.8" 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /src/main/java/com/maxmind/minfraud/request/Account.java: -------------------------------------------------------------------------------- 1 | package com.maxmind.minfraud.request; 2 | 3 | import com.fasterxml.jackson.annotation.JsonProperty; 4 | import com.maxmind.minfraud.AbstractModel; 5 | import java.math.BigInteger; 6 | import java.nio.charset.StandardCharsets; 7 | import java.security.MessageDigest; 8 | import java.security.NoSuchAlgorithmException; 9 | 10 | /** 11 | * Account related data for the minFraud request 12 | */ 13 | public final class Account extends AbstractModel { 14 | private final String userId; 15 | private final String usernameMd5; 16 | 17 | private Account(Account.Builder builder) { 18 | userId = builder.userId; 19 | usernameMd5 = builder.usernameMd5; 20 | } 21 | 22 | /** 23 | * {@code Builder} creates instances of {@code Account} from values set by the builder's 24 | * methods. 25 | */ 26 | public static final class Builder { 27 | String userId; 28 | String usernameMd5; 29 | 30 | /** 31 | * @param id A unique user ID associated with the end-user in your system. If your system 32 | * allows the login name for the account to be changed, this should not be the 33 | * login name for the account, but rather should be an internal ID that does not 34 | * change. This is not your MaxMind user ID. 35 | * @return The builder object. 36 | */ 37 | public Account.Builder userId(String id) { 38 | this.userId = id; 39 | return this; 40 | } 41 | 42 | /** 43 | * @param username The username associated with the account. This is 44 | * not the MD5 of username. This method 45 | * automatically runs {@code DigestUtils.md5Hex} on the string passed to 46 | * it. 47 | * @return The builder object. 48 | */ 49 | public Account.Builder username(String username) { 50 | try { 51 | MessageDigest d = MessageDigest.getInstance("MD5"); 52 | d.update(username.getBytes(StandardCharsets.UTF_8)); 53 | BigInteger i = new BigInteger(1, d.digest()); 54 | this.usernameMd5 = String.format("%032x", i); 55 | return this; 56 | } catch (NoSuchAlgorithmException e) { 57 | throw new RuntimeException("No MD5 algorithm for MessageDigest!", e); 58 | } 59 | } 60 | 61 | /** 62 | * @return An instance of {@code Account} created from the fields set on this builder. 63 | */ 64 | public Account build() { 65 | return new Account(this); 66 | } 67 | } 68 | 69 | /** 70 | * @return The user ID. 71 | */ 72 | @JsonProperty("user_id") 73 | public String userId() { 74 | return userId; 75 | } 76 | 77 | /** 78 | * @return The MD5 of the username passed to the builder. 79 | */ 80 | @JsonProperty("username_md5") 81 | public String usernameMd5() { 82 | return usernameMd5; 83 | } 84 | } -------------------------------------------------------------------------------- /src/main/java/com/maxmind/minfraud/response/EmailDomain.java: -------------------------------------------------------------------------------- 1 | package com.maxmind.minfraud.response; 2 | 3 | import com.fasterxml.jackson.annotation.JsonProperty; 4 | import com.maxmind.minfraud.JsonSerializable; 5 | import java.time.LocalDate; 6 | 7 | /** 8 | * This class contains minFraud response data related to the email domain. 9 | * 10 | * @param classification A classification of the email domain. Possible values are: business, 11 | * education, government, isp_email. 12 | * @param firstSeen The date an email domain was first seen by MaxMind. 13 | * @param risk A risk score associated with the email domain, ranging from 0.01 to 99. 14 | * Higher scores indicate higher risk. 15 | * @param visit An {@code EmailDomainVisit} object containing information about an 16 | * automated visit to the email domain. 17 | * @param volume The activity on the email domain across the minFraud network, expressed in 18 | * sightings per million. This value ranges from 0.001 to 1,000,000. 19 | */ 20 | public record EmailDomain( 21 | @JsonProperty("classification") 22 | Classification classification, 23 | 24 | @JsonProperty("first_seen") 25 | LocalDate firstSeen, 26 | 27 | @JsonProperty("risk") 28 | Double risk, 29 | 30 | @JsonProperty("visit") 31 | EmailDomainVisit visit, 32 | 33 | @JsonProperty("volume") 34 | Double volume 35 | ) implements JsonSerializable { 36 | 37 | /** 38 | * The classification of an email domain. 39 | */ 40 | public enum Classification { 41 | /** 42 | * A business email domain. 43 | */ 44 | BUSINESS, 45 | 46 | /** 47 | * An educational institution email domain. 48 | */ 49 | EDUCATION, 50 | 51 | /** 52 | * A government email domain. 53 | */ 54 | GOVERNMENT, 55 | 56 | /** 57 | * An ISP-provided email domain (e.g., gmail.com, yahoo.com). 58 | */ 59 | ISP_EMAIL; 60 | 61 | /** 62 | * @return a string representation of the classification in lowercase with underscores. 63 | */ 64 | @Override 65 | public String toString() { 66 | return name().toLowerCase(); 67 | } 68 | } 69 | 70 | /** 71 | * Compact canonical constructor that sets defaults for null values. 72 | */ 73 | public EmailDomain { 74 | visit = visit != null ? visit : new EmailDomainVisit(); 75 | } 76 | 77 | /** 78 | * Constructs an instance of {@code EmailDomain} with no data. 79 | */ 80 | public EmailDomain() { 81 | this(null, null, null, null, null); 82 | } 83 | 84 | /** 85 | * @return A date to identify the date an email domain was first seen by MaxMind. 86 | * @deprecated Use {@link #firstSeen()} instead. This method will be removed in 5.0.0. 87 | */ 88 | @Deprecated(since = "4.0.0", forRemoval = true) 89 | @JsonProperty("first_seen") 90 | public LocalDate getFirstSeen() { 91 | return firstSeen(); 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/main/java/com/maxmind/minfraud/response/Phone.java: -------------------------------------------------------------------------------- 1 | package com.maxmind.minfraud.response; 2 | 3 | import com.fasterxml.jackson.annotation.JsonProperty; 4 | import com.maxmind.minfraud.JsonSerializable; 5 | 6 | /** 7 | * This class contains minFraud response data related to the phone number. 8 | * 9 | * @param country The two-character ISO 3166-1 country code for the country associated 10 | * with the phone number. 11 | * @param isVoip Whether the number is VoIP. 12 | * @param matchesPostal Whether the phone number matches the postal code. 13 | * @param networkOperator The name of the original network operator associated with the phone 14 | * number. This field does not reflect phone numbers that have been ported 15 | * from the original operator to another, nor does it identify mobile 16 | * virtual network operators. 17 | * @param numberType One of the following values: {@code fixed} or {@code mobile}. 18 | * Additional values may be added in the future. 19 | */ 20 | public record Phone( 21 | @JsonProperty("country") 22 | String country, 23 | 24 | @JsonProperty("is_voip") 25 | Boolean isVoip, 26 | 27 | @JsonProperty("matches_postal") 28 | Boolean matchesPostal, 29 | 30 | @JsonProperty("network_operator") 31 | String networkOperator, 32 | 33 | @JsonProperty("number_type") 34 | String numberType 35 | ) implements JsonSerializable { 36 | 37 | /** 38 | * Constructs an instance of {@code Phone} with no data. 39 | */ 40 | public Phone() { 41 | this(null, null, null, null, null); 42 | } 43 | 44 | /** 45 | * @return The two-character ISO 3166-1 country code for the country associated with the phone 46 | * number. 47 | * @deprecated Use {@link #country()} instead. This method will be removed in 5.0.0. 48 | */ 49 | @Deprecated(since = "4.0.0", forRemoval = true) 50 | @JsonProperty("country") 51 | public String getCountry() { 52 | return country(); 53 | } 54 | 55 | /** 56 | * @return The name of the original network operator associated with the phone number. This 57 | * field does not reflect phone numbers that have been ported from the original operator to 58 | * another, nor does it identify mobile virtual network operators. 59 | * @deprecated Use {@link #networkOperator()} instead. This method will be removed in 5.0.0. 60 | */ 61 | @Deprecated(since = "4.0.0", forRemoval = true) 62 | @JsonProperty("network_operator") 63 | public String getNetworkOperator() { 64 | return networkOperator(); 65 | } 66 | 67 | /** 68 | * @return One of the following values: {@code fixed} or {@code mobile}. Additional values may 69 | * be added in the future. 70 | * @deprecated Use {@link #numberType()} instead. This method will be removed in 5.0.0. 71 | */ 72 | @Deprecated(since = "4.0.0", forRemoval = true) 73 | @JsonProperty("number_type") 74 | public String getNumberType() { 75 | return numberType(); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/main/java/com/maxmind/minfraud/response/Disposition.java: -------------------------------------------------------------------------------- 1 | package com.maxmind.minfraud.response; 2 | 3 | import com.fasterxml.jackson.annotation.JsonProperty; 4 | import com.maxmind.minfraud.JsonSerializable; 5 | 6 | /** 7 | * This class contains the disposition set by custom rules. 8 | * 9 | * @param action A {@code String} with the action to take on the transaction as defined by your 10 | * custom rules. The current set of values are "accept", "manual_review", "reject" 11 | * and "test". If you do not have custom rules set up, {@code null} will be 12 | * returned. 13 | * @param reason A {@code String} with the reason for the action. The current possible values 14 | * are "custom_rule" and "default". If you do not have custom rules set up, 15 | * {@code null} will be returned. 16 | * @param ruleLabel A {@code String} with the label of the custom rule that was triggered. If you 17 | * do not have custom rules set up, the triggered custom rule does not have a 18 | * label, or no custom rule was triggered, {@code null} will be returned. 19 | */ 20 | public record Disposition( 21 | @JsonProperty("action") 22 | String action, 23 | 24 | @JsonProperty("reason") 25 | String reason, 26 | 27 | @JsonProperty("rule_label") 28 | String ruleLabel 29 | ) implements JsonSerializable { 30 | 31 | /** 32 | * Constructs an instance of {@code Disposition} with no data. 33 | */ 34 | public Disposition() { 35 | this(null, null, null); 36 | } 37 | 38 | /** 39 | * @return A {@code String} with the action to take on the transaction as defined by your custom 40 | * rules. The current set of values are "accept", "manual_review", "reject" and "test". If 41 | * you do not have custom rules set up, {@code null} will be returned. 42 | * @deprecated Use {@link #action()} instead. This method will be removed in 5.0.0. 43 | */ 44 | @Deprecated(since = "4.0.0", forRemoval = true) 45 | @JsonProperty("action") 46 | public String getAction() { 47 | return action(); 48 | } 49 | 50 | /** 51 | * @return A {@code String} with the reason for the action. The current possible values are 52 | * "custom_rule" and "default". If you do not have custom rules set up, {@code null} will be 53 | * returned. 54 | * @deprecated Use {@link #reason()} instead. This method will be removed in 5.0.0. 55 | */ 56 | @Deprecated(since = "4.0.0", forRemoval = true) 57 | @JsonProperty("reason") 58 | public String getReason() { 59 | return reason(); 60 | } 61 | 62 | /** 63 | * @return A {@code String} with the label of the custom rule that was triggered. If you do not 64 | * have custom rules set up, the triggered custom rule does not have a label, or no custom 65 | * rule was triggered, {@code null} will be returned. 66 | * @deprecated Use {@link #ruleLabel()} instead. This method will be removed in 5.0.0. 67 | */ 68 | @Deprecated(since = "4.0.0", forRemoval = true) 69 | @JsonProperty("rule_label") 70 | public String getRuleLabel() { 71 | return ruleLabel(); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/main/java/com/maxmind/minfraud/response/BillingAddress.java: -------------------------------------------------------------------------------- 1 | package com.maxmind.minfraud.response; 2 | 3 | import com.fasterxml.jackson.annotation.JsonProperty; 4 | import com.maxmind.minfraud.JsonSerializable; 5 | 6 | /** 7 | * This class contains minFraud response data related to the billing address. 8 | * 9 | * @param distanceToIpLocation The distance in kilometers from the address to the IP location. 10 | * This will be null if there is no value in the response. 11 | * @param isInIpCountry This returns true if the address is in the IP country. It is false 12 | * when the address is not in the IP country. If the address could not 13 | * be parsed or was not provided or the IP address could not be 14 | * geolocated, then null will be returned. 15 | * @param isPostalInCity This will return true if the postal code provided with the address 16 | * is in the city for the address. It will return false when the postal 17 | * code is not in the city. If the address was not provided or could not 18 | * be parsed, null will be returned. 19 | * @param latitude The latitude associated with the address. This will be null if there 20 | * is no value in the response. 21 | * @param longitude The longitude associated with the address. This will be null if there 22 | * is no value in the response. 23 | */ 24 | public record BillingAddress( 25 | @JsonProperty("distance_to_ip_location") 26 | Integer distanceToIpLocation, 27 | 28 | @JsonProperty("is_in_ip_country") 29 | Boolean isInIpCountry, 30 | 31 | @JsonProperty("is_postal_in_city") 32 | Boolean isPostalInCity, 33 | 34 | @JsonProperty("latitude") 35 | Double latitude, 36 | 37 | @JsonProperty("longitude") 38 | Double longitude 39 | ) implements JsonSerializable { 40 | 41 | /** 42 | * Constructs an instance of {@code BillingAddress} with no data. 43 | */ 44 | public BillingAddress() { 45 | this(null, null, null, null, null); 46 | } 47 | 48 | /** 49 | * @return The latitude associated with the address. 50 | * @deprecated Use {@link #latitude()} instead. This method will be removed in 5.0.0. 51 | */ 52 | @Deprecated(since = "4.0.0", forRemoval = true) 53 | public Double getLatitude() { 54 | return latitude(); 55 | } 56 | 57 | /** 58 | * @return The longitude associated with the address. 59 | * @deprecated Use {@link #longitude()} instead. This method will be removed in 5.0.0. 60 | */ 61 | @Deprecated(since = "4.0.0", forRemoval = true) 62 | public Double getLongitude() { 63 | return longitude(); 64 | } 65 | 66 | /** 67 | * @return The distance in kilometers from the address to the IP location. 68 | * @deprecated Use {@link #distanceToIpLocation()} instead. This method will be removed in 69 | * 5.0.0. 70 | */ 71 | @Deprecated(since = "4.0.0", forRemoval = true) 72 | public Integer getDistanceToIpLocation() { 73 | return distanceToIpLocation(); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/test/java/com/maxmind/minfraud/request/OrderTest.java: -------------------------------------------------------------------------------- 1 | package com.maxmind.minfraud.request; 2 | 3 | import static org.junit.jupiter.api.Assertions.assertEquals; 4 | import static org.junit.jupiter.api.Assertions.assertThrows; 5 | import static org.junit.jupiter.api.Assertions.assertTrue; 6 | 7 | import com.maxmind.minfraud.request.Order.Builder; 8 | import java.math.BigDecimal; 9 | import java.net.URI; 10 | import org.junit.jupiter.api.Test; 11 | 12 | public class OrderTest { 13 | 14 | @Test 15 | public void testDoubleAmount() { 16 | var order = new Builder().amount(1.1).build(); 17 | assertEquals(BigDecimal.valueOf(1.1), order.amount()); 18 | } 19 | 20 | @Test 21 | public void testAmount() { 22 | var order = new Builder().amount(BigDecimal.valueOf(1.1)).build(); 23 | assertEquals(BigDecimal.valueOf(1.1), order.amount()); 24 | } 25 | 26 | @Test 27 | public void testCurrency() { 28 | var order = new Builder().currency("USD").build(); 29 | assertEquals("USD", order.currency()); 30 | } 31 | 32 | @Test 33 | public void testCurrencyWithDigits() { 34 | assertThrows( 35 | IllegalArgumentException.class, 36 | () -> new Builder().currency("US1").build() 37 | ); 38 | } 39 | 40 | @Test 41 | public void testCurrencyThatIsTooShort() { 42 | assertThrows( 43 | IllegalArgumentException.class, 44 | () -> new Builder().currency("US").build() 45 | ); 46 | } 47 | 48 | 49 | @Test 50 | public void testCurrencyThatIsTooLong() { 51 | assertThrows( 52 | IllegalArgumentException.class, 53 | () -> new Builder().currency("USDE").build() 54 | ); 55 | } 56 | 57 | @Test 58 | public void testCurrencyInWrongCase() { 59 | assertThrows( 60 | IllegalArgumentException.class, 61 | () -> new Builder().currency("usd").build() 62 | ); 63 | } 64 | 65 | @Test 66 | public void testDiscountCode() { 67 | var order = new Builder().discountCode("dsc").build(); 68 | assertEquals("dsc", order.discountCode()); 69 | } 70 | 71 | @Test 72 | public void testAffiliateId() { 73 | var order = new Builder().affiliateId("af").build(); 74 | assertEquals("af", order.affiliateId()); 75 | } 76 | 77 | @Test 78 | public void testSubaffiliateId() { 79 | var order = new Builder().subaffiliateId("saf").build(); 80 | assertEquals("saf", order.subaffiliateId()); 81 | } 82 | 83 | @Test 84 | public void testReferrerUri() throws Exception { 85 | var uri = new URI("http://www.mm.com/"); 86 | var order = new Builder().referrerUri(uri).build(); 87 | assertEquals(uri, order.referrerUri()); 88 | } 89 | 90 | @Test 91 | public void testIsGift() { 92 | var order = new Builder().isGift(true).build(); 93 | assertTrue(order.isGift()); 94 | } 95 | 96 | @Test 97 | public void testHasGiftMessage() { 98 | var order = new Builder().hasGiftMessage(true).build(); 99 | assertTrue(order.hasGiftMessage()); 100 | } 101 | } -------------------------------------------------------------------------------- /src/test/java/com/maxmind/minfraud/request/AbstractLocationTest.java: -------------------------------------------------------------------------------- 1 | package com.maxmind.minfraud.request; 2 | 3 | import static org.junit.jupiter.api.Assertions.assertEquals; 4 | import static org.junit.jupiter.api.Assertions.assertThrows; 5 | 6 | import com.maxmind.minfraud.request.AbstractLocation.Builder; 7 | import org.junit.jupiter.api.Test; 8 | 9 | public abstract class AbstractLocationTest { 10 | abstract Builder builder(); 11 | 12 | 13 | @Test 14 | public void testFirstName() { 15 | AbstractLocation loc = this.builder().firstName("frst").build(); 16 | assertEquals("frst", loc.firstName()); 17 | } 18 | 19 | @Test 20 | public void testLastName() { 21 | AbstractLocation loc = this.builder().lastName("last").build(); 22 | assertEquals("last", loc.lastName()); 23 | } 24 | 25 | @Test 26 | public void testCompany() { 27 | AbstractLocation loc = this.builder().company("company").build(); 28 | assertEquals("company", loc.company()); 29 | } 30 | 31 | @Test 32 | public void testAddress() { 33 | AbstractLocation loc = this.builder().address("addr").build(); 34 | assertEquals("addr", loc.address()); 35 | } 36 | 37 | @Test 38 | public void testAddress2() { 39 | AbstractLocation loc = this.builder().address2("addr2").build(); 40 | assertEquals("addr2", loc.address2()); 41 | } 42 | 43 | @Test 44 | public void testCity() { 45 | AbstractLocation loc = this.builder().city("Pdx").build(); 46 | assertEquals("Pdx", loc.city()); 47 | } 48 | 49 | @Test 50 | public void testRegion() { 51 | AbstractLocation loc = this.builder().region("MN").build(); 52 | assertEquals("MN", loc.region()); 53 | } 54 | 55 | @Test 56 | public void testCountry() { 57 | AbstractLocation loc = this.builder().country("US").build(); 58 | assertEquals("US", loc.country()); 59 | } 60 | 61 | @Test 62 | public void testCountryThatIsTooLong() { 63 | assertThrows( 64 | IllegalArgumentException.class, 65 | () -> this.builder().country("USA").build() 66 | ); 67 | } 68 | 69 | @Test 70 | public void testCountryWithNumbers() { 71 | assertThrows( 72 | IllegalArgumentException.class, 73 | () -> this.builder().country("U1").build() 74 | ); 75 | } 76 | 77 | @Test 78 | public void testCountryInWrongCase() { 79 | assertThrows( 80 | IllegalArgumentException.class, 81 | () -> this.builder().country("us").build() 82 | ); 83 | } 84 | 85 | @Test 86 | public void testPostal() { 87 | AbstractLocation loc = this.builder().postal("03231").build(); 88 | assertEquals("03231", loc.postal()); 89 | } 90 | 91 | @Test 92 | public void testPhoneNumber() { 93 | String phone = "321-321-3213"; 94 | AbstractLocation loc = this.builder().phoneNumber(phone).build(); 95 | assertEquals(phone, loc.phoneNumber()); 96 | } 97 | 98 | @Test 99 | public void testPhoneCountryCode() { 100 | AbstractLocation loc = this.builder().phoneCountryCode("1").build(); 101 | assertEquals("1", loc.phoneCountryCode()); 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/test/java/com/maxmind/minfraud/response/IpAddressTest.java: -------------------------------------------------------------------------------- 1 | package com.maxmind.minfraud.response; 2 | 3 | import static org.junit.jupiter.api.Assertions.assertEquals; 4 | import static org.junit.jupiter.api.Assertions.assertNotNull; 5 | import static org.junit.jupiter.api.Assertions.assertTrue; 6 | 7 | import com.fasterxml.jackson.jr.ob.JSON; 8 | import org.junit.jupiter.api.Test; 9 | 10 | public class IpAddressTest extends AbstractOutputTest { 11 | 12 | @Test 13 | public void testIpAddress() throws Exception { 14 | String time = "2015-04-19T12:59:23-01:00"; 15 | 16 | IpAddress address = this.deserialize( 17 | IpAddress.class, 18 | JSON.std 19 | .composeString() 20 | .startObject() 21 | .put("risk", 99) 22 | .startArrayField("risk_reasons") 23 | .startObject() 24 | .put("code", "ANONYMOUS_IP") 25 | .put("reason", "some reason") 26 | .end() 27 | .end() 28 | .startObjectField("country") 29 | .put("is_high_risk", true) 30 | .end() 31 | .startObjectField("location") 32 | .put("local_time", time) 33 | .end() 34 | .startObjectField("traits") 35 | .put("ip_address", "1.2.3.4") 36 | .put("is_anonymous", true) 37 | .put("is_anonymous_vpn", true) 38 | .put("is_hosting_provider", true) 39 | .put("is_public_proxy", true) 40 | .put("is_tor_exit_node", true) 41 | .put("mobile_country_code", "310") 42 | .put("mobile_network_code", "004") 43 | .put("network", "1.2.0.0/16") 44 | .end() 45 | .end() 46 | .finish() 47 | ); 48 | 49 | assertEquals(Double.valueOf(99), address.risk(), "IP risk"); 50 | assertEquals(time, address.location().localTime(), "correct local time"); 51 | assertEquals("1.2.0.0/16", address.traits().network().toString()); 52 | assertTrue(address.traits().isAnonymous(), "isAnonymous"); 53 | assertTrue(address.traits().isAnonymousVpn(), "isAnonymousVpn"); 54 | assertTrue(address.traits().isHostingProvider(), "isHostingProvider"); 55 | assertTrue(address.traits().isPublicProxy(), "isPublicProxy"); 56 | assertTrue(address.traits().isTorExitNode(), "isTorExitNode"); 57 | assertEquals( 58 | "310", 59 | address.traits().mobileCountryCode(), 60 | "mobile country code" 61 | ); 62 | assertEquals( 63 | "004", 64 | address.traits().mobileNetworkCode(), 65 | "mobile network code" 66 | ); 67 | assertEquals( 68 | "ANONYMOUS_IP", 69 | address.riskReasons().get(0).code(), 70 | "IP risk reason code" 71 | ); 72 | assertEquals( 73 | "some reason", 74 | address.riskReasons().get(0).reason(), 75 | "IP risk reason" 76 | ); 77 | } 78 | 79 | @Test 80 | public void testEmptyObject() throws Exception { 81 | IpAddress address = this.deserialize( 82 | IpAddress.class, 83 | "{}" 84 | ); 85 | 86 | assertNotNull(address.riskReasons()); 87 | } 88 | } -------------------------------------------------------------------------------- /src/test/java/com/maxmind/minfraud/response/FactorsResponseTest.java: -------------------------------------------------------------------------------- 1 | package com.maxmind.minfraud.response; 2 | 3 | import static org.junit.jupiter.api.Assertions.assertEquals; 4 | import static org.junit.jupiter.api.Assertions.assertFalse; 5 | import static org.junit.jupiter.api.Assertions.assertTrue; 6 | 7 | import com.fasterxml.jackson.jr.ob.JSON; 8 | import java.util.UUID; 9 | import org.junit.jupiter.api.Test; 10 | 11 | public class FactorsResponseTest extends AbstractOutputTest { 12 | 13 | @Test 14 | public void testFactors() throws Exception { 15 | String id = "b643d445-18b2-4b9d-bad4-c9c4366e402a"; 16 | FactorsResponse factors = this.deserialize( 17 | FactorsResponse.class, 18 | JSON.std 19 | .composeString() 20 | .startObject() 21 | .startObjectField("billing_phone") 22 | .put("is_voip", false) 23 | .put("matches_postal", true) 24 | .end() 25 | .startObjectField("shipping_phone") 26 | .put("is_voip", true) 27 | .put("matches_postal", false) 28 | .end() 29 | .put("funds_remaining", 1.20) 30 | .put("queries_remaining", 123) 31 | .put("id", id) 32 | .put("risk_score", 0.01) 33 | .startArrayField("risk_score_reasons") 34 | .startObject() 35 | .put("multiplier", 45) 36 | .startArrayField("reasons") 37 | .startObject() 38 | .put("code", "ANONYMOUS_IP") 39 | .put("reason", "Risk due to IP being an Anonymous IP") 40 | .end() 41 | .end() 42 | .end() 43 | .end() 44 | .end() 45 | .finish() 46 | ); 47 | 48 | assertTrue(factors.shippingPhone().isVoip(), "correct shipping phone isVoip"); 49 | assertFalse(factors.shippingPhone().matchesPostal(), "correct shipping phone matchesPostal"); 50 | assertFalse(factors.billingPhone().isVoip(), "correct billing phone isVoip"); 51 | assertTrue(factors.billingPhone().matchesPostal(), "correct billing phone matchesPostal"); 52 | 53 | assertEquals( 54 | Double.valueOf(1.20), 55 | factors.fundsRemaining(), 56 | "correct funnds remaining" 57 | ); 58 | assertEquals(UUID.fromString(id), factors.id(), "correct ID"); 59 | assertEquals( 60 | Integer.valueOf(123), 61 | factors.queriesRemaining(), 62 | "correct queries remaining" 63 | ); 64 | assertEquals( 65 | Double.valueOf(0.01), 66 | factors.riskScore(), 67 | "correct risk score" 68 | ); 69 | assertEquals(1, factors.riskScoreReasons().size()); 70 | assertEquals( 71 | Double.valueOf(45), 72 | factors.riskScoreReasons().get(0).multiplier(), 73 | "risk multiplier" 74 | ); 75 | assertEquals(1, factors.riskScoreReasons().get(0).reasons().size()); 76 | assertEquals( 77 | "ANONYMOUS_IP", 78 | factors.riskScoreReasons().get(0).reasons().get(0).code(), 79 | "risk reason code" 80 | ); 81 | assertEquals( 82 | "Risk due to IP being an Anonymous IP", 83 | factors.riskScoreReasons().get(0).reasons().get(0).reason(), 84 | "risk reason" 85 | ); 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/main/java/com/maxmind/minfraud/request/ShoppingCartItem.java: -------------------------------------------------------------------------------- 1 | package com.maxmind.minfraud.request; 2 | 3 | import com.fasterxml.jackson.annotation.JsonProperty; 4 | import com.maxmind.minfraud.AbstractModel; 5 | import java.math.BigDecimal; 6 | 7 | /** 8 | * An item in the shopping cart. 9 | */ 10 | public final class ShoppingCartItem extends AbstractModel { 11 | private final String category; 12 | private final String itemId; 13 | private final Integer quantity; 14 | private final BigDecimal price; 15 | 16 | private ShoppingCartItem(ShoppingCartItem.Builder builder) { 17 | category = builder.category; 18 | itemId = builder.itemId; 19 | quantity = builder.quantity; 20 | price = builder.price; 21 | } 22 | 23 | /** 24 | * {@code Builder} creates instances of {@code ShippingCartItem} from values set by the 25 | * builder's methods. 26 | */ 27 | public static final class Builder { 28 | String category; 29 | String itemId; 30 | Integer quantity; 31 | BigDecimal price; 32 | 33 | /** 34 | * @param category The category of the item. 35 | * @return The builder object. 36 | */ 37 | public ShoppingCartItem.Builder category(String category) { 38 | this.category = category; 39 | return this; 40 | } 41 | 42 | /** 43 | * @param id Your internal ID for the item 44 | * @return The builder object. 45 | */ 46 | public ShoppingCartItem.Builder itemId(String id) { 47 | itemId = id; 48 | return this; 49 | } 50 | 51 | /** 52 | * @param quantity The quantity of the item in the shopping cart. 53 | * @return The builder object. 54 | * @throws IllegalArgumentException when quantity is not positive. 55 | */ 56 | public ShoppingCartItem.Builder quantity(int quantity) { 57 | if (quantity <= 0) { 58 | throw new IllegalArgumentException( 59 | "Expected positive quantity but received: " + quantity); 60 | } 61 | this.quantity = quantity; 62 | return this; 63 | } 64 | 65 | /** 66 | * @param price The per-unit price of the item in the shopping cart. This should use the 67 | * same currency as the order currency. 68 | * @return The builder object. 69 | */ 70 | public ShoppingCartItem.Builder price(BigDecimal price) { 71 | this.price = price; 72 | return this; 73 | } 74 | 75 | /** 76 | * @param price The price of the item in the shopping cart. This should be the same currency 77 | * as the order currency. 78 | * @return The builder object. 79 | */ 80 | public ShoppingCartItem.Builder price(Double price) { 81 | this.price = BigDecimal.valueOf(price); 82 | return this; 83 | } 84 | 85 | /** 86 | * @return An instance of {@code ShoppingCartItem} created from the fields set on this 87 | * builder. 88 | */ 89 | public ShoppingCartItem build() { 90 | return new ShoppingCartItem(this); 91 | } 92 | } 93 | 94 | /** 95 | * @return The category of the item. 96 | */ 97 | @JsonProperty("category") 98 | public String category() { 99 | return category; 100 | } 101 | 102 | /** 103 | * @return The ID of the item. 104 | */ 105 | @JsonProperty("item_id") 106 | public String itemId() { 107 | return itemId; 108 | } 109 | 110 | /** 111 | * @return The quantity of the item. 112 | */ 113 | @JsonProperty("quantity") 114 | public Integer quantity() { 115 | return quantity; 116 | } 117 | 118 | /** 119 | * @return The price of the item. 120 | */ 121 | @JsonProperty("price") 122 | public BigDecimal price() { 123 | return price; 124 | } 125 | } 126 | 127 | -------------------------------------------------------------------------------- /src/test/java/com/maxmind/minfraud/request/TransactionTest.java: -------------------------------------------------------------------------------- 1 | package com.maxmind.minfraud.request; 2 | 3 | import static org.junit.jupiter.api.Assertions.assertEquals; 4 | import static org.junit.jupiter.api.Assertions.assertNull; 5 | 6 | import java.net.InetAddress; 7 | import java.net.UnknownHostException; 8 | import org.junit.jupiter.api.Test; 9 | 10 | public class TransactionTest { 11 | private Transaction.Builder builder() throws UnknownHostException { 12 | return new Transaction.Builder( 13 | new Device.Builder(InetAddress.getByName("152.216.7.110")).build()); 14 | } 15 | 16 | @Test 17 | public void testConstructorWithoutDevice() { 18 | var request = new Transaction.Builder().build(); 19 | assertNull(request.device()); 20 | } 21 | 22 | @Test 23 | public void testAccount() throws Exception { 24 | var request = 25 | this.builder().account(new Account.Builder().userId("1").build()).build(); 26 | assertEquals("1", request.account().userId()); 27 | } 28 | 29 | @Test 30 | public void testBilling() throws Exception { 31 | var request = 32 | this.builder().billing(new Billing.Builder().address("add").build()).build(); 33 | assertEquals("add", request.billing().address()); 34 | } 35 | 36 | @Test 37 | public void testCreditCard() throws Exception { 38 | var request = 39 | this.builder().creditCard(new CreditCard.Builder().bankName("name").build()).build(); 40 | assertEquals("name", request.creditCard().bankName()); 41 | } 42 | 43 | @Test 44 | public void testCustomInputs() throws Exception { 45 | var request = this.builder().customInputs( 46 | new CustomInputs.Builder().put("key", "value").build()).build(); 47 | assertEquals("value", request.customInputs().inputs().get("key")); 48 | } 49 | 50 | @Test 51 | public void testDevice() throws Exception { 52 | var request = this.builder().build(); 53 | assertEquals(InetAddress.getByName("152.216.7.110"), request.device().ipAddress()); 54 | } 55 | 56 | @Test 57 | public void testDeviceThroughMethod() throws Exception { 58 | var ip = InetAddress.getByName("152.216.7.110"); 59 | 60 | var device = new Device.Builder().ipAddress(ip).build(); 61 | 62 | var request = new Transaction.Builder() 63 | .device(device) 64 | .build(); 65 | 66 | assertEquals(ip, request.device().ipAddress()); 67 | } 68 | 69 | @Test 70 | public void testEmail() throws Exception { 71 | var request = 72 | this.builder().email(new Email.Builder().domain("test.com").build()).build(); 73 | assertEquals("test.com", request.email().domain()); 74 | } 75 | 76 | @Test 77 | public void testEvent() throws Exception { 78 | var request = this.builder().event(new Event.Builder().shopId("1").build()).build(); 79 | assertEquals("1", request.event().shopId()); 80 | } 81 | 82 | @Test 83 | public void testOrder() throws Exception { 84 | var request = 85 | this.builder().order(new Order.Builder().affiliateId("af1").build()).build(); 86 | assertEquals("af1", request.order().affiliateId()); 87 | } 88 | 89 | @Test 90 | public void testPayment() throws Exception { 91 | var request = 92 | this.builder().payment(new Payment.Builder().declineCode("d").build()).build(); 93 | assertEquals("d", request.payment().declineCode()); 94 | } 95 | 96 | @Test 97 | public void testShipping() throws Exception { 98 | var request = 99 | this.builder().shipping(new Shipping.Builder().lastName("l").build()).build(); 100 | assertEquals("l", request.shipping().lastName()); 101 | } 102 | 103 | @Test 104 | public void testShoppingCart() throws Exception { 105 | var request = 106 | this.builder().addShoppingCartItem(new ShoppingCartItem.Builder().itemId("1").build()) 107 | .build(); 108 | assertEquals("1", request.shoppingCart().get(0).itemId()); 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /src/main/java/com/maxmind/minfraud/response/IpRiskReason.java: -------------------------------------------------------------------------------- 1 | package com.maxmind.minfraud.response; 2 | 3 | import com.fasterxml.jackson.annotation.JsonProperty; 4 | import com.maxmind.minfraud.JsonSerializable; 5 | 6 | /** 7 | * This class represents the reason for the IP risk. 8 | * 9 | * @param code This provides a machine-readable code identifying the reason. Although more codes 10 | * may be added in the future, the current codes are: 11 | *

12 | *
ANONYMOUS_IP
13 | *
The IP address belongs to an anonymous network. See the 14 | * object at {@code .IPAddress.Traits} for more details.
15 | * 16 | *
BILLING_POSTAL_VELOCITY
17 | *
Many different billing postal codes have been seen on 18 | * this IP address.
19 | * 20 | *
EMAIL_VELOCITY
21 | *
Many different email addresses have been seen on this 22 | * IP address.
23 | * 24 | *
HIGH_RISK_DEVICE
25 | *
A high risk device was seen on this IP address.
26 | * 27 | *
HIGH_RISK_EMAIL
28 | *
A high risk email address was seen on this IP address in 29 | * your past transactions.
30 | * 31 | *
ISSUER_ID_NUMBER_VELOCITY
32 | *
Many different issuer ID numbers have been seen on this 33 | * IP address.
34 | * 35 | *
MINFRAUD_NETWORK_ACTIVITY
36 | *
Suspicious activity has been seen on this IP address 37 | * across minFraud customers.
38 | *
39 | * @param reason This field provides a human-readable explanation of the reason. The description may 40 | * change at any time and should not be matched against. 41 | */ 42 | public record IpRiskReason( 43 | @JsonProperty("code") 44 | String code, 45 | 46 | @JsonProperty("reason") 47 | String reason 48 | ) implements JsonSerializable { 49 | 50 | /** 51 | * This provides a machine-readable code identifying the reason. Although more codes may be 52 | * added in the future, the current codes are: 53 | *
54 | *
ANONYMOUS_IP
55 | *
The IP address belongs to an anonymous network. See the 56 | * object at {@code .IPAddress.Traits} for more details.
57 | * 58 | *
BILLING_POSTAL_VELOCITY
59 | *
Many different billing postal codes have been seen on 60 | * this IP address.
61 | * 62 | *
EMAIL_VELOCITY
63 | *
Many different email addresses have been seen on this 64 | * IP address.
65 | * 66 | *
HIGH_RISK_DEVICE
67 | *
A high risk device was seen on this IP address.
68 | * 69 | *
HIGH_RISK_EMAIL
70 | *
A high risk email address was seen on this IP address in 71 | * your past transactions.
72 | * 73 | *
ISSUER_ID_NUMBER_VELOCITY
74 | *
Many different issuer ID numbers have been seen on this 75 | * IP address.
76 | * 77 | *
MINFRAUD_NETWORK_ACTIVITY
78 | *
Suspicious activity has been seen on this IP address 79 | * across minFraud customers.
80 | *
81 | * 82 | * @return The reason code. 83 | * @deprecated Use {@link #code()} instead. This method will be removed in 5.0.0. 84 | */ 85 | @Deprecated(since = "4.0.0", forRemoval = true) 86 | @JsonProperty("code") 87 | public String getCode() { 88 | return code(); 89 | } 90 | 91 | /** 92 | * @return This field provides a human-readable explanation of the reason. The description may 93 | * change at any time and should not be matched against. 94 | * @deprecated Use {@link #reason()} instead. This method will be removed in 5.0.0. 95 | */ 96 | @Deprecated(since = "4.0.0", forRemoval = true) 97 | @JsonProperty("reason") 98 | public String getReason() { 99 | return reason(); 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /src/main/java/com/maxmind/minfraud/response/Device.java: -------------------------------------------------------------------------------- 1 | package com.maxmind.minfraud.response; 2 | 3 | import com.fasterxml.jackson.annotation.JsonIgnore; 4 | import com.fasterxml.jackson.annotation.JsonProperty; 5 | import com.maxmind.minfraud.JsonSerializable; 6 | import java.time.ZonedDateTime; 7 | import java.util.UUID; 8 | 9 | /** 10 | * This class contains minFraud response data related to the device. 11 | *

12 | * In order to receive device output from minFraud Insights or minFraud Factors, you must be using 13 | * the Device Tracking Add-on. 14 | * 15 | * @param confidence A number representing the confidence that the device ID refers to a unique 16 | * device as opposed to a cluster of similar devices. A confidence of 0.01 17 | * indicates very low confidence that the device is unique, whereas 99 indicates 18 | * very high confidence. 19 | * @param id A UUID identifying the device associated with this IP address. 20 | * @param lastSeen The date and time of the last sighting of the device. This is an RFC 3339 21 | * date-time. 22 | * @param localTime The date and time of the transaction at the UTC offset associated with the 23 | * device. This is an RFC 3339 date-time. 24 | * @see Device Tracking Add-on 25 | */ 26 | public record Device( 27 | @JsonProperty("confidence") 28 | Double confidence, 29 | 30 | @JsonProperty("id") 31 | UUID id, 32 | 33 | @JsonProperty("last_seen") 34 | String lastSeen, 35 | 36 | @JsonProperty("local_time") 37 | String localTime 38 | ) implements JsonSerializable { 39 | 40 | /** 41 | * Constructs an instance of {@code Device} with no data. 42 | */ 43 | public Device() { 44 | this(null, null, null, null); 45 | } 46 | 47 | /** 48 | * @return a number representing the confidence that the device ID refers to a unique device as 49 | * opposed to a cluster of similar devices. A confidence of 0.01 indicates very low 50 | * confidence that the device is unique, whereas 99 indicates very high confidence. 51 | * @deprecated Use {@link #confidence()} instead. This method will be removed in 5.0.0. 52 | */ 53 | @Deprecated(since = "4.0.0", forRemoval = true) 54 | @JsonProperty("confidence") 55 | public Double getConfidence() { 56 | return confidence(); 57 | } 58 | 59 | /** 60 | * @return A UUID identifying the device associated with this IP address. 61 | * @deprecated Use {@link #id()} instead. This method will be removed in 5.0.0. 62 | */ 63 | @Deprecated(since = "4.0.0", forRemoval = true) 64 | @JsonProperty("id") 65 | public UUID getId() { 66 | return id(); 67 | } 68 | 69 | /** 70 | * @return The date and time of the last sighting of the device. This is an RFC 3339 date-time. 71 | * @deprecated Use {@link #lastSeen()} instead. This method will be removed in 5.0.0. 72 | */ 73 | @Deprecated(since = "4.0.0", forRemoval = true) 74 | @JsonProperty("last_seen") 75 | public String getLastSeen() { 76 | return lastSeen(); 77 | } 78 | 79 | /** 80 | * @return The date and time of the last sighting of the device as a {@code ZonedDateTime}. 81 | */ 82 | @JsonIgnore 83 | public ZonedDateTime getLastSeenDateTime() { 84 | if (lastSeen == null) { 85 | return null; 86 | } 87 | return ZonedDateTime.parse(lastSeen); 88 | } 89 | 90 | /** 91 | * @return The date and time of the transaction at the UTC offset associated with the device. 92 | * This is an RFC 3339 date-time. 93 | * @deprecated Use {@link #localTime()} instead. This method will be removed in 5.0.0. 94 | */ 95 | @Deprecated(since = "4.0.0", forRemoval = true) 96 | @JsonProperty("local_time") 97 | public String getLocalTime() { 98 | return localTime(); 99 | } 100 | 101 | /** 102 | * @return The date and time of the transaction at the UTC offset associated with the device as 103 | * a {@code ZonedDateTime}. 104 | */ 105 | @JsonIgnore 106 | public ZonedDateTime getLocalDateTime() { 107 | if (localTime == null) { 108 | return null; 109 | } 110 | return ZonedDateTime.parse(localTime); 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /src/main/java/com/maxmind/minfraud/request/CustomInputs.java: -------------------------------------------------------------------------------- 1 | package com.maxmind.minfraud.request; 2 | 3 | import com.fasterxml.jackson.annotation.JsonAnyGetter; 4 | import com.maxmind.minfraud.AbstractModel; 5 | import java.util.HashMap; 6 | import java.util.Map; 7 | import java.util.regex.Pattern; 8 | 9 | /** 10 | * Custom inputs to be used in 11 | * Custom Rules. 12 | * In order to use custom inputs, you must set them up from your account portal. 13 | */ 14 | public final class CustomInputs extends AbstractModel { 15 | private final Map inputs; 16 | 17 | private CustomInputs(Builder builder) { 18 | inputs = Map.copyOf(builder.inputs); 19 | } 20 | 21 | /** 22 | * {@code Builder} creates instances of {@code CustomInputs} from values set by the builder's 23 | * methods. 24 | */ 25 | public static class Builder { 26 | private static final long NUM_MAX = 10_000_000_000_000L; 27 | private static final Pattern KEY_PATTERN = Pattern.compile("^[a-z0-9_]{1,25}$"); 28 | 29 | final Map inputs = new HashMap<>(); 30 | 31 | /** 32 | * Add a string custom input. 33 | * 34 | * @param key The key for the custom input as defined on your account portal. 35 | * @param value The custom input value. Must be less than 256 characters and must not 36 | * contain new lines. 37 | * @return The builder object. 38 | * @throws IllegalArgumentException when the key or value are invalid. 39 | */ 40 | public Builder put(String key, String value) { 41 | validateKey(key); 42 | if (value.length() > 255 || value.contains("\n")) { 43 | throw new IllegalArgumentException("The custom input string " 44 | + value + " is invalid. The string be less than" 45 | + "256 characters and the string must not contain a newline."); 46 | } 47 | inputs.put(key, value); 48 | return this; 49 | } 50 | 51 | /** 52 | * Add a numeric custom input. 53 | * 54 | * @param key The key for the custom input as defined on your account portal. 55 | * @param value The custom input value. Must be between -10^13 and 10^13 exclusive. 56 | * @return The builder object. 57 | * @throws IllegalArgumentException when the key or value are invalid. 58 | */ 59 | public Builder put(String key, Number value) { 60 | validateKey(key); 61 | double doubleValue = value.doubleValue(); 62 | if (doubleValue <= -NUM_MAX || doubleValue >= NUM_MAX) { 63 | throw new IllegalArgumentException( 64 | "The custom input number " + value + "is invalid. " 65 | + "The number must be between -" + NUM_MAX 66 | + " and " + NUM_MAX + ", exclusive."); 67 | } 68 | inputs.put(key, value); 69 | return this; 70 | } 71 | 72 | /** 73 | * Add a boolean custom input. 74 | * 75 | * @param key The key for the custom input as defined on your account portal. 76 | * @param value The custom input value. 77 | * @return The builder object. 78 | * @throws IllegalArgumentException when the key or value are invalid. 79 | */ 80 | public Builder put(String key, boolean value) { 81 | validateKey(key); 82 | inputs.put(key, value); 83 | return this; 84 | } 85 | 86 | /** 87 | * @return An instance of {@code CustomInputs} created from the fields set on this builder. 88 | */ 89 | public CustomInputs build() { 90 | return new CustomInputs(this); 91 | } 92 | 93 | 94 | private void validateKey(String key) { 95 | if (!KEY_PATTERN.matcher(key).matches()) { 96 | throw new IllegalArgumentException("The custom input key " 97 | + key + " is invalid."); 98 | } 99 | } 100 | } 101 | 102 | /** 103 | * @return an unmodifiable map containing the custom inputs. 104 | */ 105 | @JsonAnyGetter 106 | public Map inputs() { 107 | return inputs; 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /src/main/java/com/maxmind/minfraud/response/ShippingAddress.java: -------------------------------------------------------------------------------- 1 | package com.maxmind.minfraud.response; 2 | 3 | import com.fasterxml.jackson.annotation.JsonProperty; 4 | import com.maxmind.minfraud.JsonSerializable; 5 | 6 | /** 7 | * This class contains minFraud response data related to the shipping address. 8 | * 9 | * @param distanceToBillingAddress The distance in kilometers from the shipping address to billing 10 | * address. 11 | * @param distanceToIpLocation The distance in kilometers from the address to the IP location. 12 | * This will be null if there is no value in the response. 13 | * @param isHighRisk This returns true if the shipping address is an address 14 | * associated with fraudulent transactions. It returns false when 15 | * the address is not associated with increased risk. If the address 16 | * could not be parsed or was not provided, null is returned. 17 | * @param isInIpCountry This returns true if the address is in the IP country. It is 18 | * false when the address is not in the IP country. If the address 19 | * could not be parsed or was not provided or the IP address could 20 | * not be geolocated, then null will be returned. 21 | * @param isPostalInCity This will return true if the postal code provided with the 22 | * address is in the city for the address. It will return false when 23 | * the postal code is not in the city. If the address was not 24 | * provided or could not be parsed, null will be returned. 25 | * @param latitude The latitude associated with the address. This will be null if 26 | * there is no value in the response. 27 | * @param longitude The longitude associated with the address. This will be null if 28 | * there is no value in the response. 29 | */ 30 | public record ShippingAddress( 31 | @JsonProperty("distance_to_billing_address") 32 | Integer distanceToBillingAddress, 33 | 34 | @JsonProperty("distance_to_ip_location") 35 | Integer distanceToIpLocation, 36 | 37 | @JsonProperty("is_high_risk") 38 | Boolean isHighRisk, 39 | 40 | @JsonProperty("is_in_ip_country") 41 | Boolean isInIpCountry, 42 | 43 | @JsonProperty("is_postal_in_city") 44 | Boolean isPostalInCity, 45 | 46 | @JsonProperty("latitude") 47 | Double latitude, 48 | 49 | @JsonProperty("longitude") 50 | Double longitude 51 | ) implements JsonSerializable { 52 | 53 | /** 54 | * Constructs an instance of {@code ShippingAddress} with no data. 55 | */ 56 | public ShippingAddress() { 57 | this(null, null, null, null, null, null, null); 58 | } 59 | 60 | /** 61 | * @return The distance in kilometers from the shipping address to billing address. 62 | * @deprecated Use {@link #distanceToBillingAddress()} instead. This method will be removed 63 | * in 5.0.0. 64 | */ 65 | @Deprecated(since = "4.0.0", forRemoval = true) 66 | @JsonProperty("distance_to_billing_address") 67 | public Integer getDistanceToBillingAddress() { 68 | return distanceToBillingAddress(); 69 | } 70 | 71 | /** 72 | * @return The latitude associated with the address. 73 | * @deprecated Use {@link #latitude()} instead. This method will be removed in 5.0.0. 74 | */ 75 | @Deprecated(since = "4.0.0", forRemoval = true) 76 | public Double getLatitude() { 77 | return latitude(); 78 | } 79 | 80 | /** 81 | * @return The longitude associated with the address. 82 | * @deprecated Use {@link #longitude()} instead. This method will be removed in 5.0.0. 83 | */ 84 | @Deprecated(since = "4.0.0", forRemoval = true) 85 | public Double getLongitude() { 86 | return longitude(); 87 | } 88 | 89 | /** 90 | * @return The distance in kilometers from the address to the IP location. 91 | * @deprecated Use {@link #distanceToIpLocation()} instead. This method will be removed in 92 | * 5.0.0. 93 | */ 94 | @Deprecated(since = "4.0.0", forRemoval = true) 95 | public Integer getDistanceToIpLocation() { 96 | return distanceToIpLocation(); 97 | } 98 | } 99 | 100 | -------------------------------------------------------------------------------- /dev-bin/release.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -eu -o pipefail 4 | 5 | # Pre-flight checks - verify all required tools are available and configured 6 | # before making any changes to the repository 7 | 8 | check_command() { 9 | if ! command -v "$1" &>/dev/null; then 10 | echo "Error: $1 is not installed or not in PATH" 11 | exit 1 12 | fi 13 | } 14 | 15 | # Verify gh CLI is authenticated 16 | if ! gh auth status &>/dev/null; then 17 | echo "Error: gh CLI is not authenticated. Run 'gh auth login' first." 18 | exit 1 19 | fi 20 | 21 | # Verify we can access this repository via gh 22 | if ! gh repo view --json name &>/dev/null; then 23 | echo "Error: Cannot access repository via gh. Check your authentication and repository access." 24 | exit 1 25 | fi 26 | 27 | # Verify git can connect to the remote (catches SSH key issues, etc.) 28 | if ! git ls-remote origin &>/dev/null; then 29 | echo "Error: Cannot connect to git remote. Check your git credentials/SSH keys." 30 | exit 1 31 | fi 32 | 33 | check_command perl 34 | check_command mvn 35 | 36 | # Check that we're not on the main branch 37 | current_branch=$(git branch --show-current) 38 | if [ "$current_branch" = "main" ]; then 39 | echo "Error: Releases should not be done directly on the main branch." 40 | echo "Please create a release branch and run this script from there." 41 | exit 1 42 | fi 43 | 44 | # Fetch latest changes and check that we're not behind origin/main 45 | echo "Fetching from origin..." 46 | git fetch origin 47 | 48 | if ! git merge-base --is-ancestor origin/main HEAD; then 49 | echo "Error: Current branch is behind origin/main." 50 | echo "Please merge or rebase with origin/main before releasing." 51 | exit 1 52 | fi 53 | 54 | changelog=$(cat CHANGELOG.md) 55 | 56 | regex=' 57 | ([0-9]+\.[0-9]+\.[0-9]+[a-zA-Z0-9\-]*) \(([0-9]{4}-[0-9]{2}-[0-9]{2})\) 58 | -* 59 | 60 | ((.| 61 | )*) 62 | ' 63 | 64 | if [[ ! $changelog =~ $regex ]]; then 65 | echo "Could not find date line in change log!" 66 | exit 1 67 | fi 68 | 69 | version="${BASH_REMATCH[1]}" 70 | date="${BASH_REMATCH[2]}" 71 | notes="$(echo "${BASH_REMATCH[3]}" | sed -n -e '/^[0-9]\+\.[0-9]\+\.[0-9]\+/,$!p')" 72 | 73 | if [[ "$date" != "$(date +"%Y-%m-%d")" ]]; then 74 | echo "$date is not today!" 75 | exit 1 76 | fi 77 | 78 | tag="v$version" 79 | 80 | if [ -n "$(git status --porcelain)" ]; then 81 | echo ". is not clean." >&2 82 | exit 1 83 | fi 84 | 85 | if [ ! -d .gh-pages ]; then 86 | echo "Checking out gh-pages in .gh-pages" 87 | git clone -b gh-pages git@github.com:maxmind/minfraud-api-java.git .gh-pages 88 | pushd .gh-pages 89 | else 90 | echo "Updating .gh-pages" 91 | pushd .gh-pages 92 | git pull 93 | fi 94 | 95 | if [ -n "$(git status --porcelain)" ]; then 96 | echo ".gh-pages is not clean" >&2 97 | exit 1 98 | fi 99 | 100 | popd 101 | 102 | mvn versions:display-plugin-updates 103 | mvn versions:display-dependency-updates 104 | 105 | read -r -n 1 -p "Continue given above dependencies? (y/n) " should_continue 106 | 107 | if [ "$should_continue" != "y" ]; then 108 | echo "Aborting" 109 | exit 1 110 | fi 111 | 112 | mvn test 113 | 114 | read -r -n 1 -p "Continue given above tests? (y/n) " should_continue 115 | 116 | if [ "$should_continue" != "y" ]; then 117 | echo "Aborting" 118 | exit 1 119 | fi 120 | 121 | page=.gh-pages/index.md 122 | cat <$page 123 | --- 124 | layout: default 125 | title: MaxMind minFraud Java API 126 | language: java 127 | version: $tag 128 | --- 129 | 130 | EOF 131 | 132 | mvn versions:set -DnewVersion="$version" 133 | 134 | perl -pi -e "s/(?<=)[^<]*/$version/" README.md 135 | perl -pi -e "s/(?<=com\.maxmind\.minfraud\:minfraud\:)\d+\.\d+\.\d+([\w\-]+)?/$version/" README.md 136 | 137 | cat README.md >>$page 138 | 139 | git diff 140 | 141 | read -r -n 1 -p "Commit changes? " should_commit 142 | if [ "$should_commit" != "y" ]; then 143 | echo "Aborting" 144 | exit 1 145 | fi 146 | git add README.md pom.xml 147 | git commit -m "Preparing for $version" 148 | 149 | mvn clean deploy 150 | 151 | rm -fr ".gh-pages/doc/$tag" 152 | cp -r target/reports/apidocs ".gh-pages/doc/$tag" 153 | rm -f .gh-pages/doc/latest 154 | ln -fs "$tag" .gh-pages/doc/latest 155 | 156 | pushd .gh-pages 157 | 158 | git add doc/ 159 | git commit -m "Updated for $tag" -a 160 | 161 | echo "Release notes for $version: 162 | 163 | $notes 164 | 165 | " 166 | read -r -n 1 -p "Push to origin? " should_push 167 | 168 | if [ "$should_push" != "y" ]; then 169 | echo "Aborting" 170 | exit 1 171 | fi 172 | 173 | git push 174 | 175 | popd 176 | 177 | git push 178 | 179 | gh release create --target "$(git branch --show-current)" -t "$version" -n "$notes" "$tag" \ 180 | "target/minfraud-$version-with-dependencies.zip" \ 181 | "target/minfraud-$version-with-dependencies.zip.asc" 182 | -------------------------------------------------------------------------------- /src/main/java/com/maxmind/minfraud/request/Device.java: -------------------------------------------------------------------------------- 1 | package com.maxmind.minfraud.request; 2 | 3 | import com.fasterxml.jackson.annotation.JsonProperty; 4 | import com.maxmind.minfraud.AbstractModel; 5 | import java.net.InetAddress; 6 | 7 | /** 8 | * The device information for the transaction. 9 | */ 10 | public final class Device extends AbstractModel { 11 | private final InetAddress ipAddress; 12 | private final String userAgent; 13 | private final String acceptLanguage; 14 | private final Double sessionAge; 15 | private final String sessionId; 16 | 17 | private Device(Device.Builder builder) { 18 | ipAddress = builder.ipAddress; 19 | userAgent = builder.userAgent; 20 | acceptLanguage = builder.acceptLanguage; 21 | sessionAge = builder.sessionAge; 22 | sessionId = builder.sessionId; 23 | } 24 | 25 | /** 26 | * {@code Builder} creates instances of {@code Device} from values set by the builder's 27 | * methods. 28 | */ 29 | public static final class Builder { 30 | InetAddress ipAddress; 31 | String userAgent; 32 | String acceptLanguage; 33 | Double sessionAge; 34 | String sessionId; 35 | 36 | /** 37 | * Constructor for the {@code Device.Builder} class 38 | */ 39 | public Builder() { 40 | } 41 | 42 | /** 43 | * Constructor for the {@code Device.Builder} class 44 | * 45 | * @param ipAddress The IP address associated with the device used by the customer in the 46 | * transaction. 47 | */ 48 | public Builder(InetAddress ipAddress) { 49 | this.ipAddress = ipAddress; 50 | } 51 | 52 | /** 53 | * @param ua The HTTP "User-Agent" header of the browser used in the transaction. 54 | * @return The builder object. 55 | */ 56 | public Device.Builder userAgent(String ua) { 57 | userAgent = ua; 58 | return this; 59 | } 60 | 61 | /** 62 | * @param acceptLanguage The HTTP "Accept-Language" header of the device used in the 63 | * transaction. 64 | * @return The builder object. 65 | */ 66 | public Device.Builder acceptLanguage(String acceptLanguage) { 67 | this.acceptLanguage = acceptLanguage; 68 | return this; 69 | } 70 | 71 | /** 72 | * @param ipAddress The IP address associated with the device used by the customer in the 73 | * transaction. 74 | * @return The builder object. 75 | */ 76 | public Device.Builder ipAddress(InetAddress ipAddress) { 77 | this.ipAddress = ipAddress; 78 | return this; 79 | } 80 | 81 | /** 82 | * @param sessionAge The number of seconds between the creation of the user's session and 83 | * the time of the transaction. Note that session_age is not the duration 84 | * of the current visit, but the time since the start of the first visit. 85 | * @return The builder object. 86 | */ 87 | public Device.Builder sessionAge(Double sessionAge) { 88 | this.sessionAge = 89 | sessionAge; 90 | return this; 91 | } 92 | 93 | /** 94 | * @param sessionId A string up to 255 characters in length. This is an ID which uniquely 95 | * identifies a visitor's session on the site. 96 | * @return The builder object. 97 | */ 98 | public Device.Builder sessionId(String sessionId) { 99 | this.sessionId = sessionId; 100 | return this; 101 | } 102 | 103 | /** 104 | * @return An instance of {@code Device} created from the fields set on this builder. 105 | */ 106 | public Device build() { 107 | return new Device(this); 108 | } 109 | } 110 | 111 | /** 112 | * @return The "User-Agent" header. 113 | */ 114 | @JsonProperty("user_agent") 115 | public String userAgent() { 116 | return userAgent; 117 | } 118 | 119 | /** 120 | * @return The "Accept-Language" header. 121 | */ 122 | @JsonProperty("accept_language") 123 | public String acceptLanguage() { 124 | return acceptLanguage; 125 | } 126 | 127 | /** 128 | * @return The session age. 129 | */ 130 | @JsonProperty("session_age") 131 | public Double sessionAge() { 132 | return sessionAge; 133 | } 134 | 135 | /** 136 | * @return The session id. 137 | */ 138 | @JsonProperty("session_id") 139 | public String sessionId() { 140 | return sessionId; 141 | } 142 | 143 | /** 144 | * @return The IP address used in the transaction. 145 | */ 146 | @JsonProperty("ip_address") 147 | public InetAddress ipAddress() { 148 | return ipAddress; 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /src/main/java/com/maxmind/minfraud/response/CreditCard.java: -------------------------------------------------------------------------------- 1 | package com.maxmind.minfraud.response; 2 | 3 | import com.fasterxml.jackson.annotation.JsonProperty; 4 | import com.maxmind.minfraud.JsonSerializable; 5 | 6 | /** 7 | * This class contains minFraud response data related to the credit card. 8 | * 9 | * @param brand The credit card brand. 10 | * @param country The two letter 12 | * ISO 3166-1 alpha-2 country code associated with the 13 | * location of the majority of customers using this credit 14 | * card as determined by their billing address. In cases 15 | * where the location of customers is highly mixed, this 16 | * defaults to the country of the bank issuing the card. 17 | * @param isBusiness True if the card is a business card. False if not a 18 | * business card. If the IIN was not provided or is unknown, 19 | * null will be returned. 20 | * @param isIssuedInBillingAddressCountry True if the country of the billing address matches the 21 | * country of the majority of customers using that IIN. In 22 | * cases where the location of customers is highly mixed, the 23 | * match is to the country of the bank issuing the card. 24 | * @param isPrepaid True if the card is a prepaid card. False if not prepaid. 25 | * If the IIN was not provided or is unknown, null will be 26 | * returned. 27 | * @param isVirtual True if the card is a virtual card. False if not virtual. 28 | * If the IIN was not provided or is unknown, null will be 29 | * returned. 30 | * @param issuer The {@code Issuer} model object. 31 | * @param type The credit card type. 32 | */ 33 | public record CreditCard( 34 | @JsonProperty("brand") 35 | String brand, 36 | 37 | @JsonProperty("country") 38 | String country, 39 | 40 | @JsonProperty("is_business") 41 | Boolean isBusiness, 42 | 43 | @JsonProperty("is_issued_in_billing_address_country") 44 | Boolean isIssuedInBillingAddressCountry, 45 | 46 | @JsonProperty("is_prepaid") 47 | Boolean isPrepaid, 48 | 49 | @JsonProperty("is_virtual") 50 | Boolean isVirtual, 51 | 52 | @JsonProperty("issuer") 53 | Issuer issuer, 54 | 55 | @JsonProperty("type") 56 | String type 57 | ) implements JsonSerializable { 58 | 59 | /** 60 | * Compact canonical constructor that sets defaults for null values. 61 | */ 62 | public CreditCard { 63 | issuer = issuer != null ? issuer : new Issuer(); 64 | } 65 | 66 | /** 67 | * Constructs an instance of {@code CreditCard} with no data. 68 | */ 69 | public CreditCard() { 70 | this(null, null, null, null, null, null, null, null); 71 | } 72 | 73 | /** 74 | * @return The {@code Issuer} model object. 75 | * @deprecated Use {@link #issuer()} instead. This method will be removed in 5.0.0. 76 | */ 77 | @Deprecated(since = "4.0.0", forRemoval = true) 78 | @JsonProperty("issuer") 79 | public Issuer getIssuer() { 80 | return issuer(); 81 | } 82 | 83 | /** 84 | * @return The credit card brand. 85 | * @deprecated Use {@link #brand()} instead. This method will be removed in 5.0.0. 86 | */ 87 | @Deprecated(since = "4.0.0", forRemoval = true) 88 | @JsonProperty("brand") 89 | public String getBrand() { 90 | return brand(); 91 | } 92 | 93 | /** 94 | * @return The two letter ISO 3166-1 95 | * alpha-2 country code associated with the location of the majority of customers using 96 | * this credit card as determined by their billing address. In cases where the location of 97 | * customers is highly mixed, this defaults to the country of the bank issuing the card. 98 | * @deprecated Use {@link #country()} instead. This method will be removed in 5.0.0. 99 | */ 100 | @Deprecated(since = "4.0.0", forRemoval = true) 101 | @JsonProperty("country") 102 | public String getCountry() { 103 | return country(); 104 | } 105 | 106 | /** 107 | * @return The credit card type. 108 | * @deprecated Use {@link #type()} instead. This method will be removed in 5.0.0. 109 | */ 110 | @Deprecated(since = "4.0.0", forRemoval = true) 111 | @JsonProperty("type") 112 | public String getType() { 113 | return type(); 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /src/test/java/com/maxmind/minfraud/response/InsightsResponseTest.java: -------------------------------------------------------------------------------- 1 | package com.maxmind.minfraud.response; 2 | 3 | import static org.junit.jupiter.api.Assertions.assertEquals; 4 | import static org.junit.jupiter.api.Assertions.assertFalse; 5 | import static org.junit.jupiter.api.Assertions.assertTrue; 6 | 7 | import com.fasterxml.jackson.jr.ob.JSON; 8 | import java.time.LocalDate; 9 | import java.util.UUID; 10 | import org.junit.jupiter.api.Test; 11 | 12 | public class InsightsResponseTest extends AbstractOutputTest { 13 | 14 | @Test 15 | public void testInsights() throws Exception { 16 | String id = "b643d445-18b2-4b9d-bad4-c9c4366e402a"; 17 | InsightsResponse insights = this.deserialize( 18 | InsightsResponse.class, 19 | JSON.std 20 | .composeString() 21 | .startObject() 22 | .startObjectField("disposition") 23 | .put("action", "accept") 24 | .end() 25 | .startObjectField("email") 26 | .startObjectField("domain") 27 | .put("first_seen", "2014-02-03") 28 | .end() 29 | .end() 30 | .startObjectField("ip_address") 31 | .startObjectField("country") 32 | .put("iso_code", "US") 33 | .end() 34 | .startObjectField("traits") 35 | .put("ip_address", "152.216.7.110") 36 | .put("network", "81.2.69.0/24") 37 | .end() 38 | .end() 39 | .startObjectField("credit_card") 40 | .put("is_business", true) 41 | .put("is_prepaid", true) 42 | .end() 43 | .startObjectField("shipping_address") 44 | .put("is_in_ip_country", true) 45 | .end() 46 | .startObjectField("shipping_phone") 47 | .put("is_voip", true) 48 | .put("matches_postal", false) 49 | .end() 50 | .startObjectField("billing_address") 51 | .put("is_in_ip_country", true) 52 | .end() 53 | .startObjectField("billing_phone") 54 | .put("is_voip", false) 55 | .put("matches_postal", true) 56 | .end() 57 | .put("funds_remaining", 1.20) 58 | .put("queries_remaining", 123) 59 | .put("id", id) 60 | .put("risk_score", 0.01) 61 | .startArrayField("warnings") 62 | .startObject() 63 | .put("code", "INVALID_INPUT") 64 | .end() 65 | .end() 66 | .end() 67 | .finish() 68 | ); 69 | 70 | assertEquals("accept", insights.disposition().action(), "disposition"); 71 | assertEquals( 72 | LocalDate.parse("2014-02-03"), 73 | insights.email().domain().firstSeen(), 74 | "email domain first seen" 75 | ); 76 | assertEquals( 77 | "US", 78 | insights.ipAddress().country().isoCode(), 79 | "correct country ISO" 80 | ); 81 | assertTrue(insights.creditCard().isBusiness(), "correct credit card is business"); 82 | assertTrue(insights.creditCard().isPrepaid(), "correct credit card prepaid"); 83 | 84 | assertTrue( 85 | insights.shippingAddress().isInIpCountry(), 86 | "correct shipping address is in IP country" 87 | ); 88 | assertTrue(insights.shippingPhone().isVoip(), "correct shipping phone isVoip"); 89 | assertFalse( 90 | insights.shippingPhone().matchesPostal(), 91 | "correct shipping phone matchesPostal" 92 | ); 93 | 94 | assertTrue( 95 | insights.billingAddress().isInIpCountry(), 96 | "correct billing address is in IP country" 97 | ); 98 | assertFalse(insights.billingPhone().isVoip(), "correct billing phone isVoip"); 99 | assertTrue( 100 | insights.billingPhone().matchesPostal(), 101 | "correct billing phone matchesPostal" 102 | ); 103 | 104 | assertEquals( 105 | Double.valueOf(1.20), 106 | insights.fundsRemaining(), 107 | "correct funds remaining" 108 | ); 109 | assertEquals(UUID.fromString(id), insights.id(), "correct ID"); 110 | assertEquals( 111 | Integer.valueOf(123), 112 | insights.queriesRemaining(), 113 | "correct queries remaining" 114 | ); 115 | assertEquals(Double.valueOf(0.01), insights.riskScore(), "correct risk score"); 116 | assertEquals( 117 | "INVALID_INPUT", 118 | insights.warnings().get(0).code(), 119 | "correct warning code" 120 | ); 121 | assertEquals("152.216.7.110", insights.ipAddress().traits().ipAddress().getHostAddress()); 122 | assertEquals("81.2.69.0/24", insights.ipAddress().traits().network().toString()); 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /src/test/java/com/maxmind/minfraud/request/TransactionReportTest.java: -------------------------------------------------------------------------------- 1 | package com.maxmind.minfraud.request; 2 | 3 | import static org.junit.jupiter.api.Assertions.assertEquals; 4 | import static org.junit.jupiter.api.Assertions.assertThrows; 5 | 6 | import com.maxmind.minfraud.request.TransactionReport.Builder; 7 | import com.maxmind.minfraud.request.TransactionReport.Tag; 8 | import java.net.InetAddress; 9 | import java.net.UnknownHostException; 10 | import java.util.UUID; 11 | import org.junit.jupiter.api.Test; 12 | import org.skyscreamer.jsonassert.JSONAssert; 13 | 14 | public class TransactionReportTest { 15 | 16 | private final InetAddress ip; 17 | private final Tag tag; 18 | 19 | public TransactionReportTest() throws UnknownHostException { 20 | ip = InetAddress.getByName("1.1.1.1"); 21 | tag = Tag.NOT_FRAUD; 22 | } 23 | 24 | @Test 25 | public void testInvalidTag() { 26 | assertThrows( 27 | IllegalArgumentException.class, 28 | () -> new Builder(null).maxmindId("123456789").build() 29 | ); 30 | } 31 | 32 | @Test 33 | public void testBuildInvalidIdentifier() { 34 | assertThrows( 35 | IllegalArgumentException.class, 36 | () -> new Builder(tag).build() 37 | ); 38 | } 39 | 40 | @Test 41 | public void testBuildValidIdentifier() { 42 | final var maxmindId = "12345678"; 43 | final var minfraudId = UUID.fromString( 44 | "58fa38d8-4b87-458b-a22b-f00eda1aa20d"); 45 | final var transactionId = "abc123"; 46 | 47 | 48 | assertEquals(ip, new TransactionReport.Builder(tag) 49 | .ipAddress(ip).build().ipAddress()); 50 | assertEquals(maxmindId, new TransactionReport.Builder(tag) 51 | .maxmindId(maxmindId).build().maxmindId()); 52 | assertEquals(minfraudId, new TransactionReport.Builder(tag) 53 | .minfraudId(minfraudId).build().minfraudId()); 54 | assertEquals(transactionId, new TransactionReport.Builder(tag) 55 | .transactionId(transactionId).build().transactionId()); 56 | } 57 | 58 | @Test 59 | public void testIpAddress() { 60 | final var report = new Builder(tag).ipAddress(ip).build(); 61 | assertEquals(ip, report.ipAddress()); 62 | } 63 | 64 | @Test 65 | public void testTag() { 66 | final var report = new Builder(tag).ipAddress(ip).build(); 67 | assertEquals(Tag.NOT_FRAUD, report.tag()); 68 | } 69 | 70 | @Test 71 | public void testChargebackCode() { 72 | final var code = "foo"; 73 | final var report = 74 | new Builder(tag).ipAddress(ip).chargebackCode(code).build(); 75 | assertEquals(code, report.chargebackCode()); 76 | } 77 | 78 | @Test 79 | public void testTooLongMaxmindId() { 80 | assertThrows( 81 | IllegalArgumentException.class, 82 | () -> new Builder(tag).maxmindId("123456789").build() 83 | ); 84 | } 85 | 86 | @Test 87 | public void testTooShortMaxmindId() { 88 | assertThrows( 89 | IllegalArgumentException.class, 90 | () -> new Builder(tag).maxmindId("1234567").build() 91 | ); 92 | } 93 | 94 | @Test 95 | public void testValidMaxmindId() { 96 | final var id = "12345678"; 97 | final var report = new Builder(tag).maxmindId(id).build(); 98 | assertEquals(id, report.maxmindId()); 99 | } 100 | 101 | @Test 102 | public void testMinfraudId() { 103 | final var id = UUID.fromString("58fa38d8-4b87-458b-a22b-f00eda1aa20d"); 104 | final var report = new Builder(tag).minfraudId(id).build(); 105 | assertEquals(id, report.minfraudId()); 106 | } 107 | 108 | @Test 109 | public void testNotes() { 110 | final var notes = "foo"; 111 | final var report = new Builder(tag).ipAddress(ip).notes(notes).build(); 112 | assertEquals(notes, report.notes()); 113 | } 114 | 115 | @Test 116 | public void testTransactionID() { 117 | final var id = "foo"; 118 | final var report = new Builder(tag).transactionId(id).build(); 119 | assertEquals(id, report.transactionId()); 120 | } 121 | 122 | // Test the example in the README 123 | @Test 124 | public void testAllFields() throws Exception { 125 | final var report = new TransactionReport.Builder(Tag.NOT_FRAUD) 126 | .chargebackCode("mycode") 127 | .ipAddress(InetAddress.getByName("1.1.1.1")) 128 | .maxmindId("12345678") 129 | .minfraudId(UUID.fromString("58fa38d8-4b87-458b-a22b-f00eda1aa20d")) 130 | .notes("notes go here") 131 | .transactionId("foo") 132 | .build(); 133 | 134 | final var expectedJSON = "{" + 135 | "ip_address:'1.1.1.1'," + 136 | "tag:'not_fraud'," + 137 | "chargeback_code:'mycode'," + 138 | "maxmind_id:'12345678'," + 139 | "minfraud_id:'58fa38d8-4b87-458b-a22b-f00eda1aa20d'," + 140 | "notes:'notes go here'," + 141 | "transaction_id:'foo'" + 142 | "}"; 143 | 144 | JSONAssert.assertEquals(expectedJSON, report.toJson(), true); 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /README.dev.md: -------------------------------------------------------------------------------- 1 | # Preparing your environment for a release 2 | 3 | - Ensure you have access to publish to the repository on 4 | [Central Portal](https://central.sonatype.org/). 5 | - See the section about Central Portal access. 6 | - You need a GPG secret key. You need to publish it as well. 7 | - See the section about setting up GPG. 8 | - Ensure the SSH key you use on GitHub.com is available. 9 | - e.g., `~/.ssh/id_rsa`. 10 | - Ensure an appropriate `~/.gitconfig` is set up. 11 | - The release process generates commits. 12 | - Ensure you have the necessary dependencies available: 13 | - e.g., `apt-get install maven default-jdk git-core` 14 | - Ensure [gh](https://github.com/cli/cli) is set up and in your 15 | `PATH`. 16 | - An easy way to do this is get a release tarball and run `./install`. 17 | 18 | ## Setting up Central Portal access 19 | 20 | To get this access, first create a Central Portal account. You will then need 21 | access to our namespace, but we have not added anyone since switching to 22 | Central Portal. Previously you would need to make an account on the [Sonatype 23 | JIRA issue tracker](https://issues.sonatype.org/) and make an issue asking for 24 | access [like so](https://issues.sonatype.org/browse/OSSRH-34414). 25 | 26 | Ensure you inform MaxMind operations about your new access. 27 | 28 | Configure your `~/.m2/settings.xml` file for releasing to Central Portal. See 29 | [these instructions](https://central.sonatype.org/publish/publish-portal-maven/#credentials). 30 | 31 | Some links about Central Portal: 32 | 33 | * [Maven Central Repository homepage](https://central.sonatype.com/). You can 34 | sign-in from here. 35 | * [Publishing guide](https://central.sonatype.org/publish/publish-portal-maven/) 36 | 37 | ## Setting up GPG 38 | 39 | You need a key. It is fine to create/use your own, but you'll probably want 40 | one with your MaxMind email address. 41 | 42 | If you need to generate a key: `gpg --gen-key`. 43 | 44 | If you have one and need to export/import it: 45 | 46 | gpg --export-secret-keys --armor > secretkey.gpg 47 | gpg --import secretkey.gpg 48 | gpg --edit-key 49 | 50 | and enter `trust` and choose ultimate. 51 | 52 | Make sure the key shows up in `gpg --list-secret-keys`. 53 | 54 | Make sure you publish it to a keyserver. See 55 | [here](http://central.sonatype.org/pages/working-with-pgp-signatures.html) 56 | for more info about that and the process in general. 57 | 58 | ### gpg "inappropriate ioctl" errors 59 | 60 | You only really need to do this if you see "inappropriate ioctl" errors, 61 | but it shouldn't hurt to proactively do this. 62 | 63 | Add this to ~/.gnupg/gpg.conf: 64 | 65 | use-agent 66 | pinentry-mode loopback 67 | 68 | Add this to ~/.gnupg/gpg-agent.conf: 69 | 70 | allow-loopback-pinentry 71 | 72 | # Releasing 73 | 74 | ## Steps 75 | 76 | - Ensure you can run `mvn test` and `mvn package` successfully. Run 77 | `mvn clean` after. 78 | - Create a release branch off `main`. Ensure you have a clean checkout and that 79 | the subdirectory `.gh-pages` either does not exist or is a clean checkout. 80 | - We'll be generating commits. 81 | - When the release is complete, you should deliver the release PR for review. 82 | - Review open issues and PRs to see if any can easily be fixed, closed, or 83 | merged. 84 | - Review `CHANGELOG.md` for completeness and correctness. 85 | - Set a version and a date in `CHANGELOG.md` and commit that. 86 | - It gets used in the release process. 87 | - Bump copyright year in `README.md` if appropriate. 88 | - You don't need to update the version. `./dev-bin/release.sh` does this. 89 | - Run `./dev-bin/release.sh` 90 | - This will package the release, update the gh-pages branch, bump the 91 | version to the next development release, upload the release to GitHub 92 | and tag it, and upload to Sonatype. 93 | - This will prompt you several times. Generally you need to say `y` or `n`. 94 | - You may be prompted for your GitHub.com username and password several 95 | times depending on your workspace. 96 | - You may be prompted about "The following dependencies in Dependencies 97 | have newer versions". See the section about updating dependencies if so. 98 | - If you get HTTP 401 errors from Central Portal, you probably don't have a 99 | correct `settings.xml`. Refer to the Central Portal section. 100 | - If you get to this point, then a release is on GitHub.com and Maven 101 | Central. 102 | - You're done! 103 | - If you want to check things over, look at the commits on GitHub.com, 104 | including to the `gh-pages` branch and release tags, and do an artifact 105 | search on [Maven Central](https://central.sonatype.com/) to see the version 106 | is as you expect. It may take a few minutes for new releases to show up 107 | on Maven Central. 108 | 109 | ## Updating dependencies 110 | 111 | Review the versions and look at what changed in their changelogs. If you 112 | think it is appropriate to update the dependencies, stop the release 113 | process (say `n` or ctrl-c out). 114 | 115 | To update them: 116 | 117 | - Make a branch 118 | - Update `pom.xml` to have the new versions you want 119 | - Run `mvn test` and fix any errors 120 | - Push and ensure Travis completes successfully 121 | - Merge 122 | 123 | If you did this in the middle of releasing, you'll have to start that 124 | process over. 125 | -------------------------------------------------------------------------------- /src/test/java/com/maxmind/minfraud/request/CreditCardTest.java: -------------------------------------------------------------------------------- 1 | package com.maxmind.minfraud.request; 2 | 3 | import static org.junit.jupiter.api.Assertions.assertEquals; 4 | import static org.junit.jupiter.api.Assertions.assertThrows; 5 | import static org.junit.jupiter.api.Assertions.assertTrue; 6 | 7 | import com.maxmind.minfraud.request.CreditCard.Builder; 8 | import org.junit.jupiter.api.Test; 9 | import org.junit.jupiter.params.ParameterizedTest; 10 | import org.junit.jupiter.params.provider.ValueSource; 11 | 12 | public class CreditCardTest { 13 | 14 | @Test 15 | public void testIssuerIdNumber() { 16 | var cc = new Builder().issuerIdNumber("123456").build(); 17 | assertEquals("123456", cc.issuerIdNumber()); 18 | 19 | cc = new Builder().issuerIdNumber("12345678").build(); 20 | assertEquals("12345678", cc.issuerIdNumber()); 21 | } 22 | 23 | @Test 24 | public void testIssuerIdNumberThatIsTooLong() { 25 | assertThrows( 26 | IllegalArgumentException.class, 27 | () -> new Builder().issuerIdNumber("1234567").build() 28 | ); 29 | } 30 | 31 | @Test 32 | public void testIssuerIdNumberThatIsTooShort() { 33 | assertThrows( 34 | IllegalArgumentException.class, 35 | () -> new Builder().issuerIdNumber("12345").build() 36 | ); 37 | } 38 | 39 | @Test 40 | public void testIssuerIdNumberThatHasLetters() { 41 | assertThrows( 42 | IllegalArgumentException.class, 43 | () -> new Builder().issuerIdNumber("12345a").build() 44 | ); 45 | } 46 | 47 | @Test 48 | public void testLastDigits() { 49 | var cc = new Builder().lastDigits("1234").build(); 50 | assertEquals("1234", cc.lastDigits()); 51 | 52 | cc = new Builder().lastDigits("12").build(); 53 | assertEquals("12", cc.lastDigits()); 54 | } 55 | 56 | @Test 57 | public void testLastDigitsThatIsTooLong() { 58 | assertThrows( 59 | IllegalArgumentException.class, 60 | () -> new Builder().lastDigits("12345").build() 61 | ); 62 | } 63 | 64 | @Test 65 | public void testLastDigitsThatIsTooShort() { 66 | 67 | assertThrows( 68 | IllegalArgumentException.class, 69 | () -> new Builder().lastDigits("123").build() 70 | ); 71 | } 72 | 73 | @Test 74 | public void testLastDigitsThatHasLetters() { 75 | assertThrows( 76 | IllegalArgumentException.class, 77 | () -> new Builder().lastDigits("123a").build() 78 | ); 79 | } 80 | 81 | @Test 82 | public void testBankName() { 83 | var cc = new Builder().bankName("Bank").build(); 84 | assertEquals("Bank", cc.bankName()); 85 | } 86 | 87 | @Test 88 | public void testBankPhoneCountryCode() { 89 | var cc = new Builder().bankPhoneCountryCode("1").build(); 90 | assertEquals("1", cc.bankPhoneCountryCode()); 91 | } 92 | 93 | @Test 94 | public void testBankPhoneNumber() { 95 | var phone = "231-323-3123"; 96 | var cc = new Builder().bankPhoneNumber(phone).build(); 97 | assertEquals(phone, cc.bankPhoneNumber()); 98 | } 99 | 100 | @Test 101 | public void testCountry() { 102 | var country = "CA"; 103 | var cc = new Builder().country(country).build(); 104 | assertEquals(country, cc.country()); 105 | } 106 | 107 | @ParameterizedTest 108 | @ValueSource(strings = {"ca", "USA", "C1"}) 109 | public void testInvalidCountry(String country) { 110 | assertThrows( 111 | IllegalArgumentException.class, 112 | () -> new Builder().country(country).build() 113 | ); 114 | } 115 | 116 | @Test 117 | public void testAvsResult() { 118 | var cc = new Builder().avsResult('Y').build(); 119 | assertEquals(Character.valueOf('Y'), cc.avsResult()); 120 | } 121 | 122 | @Test 123 | public void testCvvResult() { 124 | var cc = new Builder().cvvResult('N').build(); 125 | assertEquals(Character.valueOf('N'), cc.cvvResult()); 126 | } 127 | 128 | @ParameterizedTest 129 | @ValueSource(strings = {"4485921507912924", 130 | "432312", 131 | "this is invalid", 132 | "", 133 | "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" 134 | }) 135 | public void testInvalidToken(String token) { 136 | assertThrows( 137 | IllegalArgumentException.class, 138 | () -> new Builder().token(token).build() 139 | ); 140 | } 141 | 142 | @ParameterizedTest 143 | @ValueSource(strings = {"t4485921507912924", 144 | "a7f6%gf83fhAu", 145 | "valid_token" 146 | }) 147 | public void testValidToken(String token) { 148 | var cc = new Builder().token(token).build(); 149 | assertEquals(token, cc.token()); 150 | } 151 | 152 | @Test 153 | public void testWas3dSecureSuccessful() { 154 | var cc = new Builder().was3dSecureSuccessful(true).build(); 155 | assertTrue(cc.was3dSecureSuccessful()); 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /src/main/java/com/maxmind/minfraud/response/Warning.java: -------------------------------------------------------------------------------- 1 | package com.maxmind.minfraud.response; 2 | 3 | import com.fasterxml.jackson.annotation.JsonProperty; 4 | import com.maxmind.minfraud.JsonSerializable; 5 | 6 | /** 7 | * This class represents a warning returned by the web service. 8 | * 9 | * @param code This provides a machine-readable code identifying the warning. Although more 10 | * codes may be added in the future, the current codes are: 11 | * 12 | *

    13 | *
  • BILLING_CITY_NOT_FOUND – the billing city could not be found in 14 | * our database.
  • 15 | *
  • BILLING_COUNTRY_NOT_FOUND – the billing country could not be 16 | * found in our database.
  • 17 | *
  • BILLING_POSTAL_NOT_FOUND – the billing postal could not be found 18 | * in our database.
  • 19 | *
  • INPUT_INVALID – the value associated with the key does not meet 20 | * the required constraints, e.g., "United States" in a field that 21 | * requires a two-letter country code.
  • 22 | *
  • INPUT_UNKNOWN – an unknown key was encountered in the request 23 | * body.
  • 24 | *
  • IP_ADDRESS_NOT_FOUND – the IP address could not be 25 | * geolocated.
  • 26 | *
  • SHIPPING_CITY_NOT_FOUND – the shipping city could not be found 27 | * in our database.
  • 28 | *
  • SHIPPING_COUNTRY_NOT_FOUND – the shipping country could not be 29 | * found in our database.
  • 30 | *
  • SHIPPING_POSTAL_NOT_FOUND – the shipping postal could not be 31 | * found in our database.
  • 32 | *
33 | * @param warning This field provides a human-readable explanation of the warning. The 34 | * description may change at any time and should not be matched against. 35 | * @param inputPointer This is a JSON Pointer to the input that the warning is associated with. For 36 | * instance, if the warning was about the billing city, the value would be 37 | * "/billing/city". See 38 | * RFC 6901 for the JSON 39 | * Pointer spec. 40 | */ 41 | public record Warning( 42 | @JsonProperty("code") 43 | String code, 44 | 45 | @JsonProperty("warning") 46 | String warning, 47 | 48 | @JsonProperty("input_pointer") 49 | String inputPointer 50 | ) implements JsonSerializable { 51 | 52 | /** 53 | * This provides a machine-readable code identifying the warning. Although more codes may be 54 | * added in the future, the current codes are: 55 | * 56 | *
    57 | *
  • BILLING_CITY_NOT_FOUND – the billing city could not be found in 58 | * our database.
  • 59 | *
  • BILLING_COUNTRY_NOT_FOUND – the billing country could not be 60 | * found in our database.
  • 61 | *
  • BILLING_POSTAL_NOT_FOUND – the billing postal could not be found 62 | * in our database.
  • 63 | *
  • INPUT_INVALID – the value associated with the key does not meet 64 | * the required constraints, e.g., "United States" in a field that 65 | * requires a two-letter country code.
  • 66 | *
  • INPUT_UNKNOWN – an unknown key was encountered in the request 67 | * body.
  • 68 | *
  • IP_ADDRESS_NOT_FOUND – the IP address could not be 69 | * geolocated.
  • 70 | *
  • SHIPPING_CITY_NOT_FOUND – the shipping city could not be found 71 | * in our database.
  • 72 | *
  • SHIPPING_COUNTRY_NOT_FOUND – the shipping country could not be 73 | * found in our database.
  • 74 | *
  • SHIPPING_POSTAL_NOT_FOUND – the shipping postal could not be 75 | * found in our database.
  • 76 | *
77 | * 78 | * @return The warning code. 79 | * @deprecated Use {@link #code()} instead. This method will be removed in 5.0.0. 80 | */ 81 | @Deprecated(since = "4.0.0", forRemoval = true) 82 | @JsonProperty("code") 83 | public String getCode() { 84 | return code(); 85 | } 86 | 87 | /** 88 | * @return This field provides a human-readable explanation of the warning. The description may 89 | * change at any time and should not be matched against. 90 | * @deprecated Use {@link #warning()} instead. This method will be removed in 5.0.0. 91 | */ 92 | @Deprecated(since = "4.0.0", forRemoval = true) 93 | @JsonProperty("warning") 94 | public String getWarning() { 95 | return warning(); 96 | } 97 | 98 | /** 99 | * @return This is a JSON Pointer to the input that the warning is associated with. For 100 | * instance, if the warning was about the billing city, the value would be "/billing/city". 101 | * See 102 | * RFC 6901 for the JSON Pointer spec. 103 | * @deprecated Use {@link #inputPointer()} instead. This method will be removed in 5.0.0. 104 | */ 105 | @Deprecated(since = "4.0.0", forRemoval = true) 106 | @JsonProperty("input_pointer") 107 | public String getInputPointer() { 108 | return inputPointer(); 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /src/test/java/com/maxmind/minfraud/response/EmailDomainVisitTest.java: -------------------------------------------------------------------------------- 1 | package com.maxmind.minfraud.response; 2 | 3 | import static org.junit.jupiter.api.Assertions.assertEquals; 4 | import static org.junit.jupiter.api.Assertions.assertNull; 5 | import static org.junit.jupiter.api.Assertions.assertTrue; 6 | 7 | import com.fasterxml.jackson.jr.ob.JSON; 8 | import com.maxmind.minfraud.response.EmailDomainVisit.Status; 9 | import java.time.LocalDate; 10 | import org.junit.jupiter.api.Test; 11 | 12 | public class EmailDomainVisitTest extends AbstractOutputTest { 13 | 14 | @Test 15 | public void testEmailDomainVisitWithAllFields() throws Exception { 16 | EmailDomainVisit visit = this.deserialize( 17 | EmailDomainVisit.class, 18 | JSON.std 19 | .composeString() 20 | .startObject() 21 | .put("has_redirect", true) 22 | .put("last_visited_on", "2024-11-15") 23 | .put("status", "live") 24 | .end() 25 | .finish() 26 | ); 27 | 28 | assertTrue(visit.hasRedirect()); 29 | assertEquals(LocalDate.parse("2024-11-15"), visit.lastVisitedOn()); 30 | assertEquals(Status.LIVE, visit.status()); 31 | assertEquals("live", visit.status().toString()); 32 | } 33 | 34 | @Test 35 | public void testEmailDomainVisitWithMinimalFields() throws Exception { 36 | EmailDomainVisit visit = this.deserialize( 37 | EmailDomainVisit.class, 38 | JSON.std 39 | .composeString() 40 | .startObject() 41 | .put("status", "parked") 42 | .end() 43 | .finish() 44 | ); 45 | 46 | assertNull(visit.hasRedirect()); 47 | assertNull(visit.lastVisitedOn()); 48 | assertEquals(Status.PARKED, visit.status()); 49 | assertEquals("parked", visit.status().toString()); 50 | } 51 | 52 | @Test 53 | public void testEmailDomainVisitWithDnsError() throws Exception { 54 | EmailDomainVisit visit = this.deserialize( 55 | EmailDomainVisit.class, 56 | JSON.std 57 | .composeString() 58 | .startObject() 59 | .put("status", "dns_error") 60 | .put("last_visited_on", "2024-10-01") 61 | .end() 62 | .finish() 63 | ); 64 | 65 | assertEquals(Status.DNS_ERROR, visit.status()); 66 | assertEquals("dns_error", visit.status().toString()); 67 | assertEquals(LocalDate.parse("2024-10-01"), visit.lastVisitedOn()); 68 | } 69 | 70 | @Test 71 | public void testEmailDomainVisitWithNetworkError() throws Exception { 72 | EmailDomainVisit visit = this.deserialize( 73 | EmailDomainVisit.class, 74 | JSON.std 75 | .composeString() 76 | .startObject() 77 | .put("status", "network_error") 78 | .end() 79 | .finish() 80 | ); 81 | 82 | assertEquals(Status.NETWORK_ERROR, visit.status()); 83 | assertEquals("network_error", visit.status().toString()); 84 | } 85 | 86 | @Test 87 | public void testEmailDomainVisitWithHttpError() throws Exception { 88 | EmailDomainVisit visit = this.deserialize( 89 | EmailDomainVisit.class, 90 | JSON.std 91 | .composeString() 92 | .startObject() 93 | .put("status", "http_error") 94 | .end() 95 | .finish() 96 | ); 97 | 98 | assertEquals(Status.HTTP_ERROR, visit.status()); 99 | assertEquals("http_error", visit.status().toString()); 100 | } 101 | 102 | @Test 103 | public void testEmailDomainVisitWithPreDevelopment() throws Exception { 104 | EmailDomainVisit visit = this.deserialize( 105 | EmailDomainVisit.class, 106 | JSON.std 107 | .composeString() 108 | .startObject() 109 | .put("status", "pre_development") 110 | .end() 111 | .finish() 112 | ); 113 | 114 | assertEquals(Status.PRE_DEVELOPMENT, visit.status()); 115 | assertEquals("pre_development", visit.status().toString()); 116 | } 117 | 118 | @Test 119 | public void testEmailDomainVisitWithUnknownStatus() throws Exception { 120 | EmailDomainVisit visit = this.deserialize( 121 | EmailDomainVisit.class, 122 | JSON.std 123 | .composeString() 124 | .startObject() 125 | .put("status", "future_new_status") 126 | .put("last_visited_on", "2024-11-01") 127 | .end() 128 | .finish() 129 | ); 130 | 131 | assertNull(visit.status()); 132 | assertEquals(LocalDate.parse("2024-11-01"), visit.lastVisitedOn()); 133 | } 134 | 135 | @Test 136 | public void testEmailDomainVisitEmpty() throws Exception { 137 | EmailDomainVisit visit = this.deserialize( 138 | EmailDomainVisit.class, 139 | JSON.std 140 | .composeString() 141 | .startObject() 142 | .end() 143 | .finish() 144 | ); 145 | 146 | assertNull(visit.hasRedirect()); 147 | assertNull(visit.lastVisitedOn()); 148 | assertNull(visit.status()); 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /src/main/java/com/maxmind/minfraud/response/ScoreResponse.java: -------------------------------------------------------------------------------- 1 | package com.maxmind.minfraud.response; 2 | 3 | import com.fasterxml.jackson.annotation.JsonProperty; 4 | import com.maxmind.minfraud.JsonSerializable; 5 | import java.util.List; 6 | import java.util.UUID; 7 | 8 | /** 9 | * This class represents the minFraud Score response. 10 | * 11 | * @param disposition The disposition set by your custom rules. 12 | * @param fundsRemaining The approximate US dollar value of the funds remaining on your MaxMind 13 | * account. 14 | * @param id This is a UUID that identifies the minFraud request. 15 | * @param ipAddress The {@code IpAddress} model object. 16 | * @param queriesRemaining The approximate number of queries remaining for this service before your 17 | * account runs out of funds. 18 | * @param riskScore This returns the risk score, from 0.01 to 99. A higher score indicates 19 | * a higher risk of fraud. For example, a score of 20 indicates a 20% 20 | * chance that a transaction is fraudulent. We never return a risk score of 21 | * 0, since all transactions have the possibility of being fraudulent. 22 | * Likewise, we never return a risk score of 100. 23 | * @param warnings An unmodifiable list containing warning objects that detail issues with 24 | * the request such as invalid or unknown inputs. It is highly recommended 25 | * that you check this list for issues when integrating the web service. 26 | */ 27 | public record ScoreResponse( 28 | @JsonProperty("disposition") 29 | Disposition disposition, 30 | 31 | @JsonProperty("funds_remaining") 32 | Double fundsRemaining, 33 | 34 | @JsonProperty("id") 35 | UUID id, 36 | 37 | @JsonProperty("ip_address") 38 | ScoreIpAddress ipAddress, 39 | 40 | @JsonProperty("queries_remaining") 41 | Integer queriesRemaining, 42 | 43 | @JsonProperty("risk_score") 44 | Double riskScore, 45 | 46 | @JsonProperty("warnings") 47 | List warnings 48 | ) implements JsonSerializable { 49 | 50 | /** 51 | * Compact canonical constructor that sets defaults for null values. 52 | */ 53 | public ScoreResponse { 54 | disposition = disposition != null ? disposition : new Disposition(); 55 | ipAddress = ipAddress != null ? ipAddress : new ScoreIpAddress(); 56 | warnings = warnings != null ? List.copyOf(warnings) : List.of(); 57 | } 58 | 59 | /** 60 | * Constructs an instance of {@code ScoreResponse} with no data. 61 | */ 62 | public ScoreResponse() { 63 | this(null, null, null, null, null, null, null); 64 | } 65 | 66 | /** 67 | * @return The disposition set by your custom rules. 68 | * @deprecated Use {@link #disposition()} instead. This method will be removed in 5.0.0. 69 | */ 70 | @Deprecated(since = "4.0.0", forRemoval = true) 71 | @JsonProperty("disposition") 72 | public Disposition getDisposition() { 73 | return disposition(); 74 | } 75 | 76 | /** 77 | * @return The approximate US dollar value of the funds remaining on your MaxMind account. 78 | * @deprecated Use {@link #fundsRemaining()} instead. This method will be removed in 5.0.0. 79 | */ 80 | @Deprecated(since = "4.0.0", forRemoval = true) 81 | @JsonProperty("funds_remaining") 82 | public Double getFundsRemaining() { 83 | return fundsRemaining(); 84 | } 85 | 86 | /** 87 | * @return This is a UUID that identifies the minFraud request. 88 | * @deprecated Use {@link #id()} instead. This method will be removed in 5.0.0. 89 | */ 90 | @Deprecated(since = "4.0.0", forRemoval = true) 91 | @JsonProperty("id") 92 | public UUID getId() { 93 | return id(); 94 | } 95 | 96 | /** 97 | * @return The {@code IpAddress} model object. 98 | * @deprecated Use {@link #ipAddress()} instead. This method will be removed in 5.0.0. 99 | */ 100 | @Deprecated(since = "4.0.0", forRemoval = true) 101 | @JsonProperty("ip_address") 102 | public IpAddressInterface getIpAddress() { 103 | return ipAddress(); 104 | } 105 | 106 | /** 107 | * @return The approximate number of queries remaining for this service before your account runs 108 | * out of funds. 109 | * @deprecated Use {@link #queriesRemaining()} instead. This method will be removed in 5.0.0. 110 | */ 111 | @Deprecated(since = "4.0.0", forRemoval = true) 112 | @JsonProperty("queries_remaining") 113 | public Integer getQueriesRemaining() { 114 | return queriesRemaining(); 115 | } 116 | 117 | /** 118 | * @return This returns the risk score, from 0.01 to 99. A higher score indicates a higher risk 119 | * of fraud. For example, a score of 20 indicates a 20% chance that a transaction is 120 | * fraudulent. We never return a risk score of 0, since all transactions have the 121 | * possibility of being fraudulent. Likewise, we never return a risk score of 100. 122 | * @deprecated Use {@link #riskScore()} instead. This method will be removed in 5.0.0. 123 | */ 124 | @Deprecated(since = "4.0.0", forRemoval = true) 125 | @JsonProperty("risk_score") 126 | public Double getRiskScore() { 127 | return riskScore(); 128 | } 129 | 130 | /** 131 | * @return An unmodifiable list containing warning objects that detail issues with the request 132 | * such as invalid or unknown inputs. It is highly recommended that you check this list for 133 | * issues when integrating the web service. 134 | * @deprecated Use {@link #warnings()} instead. This method will be removed in 5.0.0. 135 | */ 136 | @Deprecated(since = "4.0.0", forRemoval = true) 137 | @JsonProperty("warnings") 138 | public List getWarnings() { 139 | return warnings(); 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /src/test/java/com/maxmind/minfraud/response/EmailDomainTest.java: -------------------------------------------------------------------------------- 1 | package com.maxmind.minfraud.response; 2 | 3 | import static org.junit.jupiter.api.Assertions.assertEquals; 4 | import static org.junit.jupiter.api.Assertions.assertNotNull; 5 | import static org.junit.jupiter.api.Assertions.assertNull; 6 | import static org.junit.jupiter.api.Assertions.assertTrue; 7 | 8 | import com.fasterxml.jackson.jr.ob.JSON; 9 | import com.maxmind.minfraud.response.EmailDomain.Classification; 10 | import com.maxmind.minfraud.response.EmailDomainVisit.Status; 11 | import java.time.LocalDate; 12 | import org.junit.jupiter.api.Test; 13 | 14 | public class EmailDomainTest extends AbstractOutputTest { 15 | 16 | @Test 17 | public void testEmailDomain() throws Exception { 18 | EmailDomain domain = this.deserialize( 19 | EmailDomain.class, 20 | JSON.std 21 | .composeString() 22 | .startObject() 23 | .put("first_seen", "2014-02-03") 24 | .end() 25 | .finish() 26 | ); 27 | 28 | assertEquals(LocalDate.parse("2014-02-03"), domain.firstSeen()); 29 | } 30 | 31 | @Test 32 | public void testEmailDomainWithAllFields() throws Exception { 33 | EmailDomain domain = this.deserialize( 34 | EmailDomain.class, 35 | JSON.std 36 | .composeString() 37 | .startObject() 38 | .put("classification", "education") 39 | .put("first_seen", "2019-01-15") 40 | .put("risk", 15.5) 41 | .startObjectField("visit") 42 | .put("has_redirect", true) 43 | .put("last_visited_on", "2024-11-15") 44 | .put("status", "live") 45 | .end() 46 | .put("volume", 630000.0) 47 | .end() 48 | .finish() 49 | ); 50 | 51 | assertEquals(Classification.EDUCATION, domain.classification()); 52 | assertEquals("education", domain.classification().toString()); 53 | assertEquals(LocalDate.parse("2019-01-15"), domain.firstSeen()); 54 | assertEquals(15.5, domain.risk()); 55 | assertEquals(630000.0, domain.volume()); 56 | 57 | assertNotNull(domain.visit()); 58 | assertTrue(domain.visit().hasRedirect()); 59 | assertEquals(LocalDate.parse("2024-11-15"), domain.visit().lastVisitedOn()); 60 | assertEquals(Status.LIVE, domain.visit().status()); 61 | } 62 | 63 | @Test 64 | public void testEmailDomainWithBusinessClassification() throws Exception { 65 | EmailDomain domain = this.deserialize( 66 | EmailDomain.class, 67 | JSON.std 68 | .composeString() 69 | .startObject() 70 | .put("classification", "business") 71 | .put("risk", 5.0) 72 | .end() 73 | .finish() 74 | ); 75 | 76 | assertEquals(Classification.BUSINESS, domain.classification()); 77 | assertEquals("business", domain.classification().toString()); 78 | assertEquals(5.0, domain.risk()); 79 | } 80 | 81 | @Test 82 | public void testEmailDomainWithGovernmentClassification() throws Exception { 83 | EmailDomain domain = this.deserialize( 84 | EmailDomain.class, 85 | JSON.std 86 | .composeString() 87 | .startObject() 88 | .put("classification", "government") 89 | .end() 90 | .finish() 91 | ); 92 | 93 | assertEquals(Classification.GOVERNMENT, domain.classification()); 94 | assertEquals("government", domain.classification().toString()); 95 | } 96 | 97 | @Test 98 | public void testEmailDomainWithIspEmailClassification() throws Exception { 99 | EmailDomain domain = this.deserialize( 100 | EmailDomain.class, 101 | JSON.std 102 | .composeString() 103 | .startObject() 104 | .put("classification", "isp_email") 105 | .put("volume", 500000.5) 106 | .end() 107 | .finish() 108 | ); 109 | 110 | assertEquals(Classification.ISP_EMAIL, domain.classification()); 111 | assertEquals("isp_email", domain.classification().toString()); 112 | assertEquals(500000.5, domain.volume()); 113 | } 114 | 115 | @Test 116 | public void testEmailDomainWithUnknownClassification() throws Exception { 117 | EmailDomain domain = this.deserialize( 118 | EmailDomain.class, 119 | JSON.std 120 | .composeString() 121 | .startObject() 122 | .put("classification", "future_new_classification") 123 | .put("risk", 20.0) 124 | .end() 125 | .finish() 126 | ); 127 | 128 | assertNull(domain.classification()); 129 | assertEquals(20.0, domain.risk()); 130 | } 131 | 132 | @Test 133 | public void testEmailDomainWithVisitOnly() throws Exception { 134 | EmailDomain domain = this.deserialize( 135 | EmailDomain.class, 136 | JSON.std 137 | .composeString() 138 | .startObject() 139 | .startObjectField("visit") 140 | .put("status", "parked") 141 | .put("last_visited_on", "2024-10-20") 142 | .end() 143 | .end() 144 | .finish() 145 | ); 146 | 147 | assertNotNull(domain.visit()); 148 | assertEquals(Status.PARKED, domain.visit().status()); 149 | assertEquals(LocalDate.parse("2024-10-20"), domain.visit().lastVisitedOn()); 150 | } 151 | 152 | @Test 153 | public void testEmailDomainEmpty() throws Exception { 154 | EmailDomain domain = this.deserialize( 155 | EmailDomain.class, 156 | JSON.std 157 | .composeString() 158 | .startObject() 159 | .end() 160 | .finish() 161 | ); 162 | 163 | assertNull(domain.classification()); 164 | assertNull(domain.firstSeen()); 165 | assertNull(domain.risk()); 166 | assertNotNull(domain.visit()); 167 | assertNull(domain.volume()); 168 | } 169 | } 170 | -------------------------------------------------------------------------------- /src/main/java/com/maxmind/minfraud/response/GeoIp2Location.java: -------------------------------------------------------------------------------- 1 | package com.maxmind.minfraud.response; 2 | 3 | import com.fasterxml.jackson.annotation.JsonIgnore; 4 | import com.fasterxml.jackson.annotation.JsonProperty; 5 | import com.maxmind.minfraud.JsonSerializable; 6 | import java.time.ZonedDateTime; 7 | 8 | /** 9 | * This class contains minFraud response data related to the GeoIP2 Insights location. 10 | * 11 | * @param accuracyRadius The approximate accuracy radius in kilometers around the latitude and 12 | * longitude for the geographical entity (country, subdivision, city or 13 | * postal code) associated with the IP address. 14 | * @param averageIncome The average income in US dollars associated with the requested IP 15 | * address. 16 | * @param latitude The approximate latitude of the location associated with the IP 17 | * address. 18 | * @param localTime The date and time of the transaction in the time zone associated with 19 | * the IP address. The value is formatted according to RFC 3339. For 20 | * instance, the local time in Boston might be returned as 21 | * "2015-04-27T19:17:24-04:00". 22 | * @param longitude The approximate longitude of the location associated with the IP 23 | * address. 24 | * @param metroCode Deprecated. The no-longer-maintained code for targeting advertisements 25 | * in Google, if the location is in the US. 26 | * @param populationDensity The estimated population per square kilometer associated with the IP 27 | * address. 28 | * @param timeZone The time zone associated with location, as specified by the IANA Time 29 | * Zone Database. 30 | */ 31 | public record GeoIp2Location( 32 | @JsonProperty("accuracy_radius") 33 | Integer accuracyRadius, 34 | 35 | @JsonProperty("average_income") 36 | Integer averageIncome, 37 | 38 | @JsonProperty("latitude") 39 | Double latitude, 40 | 41 | @JsonProperty("local_time") 42 | String localTime, 43 | 44 | @JsonProperty("longitude") 45 | Double longitude, 46 | 47 | @JsonProperty("metro_code") 48 | Integer metroCode, 49 | 50 | @JsonProperty("population_density") 51 | Integer populationDensity, 52 | 53 | @JsonProperty("time_zone") 54 | String timeZone 55 | ) implements JsonSerializable { 56 | 57 | /** 58 | * Constructs an instance of {@code GeoIp2Location} with no data. 59 | */ 60 | public GeoIp2Location() { 61 | this(null, null, null, null, null, null, null, null); 62 | } 63 | 64 | /** 65 | * @return The approximate accuracy radius in kilometers around the latitude and longitude 66 | * for the geographical entity (country, subdivision, city or postal code) associated 67 | * with the IP address. 68 | * @deprecated Use {@link #accuracyRadius()} instead. This method will be removed in 5.0.0. 69 | */ 70 | @Deprecated(since = "4.0.0", forRemoval = true) 71 | @JsonProperty("accuracy_radius") 72 | public Integer getAccuracyRadius() { 73 | return accuracyRadius(); 74 | } 75 | 76 | /** 77 | * @return The average income in US dollars associated with the requested IP address. 78 | * @deprecated Use {@link #averageIncome()} instead. This method will be removed in 5.0.0. 79 | */ 80 | @Deprecated(since = "4.0.0", forRemoval = true) 81 | @JsonProperty("average_income") 82 | public Integer getAverageIncome() { 83 | return averageIncome(); 84 | } 85 | 86 | /** 87 | * @return The approximate latitude of the location associated with the IP address. 88 | * @deprecated Use {@link #latitude()} instead. This method will be removed in 5.0.0. 89 | */ 90 | @Deprecated(since = "4.0.0", forRemoval = true) 91 | @JsonProperty("latitude") 92 | public Double getLatitude() { 93 | return latitude(); 94 | } 95 | 96 | /** 97 | * @return The approximate longitude of the location associated with the IP address. 98 | * @deprecated Use {@link #longitude()} instead. This method will be removed in 5.0.0. 99 | */ 100 | @Deprecated(since = "4.0.0", forRemoval = true) 101 | @JsonProperty("longitude") 102 | public Double getLongitude() { 103 | return longitude(); 104 | } 105 | 106 | /** 107 | * @return The no-longer-maintained code for targeting advertisements in Google. 108 | * @deprecated Use {@link #metroCode()} instead. This method will be removed in 5.0.0. 109 | */ 110 | @Deprecated(since = "4.0.0", forRemoval = true) 111 | @JsonProperty("metro_code") 112 | public Integer getMetroCode() { 113 | return metroCode(); 114 | } 115 | 116 | /** 117 | * @return The estimated population per square kilometer associated with the IP address. 118 | * @deprecated Use {@link #populationDensity()} instead. This method will be removed in 5.0.0. 119 | */ 120 | @Deprecated(since = "4.0.0", forRemoval = true) 121 | @JsonProperty("population_density") 122 | public Integer getPopulationDensity() { 123 | return populationDensity(); 124 | } 125 | 126 | /** 127 | * @return The time zone associated with location, as specified by the IANA Time Zone Database. 128 | * @deprecated Use {@link #timeZone()} instead. This method will be removed in 5.0.0. 129 | */ 130 | @Deprecated(since = "4.0.0", forRemoval = true) 131 | @JsonProperty("time_zone") 132 | public String getTimeZone() { 133 | return timeZone(); 134 | } 135 | 136 | /** 137 | * @return The date and time of the transaction in the time zone associated with the IP address. 138 | * The value is formatted according to RFC 3339. For instance, the local time in Boston 139 | * might be returned as "2015-04-27T19:17:24-04:00". 140 | * @deprecated Use {@link #localTime()} instead. This method will be removed in 5.0.0. 141 | */ 142 | @Deprecated(since = "4.0.0", forRemoval = true) 143 | @JsonProperty("local_time") 144 | public String getLocalTime() { 145 | return localTime(); 146 | } 147 | 148 | /** 149 | * @return The date and time of the transaction in the time zone associated with the IP address 150 | * as a {@code ZonedDateTime}. 151 | */ 152 | @JsonIgnore 153 | public ZonedDateTime getLocalDateTime() { 154 | if (localTime == null) { 155 | return null; 156 | } 157 | return ZonedDateTime.parse(localTime); 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /src/main/java/com/maxmind/minfraud/request/Order.java: -------------------------------------------------------------------------------- 1 | package com.maxmind.minfraud.request; 2 | 3 | import com.fasterxml.jackson.annotation.JsonProperty; 4 | import com.maxmind.minfraud.AbstractModel; 5 | import java.math.BigDecimal; 6 | import java.net.URI; 7 | import java.util.regex.Pattern; 8 | 9 | /** 10 | * The order information for the transaction. 11 | */ 12 | public final class Order extends AbstractModel { 13 | private final BigDecimal amount; 14 | private final String currency; 15 | private final String discountCode; 16 | private final String affiliateId; 17 | private final String subaffiliateId; 18 | private final URI referrerUri; 19 | private final Boolean isGift; 20 | private final Boolean hasGiftMessage; 21 | 22 | private Order(Order.Builder builder) { 23 | amount = builder.amount; 24 | currency = builder.currency; 25 | discountCode = builder.discountCode; 26 | affiliateId = builder.affiliateId; 27 | subaffiliateId = builder.subaffiliateId; 28 | referrerUri = builder.referrerUri; 29 | isGift = builder.isGift; 30 | hasGiftMessage = builder.hasGiftMessage; 31 | } 32 | 33 | /** 34 | * {@code Builder} creates instances of {@code Order} from values set by the builder's methods. 35 | */ 36 | public static final class Builder { 37 | private static final Pattern CURRENCY_CODE_PATTERN = Pattern.compile("^[A-Z]{3}$"); 38 | 39 | BigDecimal amount; 40 | String currency; 41 | String discountCode; 42 | String affiliateId; 43 | String subaffiliateId; 44 | URI referrerUri; 45 | private Boolean isGift; 46 | private Boolean hasGiftMessage; 47 | 48 | /** 49 | * @param amount The total order amount for the transaction. 50 | * @return The builder object. 51 | */ 52 | public Order.Builder amount(BigDecimal amount) { 53 | this.amount = amount; 54 | return this; 55 | } 56 | 57 | /** 58 | * @param amount The total order amount for the transaction. 59 | * @return The builder object. 60 | */ 61 | public Order.Builder amount(Double amount) { 62 | this.amount = BigDecimal.valueOf(amount); 63 | return this; 64 | } 65 | 66 | /** 67 | * @param code The ISO 4217 currency code for the currency used in the transaction. 68 | * @return The builder object. 69 | * @throws IllegalArgumentException when currency is not a valid three-letter currency 70 | * code. 71 | */ 72 | public Order.Builder currency(String code) { 73 | if (!CURRENCY_CODE_PATTERN.matcher(code).matches()) { 74 | throw new IllegalArgumentException("The currency code " + code + " is invalid."); 75 | } 76 | currency = code; 77 | return this; 78 | } 79 | 80 | /** 81 | * @param code The discount code applied to the transaction. If multiple discount codes were 82 | * used, please separate them with a comma. 83 | * @return The builder object. 84 | */ 85 | public Order.Builder discountCode(String code) { 86 | discountCode = code; 87 | return this; 88 | } 89 | 90 | /** 91 | * @param id The ID of the affiliate where the order is coming from. 92 | * @return The builder object. 93 | */ 94 | public Order.Builder affiliateId(String id) { 95 | affiliateId = id; 96 | return this; 97 | } 98 | 99 | /** 100 | * @param isGift Whether order was marked as a gift by the purchaser. 101 | * @return The builder object. 102 | */ 103 | public Order.Builder isGift(boolean isGift) { 104 | this.isGift = isGift; 105 | return this; 106 | } 107 | 108 | /** 109 | * @param hasGiftMessage Whether the purchaser included a gift message. 110 | * @return The builder object. 111 | */ 112 | public Order.Builder hasGiftMessage(boolean hasGiftMessage) { 113 | this.hasGiftMessage = hasGiftMessage; 114 | return this; 115 | } 116 | 117 | /** 118 | * @param id The ID of the sub-affiliate where the order is coming from. 119 | * @return The builder object. 120 | */ 121 | public Order.Builder subaffiliateId(String id) { 122 | subaffiliateId = id; 123 | return this; 124 | } 125 | 126 | /** 127 | * @param uri The URI of the referring site for this order. 128 | * @return The builder object. 129 | */ 130 | public Order.Builder referrerUri(URI uri) { 131 | referrerUri = uri; 132 | return this; 133 | } 134 | 135 | /** 136 | * @return An instance of {@code Order} created from the fields set on this builder. 137 | */ 138 | public Order build() { 139 | return new Order(this); 140 | } 141 | } 142 | 143 | /** 144 | * @return The total order amount. 145 | */ 146 | @JsonProperty("amount") 147 | public BigDecimal amount() { 148 | return amount; 149 | } 150 | 151 | /** 152 | * @return The currency code. 153 | */ 154 | @JsonProperty("currency") 155 | public String currency() { 156 | return currency; 157 | } 158 | 159 | /** 160 | * @return The discount codes. 161 | */ 162 | @JsonProperty("discount_code") 163 | public String discountCode() { 164 | return discountCode; 165 | } 166 | 167 | /** 168 | * @return The affiliate ID. 169 | */ 170 | @JsonProperty("affiliate_id") 171 | public String affiliateId() { 172 | return affiliateId; 173 | } 174 | 175 | /** 176 | * @return The sub-affiliate ID. 177 | */ 178 | @JsonProperty("subaffiliate_id") 179 | public String subaffiliateId() { 180 | return subaffiliateId; 181 | } 182 | 183 | /** 184 | * @return The referrer URI. 185 | */ 186 | @JsonProperty("referrer_uri") 187 | public URI referrerUri() { 188 | return referrerUri; 189 | } 190 | 191 | /** 192 | * @return The order is a gift. 193 | */ 194 | @JsonProperty("has_gift_message") 195 | public Boolean hasGiftMessage() { 196 | return hasGiftMessage; 197 | } 198 | 199 | /** 200 | * @return The order is a gift. 201 | */ 202 | @JsonProperty("is_gift") 203 | public Boolean isGift() { 204 | return isGift; 205 | } 206 | } 207 | -------------------------------------------------------------------------------- /src/test/resources/test-data/insights-response.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "27d26476-e2bc-11e4-92b8-962e705b4af5", 3 | "risk_score": 0.01, 4 | "funds_remaining": 10.00, 5 | "queries_remaining": 1000, 6 | "ip_address": { 7 | "risk": 0.01, 8 | "risk_reasons": [ 9 | { 10 | "code": "ANONYMOUS_IP", 11 | "reason": "The IP address belongs to an anonymous network. See /ip_address/traits for more details." 12 | }, 13 | { 14 | "code": "MINFRAUD_NETWORK_ACTIVITY", 15 | "reason": "Suspicious activity has been seen on this IP address across minFraud customers." 16 | } 17 | ], 18 | "city": { 19 | "confidence": 42, 20 | "geoname_id": 2643743, 21 | "names": { 22 | "de": "London", 23 | "en": "London", 24 | "es": "Londres", 25 | "fr": "Londres", 26 | "ja": "\u30ed\u30f3\u30c9\u30f3", 27 | "pt-BR": "Londres", 28 | "ru": "\u041b\u043e\u043d\u0434\u043e\u043d" 29 | } 30 | }, 31 | "continent": { 32 | "code": "EU", 33 | "geoname_id": 6255148, 34 | "names": { 35 | "de": "Europa", 36 | "en": "Europe", 37 | "es": "Europa", 38 | "fr": "Europe", 39 | "ja": "\u30e8\u30fc\u30ed\u30c3\u30d1", 40 | "pt-BR": "Europa", 41 | "ru": "\u0415\u0432\u0440\u043e\u043f\u0430", 42 | "zh-CN": "\u6b27\u6d32" 43 | } 44 | }, 45 | "country": { 46 | "confidence": 99, 47 | "geoname_id": 2635167, 48 | "is_in_european_union": true, 49 | "iso_code": "GB", 50 | "names": { 51 | "de": "Vereinigtes K\u00f6nigreich", 52 | "en": "United Kingdom", 53 | "es": "Reino Unido", 54 | "fr": "Royaume-Uni", 55 | "ja": "\u30a4\u30ae\u30ea\u30b9", 56 | "pt-BR": "Reino Unido", 57 | "ru": "\u0412\u0435\u043b\u0438\u043a\u043e\u0431\u0440\u0438\u0442\u0430\u043d\u0438\u044f", 58 | "zh-CN": "\u82f1\u56fd" 59 | } 60 | }, 61 | "location": { 62 | "accuracy_radius": 96, 63 | "latitude": 51.5142, 64 | "local_time": "2012-04-13T00:20:50+01:00", 65 | "longitude": -0.0931, 66 | "time_zone": "Europe\/London" 67 | }, 68 | "registered_country": { 69 | "geoname_id": 6252001, 70 | "iso_code": "US", 71 | "names": { 72 | "de": "USA", 73 | "en": "United States", 74 | "es": "Estados Unidos", 75 | "fr": "\u00c9tats-Unis", 76 | "ja": "\u30a2\u30e1\u30ea\u30ab\u5408\u8846\u56fd", 77 | "pt-BR": "Estados Unidos", 78 | "ru": "\u0421\u0428\u0410", 79 | "zh-CN": "\u7f8e\u56fd" 80 | } 81 | }, 82 | "represented_country": { 83 | "geoname_id": 2635167, 84 | "is_in_european_union": true, 85 | "iso_code": "GB", 86 | "names": { 87 | "de": "Vereinigtes K\u00f6nigreich", 88 | "en": "United Kingdom", 89 | "es": "Reino Unido", 90 | "fr": "Royaume-Uni", 91 | "ja": "\u30a4\u30ae\u30ea\u30b9", 92 | "pt-BR": "Reino Unido", 93 | "ru": "\u0412\u0435\u043b\u0438\u043a\u043e\u0431\u0440\u0438\u0442\u0430\u043d\u0438\u044f", 94 | "zh-CN": "\u82f1\u56fd" 95 | } 96 | }, 97 | "subdivisions": [ 98 | { 99 | "confidence": 42, 100 | "geoname_id": 6269131, 101 | "iso_code": "ENG", 102 | "names": { 103 | "en": "England", 104 | "es": "Inglaterra", 105 | "fr": "Angleterre", 106 | "pt-BR": "Inglaterra" 107 | } 108 | } 109 | ], 110 | "traits": { 111 | "connection_type": "Cable/DSL", 112 | "domain": "in-addr.arpa", 113 | "ip_address": "152.216.7.110", 114 | "is_anonymous": true, 115 | "is_anonymous_vpn": true, 116 | "is_hosting_provider": true, 117 | "is_public_proxy": true, 118 | "is_tor_exit_node": true, 119 | "isp": "Andrews & Arnold Ltd", 120 | "mobile_country_code": "310", 121 | "mobile_network_code": "004", 122 | "network": "81.2.69.0/24", 123 | "organization": "STONEHOUSE office network", 124 | "user_type": "government" 125 | } 126 | }, 127 | "billing_address": { 128 | "is_postal_in_city": false, 129 | "latitude": 41.310571, 130 | "longitude": -72.922891, 131 | "distance_to_ip_location": 5465, 132 | "is_in_ip_country": false 133 | }, 134 | "billing_phone": { 135 | "country": "US", 136 | "is_voip": false, 137 | "matches_postal": true, 138 | "network_operator": "Verizon/1", 139 | "number_type": "fixed" 140 | }, 141 | "credit_card": { 142 | "issuer": { 143 | "name": "Bank of No Hope", 144 | "matches_provided_name": true, 145 | "phone_number": "8003421232", 146 | "matches_provided_phone_number": true 147 | }, 148 | "brand": "Visa", 149 | "country": "US", 150 | "is_business": true, 151 | "is_issued_in_billing_address_country": true, 152 | "is_prepaid": true, 153 | "is_virtual": true, 154 | "type": "credit" 155 | }, 156 | "device": { 157 | "confidence": 99, 158 | "id": "7835b099-d385-4e5b-969e-7df26181d73b", 159 | "last_seen": "2016-06-08T14:16:38Z", 160 | "local_time": "2018-04-05T15:34:40-07:00" 161 | }, 162 | "disposition": { 163 | "action": "reject", 164 | "reason": "custom_rule", 165 | "rule_label": "the label" 166 | }, 167 | "email": { 168 | "domain": { 169 | "classification": "education", 170 | "first_seen": "2014-02-23", 171 | "risk": 15.5, 172 | "visit": { 173 | "has_redirect": true, 174 | "last_visited_on": "2024-11-15", 175 | "status": "live" 176 | }, 177 | "volume": 630000.0 178 | }, 179 | "first_seen": "2017-01-02", 180 | "is_disposable": true, 181 | "is_free": true, 182 | "is_high_risk": true 183 | }, 184 | "shipping_address": { 185 | "distance_to_billing_address": 2227, 186 | "distance_to_ip_location": 7456, 187 | "is_in_ip_country": false, 188 | "is_high_risk": false, 189 | "is_postal_in_city": false, 190 | "latitude": 35.704729, 191 | "longitude": -97.568619 192 | }, 193 | "shipping_phone": { 194 | "country": "CA", 195 | "is_voip": true, 196 | "matches_postal": false, 197 | "network_operator": "Telus Mobility-SVR/2", 198 | "number_type": "mobile" 199 | }, 200 | "warnings": [ 201 | { 202 | "code": "INPUT_INVALID", 203 | "input_pointer": "/account/user_id", 204 | "warning": "Encountered value at \/account\/user_id that does meet the required constraints" 205 | }, 206 | { 207 | "code": "INPUT_INVALID", 208 | "input_pointer": "/account/username_md5", 209 | "warning": "Encountered value at \/account\/username_md5 that does meet the required constraints" 210 | } 211 | ] 212 | } 213 | -------------------------------------------------------------------------------- /src/main/java/com/maxmind/minfraud/request/Event.java: -------------------------------------------------------------------------------- 1 | package com.maxmind.minfraud.request; 2 | 3 | import com.fasterxml.jackson.annotation.JsonIgnore; 4 | import com.fasterxml.jackson.annotation.JsonProperty; 5 | import com.maxmind.minfraud.AbstractModel; 6 | import java.time.ZoneId; 7 | import java.time.ZonedDateTime; 8 | import java.util.Date; 9 | 10 | /** 11 | * This class contains general information related to the event being scored. 12 | */ 13 | public final class Event extends AbstractModel { 14 | 15 | private final Party party; 16 | private final String transactionId; 17 | private final String shopId; 18 | private final ZonedDateTime time; 19 | private final Type type; 20 | 21 | private Event(Event.Builder builder) { 22 | party = builder.party; 23 | transactionId = builder.transactionId; 24 | shopId = builder.shopId; 25 | time = builder.time; 26 | type = builder.type; 27 | } 28 | 29 | /** 30 | * {@code Builder} creates instances of {@code Event} from values set by the builder's methods. 31 | */ 32 | public static final class Builder { 33 | Party party; 34 | String transactionId; 35 | String shopId; 36 | ZonedDateTime time; 37 | Type type; 38 | 39 | /** 40 | * @param party The party submitting the transaction. 41 | * @return The builder object. 42 | */ 43 | public Event.Builder party(Party party) { 44 | this.party = party; 45 | return this; 46 | } 47 | 48 | /** 49 | * @param id Your internal ID for the transaction. We can use this to locate a specific 50 | * transaction in our logs, and it will also show up in email alerts and 51 | * notifications from us to you. 52 | * @return The builder object. 53 | */ 54 | public Event.Builder transactionId(String id) { 55 | this.transactionId = id; 56 | return this; 57 | } 58 | 59 | /** 60 | * @param id Your internal ID for the shop, affiliate, or merchant this order is coming 61 | * from. Required for minFraud users who are resellers, payment providers, 62 | * gateways and affiliate networks. 63 | * @return The builder object. 64 | */ 65 | public Event.Builder shopId(String id) { 66 | this.shopId = id; 67 | return this; 68 | } 69 | 70 | /** 71 | * @param date The date and time the event occurred. 72 | * @return The builder object. 73 | */ 74 | public Event.Builder time(Date date) { 75 | time = date.toInstant().atZone(ZoneId.systemDefault()); 76 | return this; 77 | } 78 | 79 | /** 80 | * @param date The date and time the event occurred. 81 | * @return The builder object. 82 | */ 83 | public Event.Builder time(ZonedDateTime date) { 84 | time = date; 85 | return this; 86 | } 87 | 88 | /** 89 | * @param type The type of event being scored. 90 | * @return The builder object. 91 | */ 92 | public Event.Builder type(Type type) { 93 | this.type = type; 94 | return this; 95 | } 96 | 97 | /** 98 | * @return An instance of {@code Event} created from the fields set on this builder. 99 | */ 100 | public Event build() { 101 | return new Event(this); 102 | } 103 | } 104 | 105 | /** 106 | * @return The party submitting the transaction. 107 | */ 108 | @JsonProperty("party") 109 | public Party party() { 110 | return party; 111 | } 112 | 113 | /** 114 | * @return The transaction ID. 115 | */ 116 | @JsonProperty("transaction_id") 117 | public String transactionId() { 118 | return transactionId; 119 | } 120 | 121 | /** 122 | * @return The shop ID. 123 | */ 124 | @JsonProperty("shop_id") 125 | public String shopId() { 126 | return shopId; 127 | } 128 | 129 | /** 130 | * @return The date and time of the event. 131 | */ 132 | @JsonIgnore 133 | public Date time() { 134 | return Date.from(time.toInstant()); 135 | } 136 | 137 | /** 138 | * @return The date and time of the event. 139 | */ 140 | @JsonProperty("time") 141 | public ZonedDateTime dateTime() { 142 | return time; 143 | } 144 | 145 | /** 146 | * @return The type of the event. 147 | */ 148 | @JsonProperty("type") 149 | public Type type() { 150 | return type; 151 | } 152 | 153 | /** 154 | * The enumerated event types. 155 | */ 156 | public enum Type { 157 | /** 158 | * The account was created 159 | */ 160 | ACCOUNT_CREATION, 161 | /** 162 | * The account was logged into 163 | */ 164 | ACCOUNT_LOGIN, 165 | /** 166 | * A credit application was submitted 167 | */ 168 | CREDIT_APPLICATION, 169 | /** 170 | * The account email was changed 171 | */ 172 | EMAIL_CHANGE, 173 | /** 174 | * A fund transfer was initiated 175 | */ 176 | FUND_TRANSFER, 177 | /** 178 | * The account password was reset 179 | */ 180 | PASSWORD_RESET, 181 | /** 182 | * The account payout was changed 183 | */ 184 | PAYOUT_CHANGE, 185 | /** 186 | * A purchase was made 187 | */ 188 | PURCHASE, 189 | /** 190 | * A recurring purchase was made 191 | */ 192 | RECURRING_PURCHASE, 193 | /** 194 | * A referral was made 195 | */ 196 | REFERRAL, 197 | /** 198 | * A SIM card was swapped 199 | */ 200 | SIM_SWAP, 201 | /** 202 | * A survey was completed 203 | */ 204 | SURVEY; 205 | 206 | /** 207 | * @return a string representation of the object. 208 | */ 209 | public String toString() { 210 | return this.name().toLowerCase(); 211 | } 212 | } 213 | 214 | /** 215 | * The enumerated event party types. 216 | */ 217 | public enum Party { 218 | /** 219 | * An agent is submitting the transaction 220 | */ 221 | AGENT, 222 | /** 223 | * A customer is submitting the transaction 224 | */ 225 | CUSTOMER; 226 | 227 | /** 228 | * @return a string representation of the object. 229 | */ 230 | public String toString() { 231 | return this.name().toLowerCase(); 232 | } 233 | } 234 | } -------------------------------------------------------------------------------- /src/test/resources/test-data/factors-response.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "27d26476-e2bc-11e4-92b8-962e705b4af5", 3 | "risk_score": 0.01, 4 | "funds_remaining": 10.00, 5 | "queries_remaining": 1000, 6 | "ip_address": { 7 | "risk": 0.01, 8 | "risk_reasons": [ 9 | { 10 | "code": "ANONYMOUS_IP", 11 | "reason": "The IP address belongs to an anonymous network. See /ip_address/traits for more details." 12 | }, 13 | { 14 | "code": "MINFRAUD_NETWORK_ACTIVITY", 15 | "reason": "Suspicious activity has been seen on this IP address across minFraud customers." 16 | } 17 | ], 18 | "city": { 19 | "confidence": 42, 20 | "geoname_id": 2643743, 21 | "names": { 22 | "de": "London", 23 | "en": "London", 24 | "es": "Londres", 25 | "fr": "Londres", 26 | "ja": "\u30ed\u30f3\u30c9\u30f3", 27 | "pt-BR": "Londres", 28 | "ru": "\u041b\u043e\u043d\u0434\u043e\u043d" 29 | } 30 | }, 31 | "continent": { 32 | "code": "EU", 33 | "geoname_id": 6255148, 34 | "names": { 35 | "de": "Europa", 36 | "en": "Europe", 37 | "es": "Europa", 38 | "fr": "Europe", 39 | "ja": "\u30e8\u30fc\u30ed\u30c3\u30d1", 40 | "pt-BR": "Europa", 41 | "ru": "\u0415\u0432\u0440\u043e\u043f\u0430", 42 | "zh-CN": "\u6b27\u6d32" 43 | } 44 | }, 45 | "country": { 46 | "confidence": 99, 47 | "geoname_id": 2635167, 48 | "is_in_european_union": true, 49 | "iso_code": "GB", 50 | "names": { 51 | "de": "Vereinigtes K\u00f6nigreich", 52 | "en": "United Kingdom", 53 | "es": "Reino Unido", 54 | "fr": "Royaume-Uni", 55 | "ja": "\u30a4\u30ae\u30ea\u30b9", 56 | "pt-BR": "Reino Unido", 57 | "ru": "\u0412\u0435\u043b\u0438\u043a\u043e\u0431\u0440\u0438\u0442\u0430\u043d\u0438\u044f", 58 | "zh-CN": "\u82f1\u56fd" 59 | } 60 | }, 61 | "location": { 62 | "accuracy_radius": 96, 63 | "latitude": 51.5142, 64 | "local_time": "2012-04-13T00:20:50+01:00", 65 | "longitude": -0.0931, 66 | "time_zone": "Europe\/London" 67 | }, 68 | "registered_country": { 69 | "geoname_id": 2635167, 70 | "is_in_european_union": true, 71 | "iso_code": "GB", 72 | "names": { 73 | "de": "Vereinigtes K\u00f6nigreich", 74 | "en": "United Kingdom", 75 | "es": "Reino Unido", 76 | "fr": "Royaume-Uni", 77 | "ja": "\u30a4\u30ae\u30ea\u30b9", 78 | "pt-BR": "Reino Unido", 79 | "ru": "\u0412\u0435\u043b\u0438\u043a\u043e\u0431\u0440\u0438\u0442\u0430\u043d\u0438\u044f", 80 | "zh-CN": "\u82f1\u56fd" 81 | } 82 | }, 83 | "represented_country": { 84 | "geoname_id": 6252001, 85 | "is_in_european_union": false, 86 | "iso_code": "US", 87 | "names": { 88 | "de": "Vereinigte Staaten", 89 | "en": "United States", 90 | "es": "Estados Unidos", 91 | "fr": "\u00c9tats-Unis", 92 | "ja": "\u30a2\u30e1\u30ea\u30ab\u5408\u8846\u56fd", 93 | "pt-BR": "Estados Unidos", 94 | "ru": "\u0421\u0428\u0410", 95 | "zh-CN": "\u7f8e\u56fd" 96 | }, 97 | "type": "military" 98 | }, 99 | "subdivisions": [ 100 | { 101 | "confidence": 42, 102 | "geoname_id": 6269131, 103 | "iso_code": "ENG", 104 | "names": { 105 | "en": "England", 106 | "es": "Inglaterra", 107 | "fr": "Angleterre", 108 | "pt-BR": "Inglaterra" 109 | } 110 | } 111 | ], 112 | "traits": { 113 | "connection_type": "Cable/DSL", 114 | "domain": "in-addr.arpa", 115 | "ip_address": "152.216.7.110", 116 | "is_anonymous": true, 117 | "is_anonymous_vpn": true, 118 | "is_hosting_provider": true, 119 | "is_public_proxy": true, 120 | "is_tor_exit_node": true, 121 | "isp": "Andrews & Arnold Ltd", 122 | "mobile_country_code": "310", 123 | "mobile_network_code": "004", 124 | "network": "81.2.69.0/24", 125 | "organization": "STONEHOUSE office network", 126 | "user_type": "government" 127 | } 128 | }, 129 | "billing_address": { 130 | "is_postal_in_city": false, 131 | "latitude": 41.310571, 132 | "longitude": -72.922891, 133 | "distance_to_ip_location": 5465, 134 | "is_in_ip_country": false 135 | }, 136 | "credit_card": { 137 | "issuer": { 138 | "name": "Bank of No Hope", 139 | "matches_provided_name": true, 140 | "phone_number": "8003421232", 141 | "matches_provided_phone_number": true 142 | }, 143 | "brand": "Visa", 144 | "country": "US", 145 | "is_business": true, 146 | "is_issued_in_billing_address_country": true, 147 | "is_prepaid": true, 148 | "type": "credit" 149 | }, 150 | "device": { 151 | "confidence": 99, 152 | "id": "7835b099-d385-4e5b-969e-7df26181d73b", 153 | "last_seen": "2016-06-08T14:16:38Z" 154 | }, 155 | "disposition": { 156 | "action": "reject", 157 | "reason": "custom_rule", 158 | "rule_label": "the label" 159 | }, 160 | "email": { 161 | "domain": { 162 | "classification": "isp_email", 163 | "first_seen": "2014-02-23", 164 | "risk": 25.0, 165 | "visit": { 166 | "has_redirect": false, 167 | "last_visited_on": "2024-10-20", 168 | "status": "parked" 169 | }, 170 | "volume": 500000.5 171 | }, 172 | "first_seen": "2017-01-02", 173 | "is_disposable": true, 174 | "is_free": true, 175 | "is_high_risk": true 176 | }, 177 | "shipping_address": { 178 | "distance_to_billing_address": 2227, 179 | "distance_to_ip_location": 7456, 180 | "is_in_ip_country": false, 181 | "is_high_risk": false, 182 | "is_postal_in_city": false, 183 | "latitude": 35.704729, 184 | "longitude": -97.568619 185 | }, 186 | "warnings": [ 187 | { 188 | "code": "INPUT_INVALID", 189 | "input_pointer": "/account/user_id", 190 | "warning": "Encountered value at \/account\/user_id that does meet the required constraints" 191 | }, 192 | { 193 | "code": "INPUT_INVALID", 194 | "input_pointer": "/account/username_md5", 195 | "warning": "Encountered value at \/account\/username_md5 that does meet the required constraints" 196 | } 197 | ], 198 | "risk_score_reasons": [ 199 | { 200 | "multiplier": 45.0, 201 | "reasons": [ 202 | { 203 | "code": "ANONYMOUS_IP", 204 | "reason": "Risk due to IP being an Anonymous IP" 205 | } 206 | ] 207 | }, 208 | { 209 | "multiplier": 1.8, 210 | "reasons": [ 211 | { 212 | "code": "TIME_OF_DAY", 213 | "reason": "Risk due to local time of day" 214 | } 215 | ] 216 | }, 217 | { 218 | "multiplier": 1.6, 219 | "reasons": [ 220 | { 221 | "reason": "Riskiness of newly-sighted email domain", 222 | "code": "EMAIL_DOMAIN_NEW" 223 | } 224 | ] 225 | }, 226 | { 227 | "multiplier": 0.34, 228 | "reasons": [ 229 | { 230 | "code": "EMAIL_ADDRESS_NEW", 231 | "reason": "Riskiness of newly-sighted email address" 232 | } 233 | ] 234 | } 235 | ] 236 | } 237 | --------------------------------------------------------------------------------