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
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,13 @@ CHANGELOG
* The PHPDoc for the `with()` and `with*()` methods of `MaxMind\MinFraud` now
lists the `InvalidInputException` they throw when input validation is
enabled.
* Added the `phone_verification_method`, `phone_was_verification_successful`,
and `phone_verification_time` inputs to the `/billing` object. These describe
the most recent verification of the billing phone number. Use them with
`withBilling()` as array keys or as the `phoneVerificationMethod`,
`phoneWasVerificationSuccessful`, and `phoneVerificationTime` named
arguments. Omit `phone_was_verification_successful` if no verification was
attempted.

3.7.0 (2026-07-21)
------------------
Expand Down
5 changes: 4 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -182,7 +182,10 @@ $request = $mf->withDevice(
country: 'US',
postal: '06510',
phoneNumber: '123-456-7890',
phoneCountryCode: '1'
phoneCountryCode: '1',
phoneVerificationMethod: 'delivered_code',
phoneWasVerificationSuccessful: true,
phoneVerificationTime: '2026-10-01T14:30:00Z'
)->withShipping(
firstName: 'ShipFirst',
lastName: 'ShipLast',
Expand Down
114 changes: 81 additions & 33 deletions src/MinFraud.php
Original file line number Diff line number Diff line change
Expand Up @@ -393,12 +393,7 @@ public function withEvent(
}

if ($time !== null) {
if (\DateTime::createFromFormat(\DateTime::RFC3339, $time) === false
&& \DateTime::createFromFormat(\DateTime::RFC3339_EXTENDED, $time) === false
) {
$this->maybeThrowInvalidInputException("$time is not a valid RFC 3339 formatted datetime string");
}

$this->verifyRfc3339DateTime($time);
$values['time'] = $time;
}

Expand Down Expand Up @@ -561,33 +556,43 @@ public function withEmail(
* @link https://dev.maxmind.com/minfraud/api-documentation/requests/?lang=en#schema--request--billing
* minFraud billing API docs
*
* @param array<string, mixed> $values An array of billing data. The keys are the same as
* the JSON keys. You may use either this or the named
* arguments, but not both.
* @param string|null $address The first line of the user's billing address
* @param string|null $address2 The second line of the user's billing address
* @param string|null $city The city of the user's billing address
* @param string|null $company The company of the end user as provided in
* their billing information
* @param string|null $country The two character ISO 3166-1 alpha-2 country
* code of the user's billing address
* @param string|null $firstName The first name of the end user as provided
* in their billing information
* @param string|null $lastName The last name of the end user as provided
* in their billing information
* @param string|null $phoneCountryCode The country code for phone number
* associated with the user's billing
* address. If you provide this
* information then you must provide
* at least one digit.
* @param string|null $phoneNumber The phone number without the country code
* for the user's billing address. Punctuation
* characters will be stripped. After
* stripping punctuation characters, the
* number must contain only digits.
* @param string|null $postal The postal code of the user's billing address
* @param string|null $region The ISO 3166-2 subdivision code for the user's
* billing address
* @param array<string, mixed> $values An array of billing data. The keys are the same as
* the JSON keys. You may use either this or the named
* arguments, but not both.
* @param string|null $address The first line of the user's billing address
* @param string|null $address2 The second line of the user's billing address
* @param string|null $city The city of the user's billing address
* @param string|null $company The company of the end user as provided in
* their billing information
* @param string|null $country The two character ISO 3166-1 alpha-2 country
* code of the user's billing address
* @param string|null $firstName The first name of the end user as provided
* in their billing information
* @param string|null $lastName The last name of the end user as provided
* in their billing information
* @param string|null $phoneCountryCode The country code for phone number
* associated with the user's billing
* address. If you provide this
* information then you must provide
* at least one digit.
* @param string|null $phoneNumber The phone number without the country code
* for the user's billing address. Punctuation
* characters will be stripped. After
* stripping punctuation characters, the
* number must contain only digits.
* @param string|null $phoneVerificationMethod The most recent method used to verify
* the billing phone number: delivered_code,
* network, or other
* @param string|null $phoneVerificationTime The date and time of the most recent
* verification of the billing phone number,
* in RFC 3339 date-time format
* @param bool|null $phoneWasVerificationSuccessful Whether the most recent verification
* of the billing phone number succeeded.
* Omit this if no verification was
* attempted.
* @param string|null $postal The postal code of the user's billing address
* @param string|null $region The ISO 3166-2 subdivision code for the user's
* billing address
*
* @throws InvalidInputException if input validation is enabled and a
* value is invalid, a value has the wrong
Expand All @@ -607,6 +612,9 @@ public function withBilling(
?string $lastName = null,
?string $phoneCountryCode = null,
?string $phoneNumber = null,
?string $phoneVerificationMethod = null,
?string $phoneVerificationTime = null,
?bool $phoneWasVerificationSuccessful = null,
?string $postal = null,
?string $region = null,
): self {
Expand All @@ -626,6 +634,13 @@ public function withBilling(
$lastName = $this->remove($values, 'last_name');
$phoneCountryCode = $this->remove($values, 'phone_country_code');
$phoneNumber = $this->remove($values, 'phone_number');
$phoneVerificationMethod = $this->remove($values, 'phone_verification_method');
$phoneVerificationTime = $this->remove($values, 'phone_verification_time');
$phoneWasVerificationSuccessful = $this->remove(
$values,
'phone_was_verification_successful',
['boolean'],
);
$postal = $this->remove($values, 'postal');
$region = $this->remove($values, 'region');

Expand Down Expand Up @@ -674,6 +689,26 @@ public function withBilling(
$values['phone_number'] = $phoneNumber;
}

if ($phoneVerificationMethod !== null) {
if (!\in_array($phoneVerificationMethod, ['delivered_code', 'network', 'other'], true)) {
$this->maybeThrowInvalidInputException(
"$phoneVerificationMethod is not a valid phone verification method",
);
}
$values['phone_verification_method'] = $phoneVerificationMethod;
}

if ($phoneVerificationTime !== null) {
if ($this->validateInput) {
$this->verifyRfc3339DateTime($phoneVerificationTime);
}
$values['phone_verification_time'] = $phoneVerificationTime;
}

if ($phoneWasVerificationSuccessful !== null) {
$values['phone_was_verification_successful'] = $phoneWasVerificationSuccessful;
}

if ($postal !== null) {
$values['postal'] = $postal;
}
Expand Down Expand Up @@ -1587,6 +1622,19 @@ private function post(string $class, string $path)
);
}

/**
* @throws InvalidInputException if the value is invalid and input
* validation is enabled
*/
private function verifyRfc3339DateTime(string $dateTime): void
{
if (\DateTime::createFromFormat(\DateTime::RFC3339, $dateTime) === false
&& \DateTime::createFromFormat(\DateTime::RFC3339_EXTENDED, $dateTime) === false
) {
$this->maybeThrowInvalidInputException("$dateTime is not a valid RFC 3339 formatted datetime string");
}
}

/**
* @throws InvalidInputException if the value is invalid and input
* validation is enabled
Expand Down
97 changes: 96 additions & 1 deletion tests/MaxMind/Test/MinFraudTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ public static function unvalidatedAddressFields(): array
['withBilling', 'billing', 'country'],
['withBilling', 'billing', 'region'],
['withBilling', 'billing', 'phone_country_code'],
['withBilling', 'billing', 'phone_verification_time'],
['withShipping', 'shipping', 'country'],
['withShipping', 'shipping', 'region'],
['withShipping', 'shipping', 'phone_country_code'],
Expand Down Expand Up @@ -196,7 +197,10 @@ public function testFullInsightsRequestUsingNamedArgs(string $class, string $ser
country: 'US',
postal: '06510',
phoneNumber: '123-456-7890',
phoneCountryCode: '1'
phoneCountryCode: '1',
phoneVerificationMethod: 'delivered_code',
phoneWasVerificationSuccessful: true,
phoneVerificationTime: '2026-10-01T14:30:00Z'
)
->withCreditCard(
country: 'US',
Expand Down Expand Up @@ -763,6 +767,97 @@ public function testBadDeliverySpeed(): void
)->withShipping(['delivery_speed' => 'slow']);
}

/**
* @dataProvider goodBillingPhoneVerificationMethods
*/
public function testGoodBillingPhoneVerificationMethod(string $good): void
{
$result = $this->createMinFraudRequestWithFullResponse(
'insights',
0
)->withBilling(['phone_verification_method' => $good]);

$this->assertSame(
$good,
$result->jsonSerialize()['content']['billing']['phone_verification_method'],
);
}

/**
* @return array<list<string>>
*/
public static function goodBillingPhoneVerificationMethods(): array
{
return [
['delivered_code'],
['network'],
['other'],
];
}

public function testBadBillingPhoneVerificationMethod(): void
{
$this->expectException(InvalidInputException::class);
$this->expectExceptionMessage('valid phone verification method');

$this->createMinFraudRequestWithFullResponse(
'insights',
0
)->withBilling(['phone_verification_method' => 'sms']);
}

/**
* @dataProvider goodTimes
*/
public function testGoodBillingPhoneVerificationTimes(string $time): void
{
$result = $this->createMinFraudRequestWithFullResponse(
'insights',
0
)->withBilling(['phone_verification_time' => $time]);

$this->assertSame(
$time,
$result->jsonSerialize()['content']['billing']['phone_verification_time'],
);
}

public function testBadBillingPhoneVerificationTime(): void
{
$this->expectException(InvalidInputException::class);
$this->expectExceptionMessage('valid RFC 3339');

$this->createMinFraudRequestWithFullResponse(
'insights',
0
)->withBilling(['phone_verification_time' => '2026/10/01 14:30']);
}

public function testBillingPhoneWasVerificationSuccessfulFalse(): void
{
$result = $this->createMinFraudRequestWithFullResponse(
'insights',
0
)->withBilling(phoneWasVerificationSuccessful: false);

$this->assertFalse(
$result->jsonSerialize()['content']['billing']['phone_was_verification_successful'],
);
}

public function testBadBillingPhoneWasVerificationSuccessful(): void
{
$this->expectException(InvalidInputException::class);
$this->expectExceptionMessage(
'Expected phone_was_verification_successful to be in [boolean] but was string',
);

$this->createMinFraudRequestWithFullResponse(
'insights',
0
)->withBilling(['phone_was_verification_successful' => 'true']);
}

/**
* @dataProvider badIins
*/
Expand Down
5 changes: 4 additions & 1 deletion tests/data/minfraud/full-request.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,10 @@
"country": "US",
"postal": "06510",
"phone_number": "123-456-7890",
"phone_country_code": "1"
"phone_country_code": "1",
"phone_verification_method": "delivered_code",
"phone_was_verification_successful": true,
"phone_verification_time": "2026-10-01T14:30:00Z"
},
"shipping": {
"first_name": "ShipFirst",
Expand Down
Loading