Skip to content
Merged
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
18 changes: 18 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,24 @@
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
`phoneVerificationMethod` or `phoneWasVerificationSuccessful` if no
verification was attempted.
* Fixed enum serialization, enum deserialization, 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. Also, some `EmailDomain.Classification` and `EmailDomainVisit.Status`
response values, for example `business` and `live`, deserialized as
`null`. 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)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: PhoneVerificationMethod is declared in AbstractLocation. The README (here and at line 260) and RequestTestHelper get it through the subclasses as Billing.PhoneVerificationMethod and Shipping.PhoneVerificationMethod. A reader can think that these are two different types. Also, the enum does not show in the Billing or Shipping Javadoc. Some IDE inspections flag a static member that is accessed through a subclass.

Suggestion: use AbstractLocation.PhoneVerificationMethod in the README, and give that name in the CHANGELOG.

馃 Comment by Claude Opus 5.5.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, mentioning AbstractLocation seems odd though.

.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
96 changes: 96 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,39 @@ 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. Do not set this if no verification was attempted.
* @return The builder object.
*/
public final T phoneVerificationMethod(PhoneVerificationMethod method) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: The Javadoc for phoneWasVerificationSuccessful says "Do not set this if no verification was attempted". The Javadoc for the method field does not say this. dev-site gives the same rule for phone_verification_method: "Do not include this field if no verification was attempted." Without it, an integrator can set OTHER as a default when no verification occurred. Add the same sentence here and in the CHANGELOG entry.

馃 Comment by Claude Opus 5.5.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 828dec1, also in the CHANGELOG.

Reply by Claude Opus 5.5 on Greg's behalf.

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. The time must not be before

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: I saw the earlier thread about a range check, and I agree with the decision not to add one. But this Javadoc now states a rule that the client does not enforce. A reader can think that the builder checks the range.

Suggestion: tell the reader what occurs, as dev-site does. For example: "The web service ignores a time outside this range and returns a warning."

馃 Comment by Claude Opus 5.5.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 828dec1.

Reply by Claude Opus 5.5 on Greg's behalf.

* 1970-01-01T00:00:00Z or more than two days in the future. The web
* service ignores a time outside this range and returns a warning.
* @return The builder object.
*/
public final T phoneVerificationTime(ZonedDateTime time) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Question: Event.Builder has time(Date) and time(ZonedDateTime). This builder has only phoneVerificationTime(ZonedDateTime). Do you want the API for the two time inputs to be the same? If yes, add a Date overload. If ZonedDateTime only is intentional (for example, because java.util.Date is a legacy type), you can ignore this comment.

馃 Comment by Claude Opus 5.5.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ZonedDateTime only is intentional, because java.util.Date is a legacy type.

Reply by Claude Opus 5.5 on Greg's behalf.

phoneVerificationTime = time;
return (T) this;
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

abstract AbstractLocation build();
}

Expand Down Expand Up @@ -257,4 +301,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() {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor: On line 326, Jackson serializes phone_verification_time with ISO_OFFSET_DATE_TIME and keeps the caller's offset. If the offset has a seconds part, the output is not RFC 3339.

I checked this on the branch. ZonedDateTime.of(1971, 6, 1, 0, 0, 0, 0, ZoneId.of("Africa/Monrovia")) serializes as "1971-06-01T00:00:00-00:44:30". The same thing occurs for any ZoneOffset.ofTotalSeconds(...) with a nonzero seconds part. RFC 3339 allows only [+-]HH:MM, so the service ignores the input and returns a warning, although the time is in the valid range.

This is an edge case. Event.time has the same behavior. A simple fix is to convert to UTC before serialization, for example with withZoneSameInstant(ZoneOffset.UTC) in the accessor or in a custom serializer. Then every value is ...Z. If you prefer to keep the caller's offset, you can ignore this comment.

馃 Comment by Claude Opus 5.5.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We will keep the caller's offset. This is an edge case, and Event.time works the same way.

Reply by Claude Opus 5.5 on Greg's behalf.

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
Loading
Loading