Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,21 @@
CHANGELOG
=========

4.5.0
------------------

* Added `phoneVerificationMethod`, `phoneWasVerificationSuccessful`, and
`phoneVerificationTime` to the `Billing` and `Shipping` request objects.
These describe the most recent verification of the phone number for the
address. The `PhoneVerificationMethod` enum has the values
`DELIVERED_CODE`, `NETWORK`, and `OTHER`. Do not set
`phoneWasVerificationSuccessful` if no verification was attempted.
* Fixed enum serialization and email address hashing in locales that
change case conversion, such as Turkish (`tr`) and Azerbaijani (`az`).
In those locales, some enum values serialized with a dotless `ı`, for
example `account_creatıon`, and the web service rejected them. The library
now uses `Locale.ROOT` for these case conversions.

4.4.0 (2026-07-21)
------------------

Expand Down
4 changes: 2 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -170,14 +170,14 @@ public enum Status {

@Override
public String toString() {
return name().toLowerCase();
return name().toLowerCase(Locale.ROOT);
}
}
```

**How it works:**
- Enum constants use `UPPER_SNAKE_CASE` (e.g., `DNS_ERROR`, `ISP_EMAIL`)
- Override `toString()` to return `name().toLowerCase()` for JSON serialization
- Override `toString()` to return `name().toLowerCase(Locale.ROOT)` for JSON serialization
- The `Mapper` class configures Jackson with:
- `READ_ENUMS_USING_TO_STRING` - Deserializes using `toString()`
- `READ_UNKNOWN_ENUM_VALUES_AS_NULL` - Unknown values become `null` instead of throwing exceptions
Expand Down
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,9 @@ Transaction request = new Transaction.Builder(
.lastName("Smith")
.phoneCountryCode("1")
.phoneNumber("321-321-3211")
.phoneVerificationMethod(Billing.PhoneVerificationMethod.DELIVERED_CODE)
.phoneVerificationTime(ZonedDateTime.parse("2026-10-01T14:30:00Z"))
.phoneWasVerificationSuccessful(true)
.postal("06510")
.region("CT")
.build()
Expand Down Expand Up @@ -254,6 +257,9 @@ Transaction request = new Transaction.Builder(
.postal("55455")
.phoneNumber("313-545-3113")
.phoneCountryCode("1")
.phoneVerificationMethod(Shipping.PhoneVerificationMethod.NETWORK)
.phoneVerificationTime(ZonedDateTime.parse("2026-10-02T09:15:00Z"))
.phoneWasVerificationSuccessful(false)
.deliverySpeed(Shipping.DeliverySpeed.EXPEDITED)
.address("32 Washington Ave.")
.address2("18")
Expand Down
94 changes: 94 additions & 0 deletions src/main/java/com/maxmind/minfraud/request/AbstractLocation.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

import com.fasterxml.jackson.annotation.JsonProperty;
import com.maxmind.minfraud.AbstractModel;
import java.time.ZonedDateTime;
import java.util.Locale;
import java.util.regex.Pattern;

/**
Expand All @@ -20,6 +22,9 @@ public abstract sealed class AbstractLocation extends AbstractModel
private final String postal;
private final String phoneNumber;
private final String phoneCountryCode;
private final PhoneVerificationMethod phoneVerificationMethod;
private final Boolean phoneWasVerificationSuccessful;
private final ZonedDateTime phoneVerificationTime;

protected AbstractLocation(AbstractLocation.Builder builder) {
firstName = builder.firstName;
Expand All @@ -33,6 +38,9 @@ protected AbstractLocation(AbstractLocation.Builder builder) {
postal = builder.postal;
phoneNumber = builder.phoneNumber;
phoneCountryCode = builder.phoneCountryCode;
phoneVerificationMethod = builder.phoneVerificationMethod;
phoneWasVerificationSuccessful = builder.phoneWasVerificationSuccessful;
phoneVerificationTime = builder.phoneVerificationTime;
}

/**
Expand All @@ -56,6 +64,9 @@ abstract static class Builder<T extends AbstractLocation.Builder<T>> {
String postal;
String phoneNumber;
String phoneCountryCode;
PhoneVerificationMethod phoneVerificationMethod;
Boolean phoneWasVerificationSuccessful;
ZonedDateTime phoneVerificationTime;

/**
* @param name The first name associated with the address
Expand Down Expand Up @@ -162,6 +173,37 @@ public final T phoneNumber(String number) {
return (T) this;
}

/**
* @param method The most recent method used to verify the phone number associated with
* the address
* @return The builder object.
*/
public final T phoneVerificationMethod(PhoneVerificationMethod method) {
phoneVerificationMethod = method;
return (T) this;
}

/**
* @param wasSuccessful Whether the most recent verification of the phone number
* associated with the address succeeded. Do not set this if no
* verification was attempted.
* @return The builder object.
*/
public final T phoneWasVerificationSuccessful(Boolean wasSuccessful) {
phoneWasVerificationSuccessful = wasSuccessful;
return (T) this;
}

/**
* @param time The date and time of the most recent verification of the phone number
* associated with the address
* @return The builder object.
*/
public final T phoneVerificationTime(ZonedDateTime time) {
phoneVerificationTime = time;
return (T) this;
}

abstract AbstractLocation build();
}

Expand Down Expand Up @@ -257,4 +299,56 @@ public final String phoneNumber() {
public final String phoneCountryCode() {
return phoneCountryCode;
}

/**
* @return The most recent method used to verify the phone number associated with the address
*/
@JsonProperty("phone_verification_method")
public final PhoneVerificationMethod phoneVerificationMethod() {
return phoneVerificationMethod;
}

/**
* @return Whether the most recent verification of the phone number associated with the
* address succeeded
*/
@JsonProperty("phone_was_verification_successful")
public final Boolean phoneWasVerificationSuccessful() {
return phoneWasVerificationSuccessful;
}

/**
* @return The date and time of the most recent verification of the phone number associated
* with the address
*/
@JsonProperty("phone_verification_time")
public final ZonedDateTime phoneVerificationTime() {
return phoneVerificationTime;
}

/**
* Enumerated phone verification methods.
*/
public enum PhoneVerificationMethod {
/**
* A code delivered to the phone, such as by SMS, voice call, or messaging app
*/
DELIVERED_CODE,
/**
* Verification through the mobile network operator, such as silent network
* authentication
*/
NETWORK,
/**
* Another verification method
*/
OTHER;

/**
* @return a string representation of the object.
*/
public String toString() {
return this.name().toLowerCase(Locale.ROOT);
}
}
}
3 changes: 2 additions & 1 deletion src/main/java/com/maxmind/minfraud/request/Email.java
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import java.util.regex.Pattern;

Expand Down Expand Up @@ -416,7 +417,7 @@ public String address() {
}

private String cleanAddress(String address) {
address = address.trim().toLowerCase();
address = address.trim().toLowerCase(Locale.ROOT);

int domainIndex = address.lastIndexOf('@');
if (domainIndex == -1 || domainIndex + 1 == address.length()) {
Expand Down
5 changes: 3 additions & 2 deletions src/main/java/com/maxmind/minfraud/request/Event.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.util.Date;
import java.util.Locale;

/**
* This class contains general information related to the event being scored.
Expand Down Expand Up @@ -207,7 +208,7 @@ public enum Type {
* @return a string representation of the object.
*/
public String toString() {
return this.name().toLowerCase();
return this.name().toLowerCase(Locale.ROOT);
}
}

Expand All @@ -228,7 +229,7 @@ public enum Party {
* @return a string representation of the object.
*/
public String toString() {
return this.name().toLowerCase();
return this.name().toLowerCase(Locale.ROOT);
}
}
}
5 changes: 3 additions & 2 deletions src/main/java/com/maxmind/minfraud/request/Payment.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import com.fasterxml.jackson.annotation.JsonProperty;
import com.maxmind.minfraud.AbstractModel;
import java.util.Locale;

/**
* The payment information for the transaction.
Expand Down Expand Up @@ -283,7 +284,7 @@ public enum Processor {
* @return a string representation of the object.
*/
public String toString() {
return this.name().toLowerCase();
return this.name().toLowerCase(Locale.ROOT);
}
}

Expand Down Expand Up @@ -336,7 +337,7 @@ public enum Method {
* @return a string representation of the object.
*/
public String toString() {
return this.name().toLowerCase();
return this.name().toLowerCase(Locale.ROOT);
}
}
}
3 changes: 2 additions & 1 deletion src/main/java/com/maxmind/minfraud/request/Shipping.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.maxmind.minfraud.request;

import com.fasterxml.jackson.annotation.JsonProperty;
import java.util.Locale;

/**
* The shipping information for the transaction.
Expand Down Expand Up @@ -71,7 +72,7 @@ public enum DeliverySpeed {
* @return a string representation of the object.
*/
public String toString() {
return this.name().toLowerCase();
return this.name().toLowerCase(Locale.ROOT);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import com.fasterxml.jackson.annotation.JsonProperty;
import com.maxmind.minfraud.AbstractModel;
import java.net.InetAddress;
import java.util.Locale;
import java.util.UUID;

/**
Expand Down Expand Up @@ -231,7 +232,7 @@ public enum Tag {
* @return a string representation of the object.
*/
public String toString() {
return this.name().toLowerCase();
return this.name().toLowerCase(Locale.ROOT);
}
}

Expand Down
3 changes: 2 additions & 1 deletion src/main/java/com/maxmind/minfraud/response/EmailDomain.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import com.fasterxml.jackson.annotation.JsonProperty;
import com.maxmind.minfraud.JsonSerializable;
import java.time.LocalDate;
import java.util.Locale;

/**
* This class contains minFraud response data related to the email domain.
Expand Down Expand Up @@ -63,7 +64,7 @@ public enum Classification {
*/
@Override
public String toString() {
return name().toLowerCase();
return name().toLowerCase(Locale.ROOT);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import com.fasterxml.jackson.annotation.JsonProperty;
import com.maxmind.minfraud.JsonSerializable;
import java.time.LocalDate;
import java.util.Locale;

/**
* This class contains information about an automated visit to the email domain.
Expand Down Expand Up @@ -63,7 +64,7 @@ public enum Status {
*/
@Override
public String toString() {
return name().toLowerCase();
return name().toLowerCase(Locale.ROOT);
}
}

Expand Down
65 changes: 65 additions & 0 deletions src/test/java/com/maxmind/minfraud/LocaleTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package com.maxmind.minfraud;

import static org.junit.jupiter.api.Assertions.assertEquals;

import com.maxmind.minfraud.request.AbstractLocation;
import com.maxmind.minfraud.request.Email;
import com.maxmind.minfraud.request.Event;
import com.maxmind.minfraud.request.Payment;
import com.maxmind.minfraud.request.Shipping;
import com.maxmind.minfraud.request.TransactionReport;
import com.maxmind.minfraud.response.EmailDomain;
import com.maxmind.minfraud.response.EmailDomainVisit;
import java.util.List;
import java.util.Locale;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

/**
* Checks that the default locale does not change serialized values. In the tr-TR locale,
* {@code "I".toLowerCase()} is a dotless "ı".
*/
public class LocaleTest {
private final Locale originalLocale = Locale.getDefault();

@BeforeEach
public void setTurkishLocale() {
Locale.setDefault(Locale.forLanguageTag("tr-TR"));
}

@AfterEach
public void restoreLocale() {
Locale.setDefault(originalLocale);
}

@Test
public void testEnumToString() {
var values = List.<Enum<?>[]>of(
AbstractLocation.PhoneVerificationMethod.values(),
Shipping.DeliverySpeed.values(),
Payment.Processor.values(),
Payment.Method.values(),
TransactionReport.Tag.values(),
Event.Type.values(),
Event.Party.values(),
EmailDomain.Classification.values(),
EmailDomainVisit.Status.values()
);
for (var enumValues : values) {
for (var value : enumValues) {
assertEquals(
value.name().toLowerCase(Locale.ROOT),
value.toString(),
value.getClass().getName() + "." + value.name()
);
}
}
}

@Test
public void testEmailAddressLowercasing() {
var email = new Email.Builder().address("INFO@TEST.org").hashAddress().build();
assertEquals("fad0cdbc3804b804116ea8972221d2a1", email.address());
}
}
Loading
Loading