diff --git a/CHANGELOG.md b/CHANGELOG.md index a0c7e24..6bb7511 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,15 @@ CHANGELOG ========= +3.9.0 +------------------ + +* Added the `was_verification_successful` and `verification_time` inputs to + the `/email` object. These describe the most recent verification of the + email address. Use them with `withEmail()` as array keys or as the + `wasVerificationSuccessful` and `verificationTime` named arguments. Omit + `was_verification_successful` if no verification was attempted. + 3.8.0 ------------------ diff --git a/README.md b/README.md index 444ced1..766f570 100644 --- a/README.md +++ b/README.md @@ -172,7 +172,9 @@ $request = $mf->withDevice( usernameMd5: '4f9726678c438914fa04bdb8c1a24088' )->withEmail( address: 'test@maxmind.com', - domain: 'maxmind.com' + domain: 'maxmind.com', + wasVerificationSuccessful: true, + verificationTime: '2026-10-01T14:30:00Z' )->withBilling( firstName: 'First', lastName: 'Last', diff --git a/src/MinFraud.php b/src/MinFraud.php index 2bf4287..3d7ce19 100644 --- a/src/MinFraud.php +++ b/src/MinFraud.php @@ -487,14 +487,22 @@ public function withAccount( * @link https://dev.maxmind.com/minfraud/api-documentation/requests/?lang=en#schema--request--email * minFraud email API docs * - * @param array $values An array of email 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 email address used in the transaction. - * This field must be a valid email address. - * @param string|null $domain The domain of the email address used in the - * transaction. Do not include the `@` in this - * field. + * @param array $values An array of email 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 email address used in the transaction. + * This field must be a valid email address. + * @param string|null $domain The domain of the email address used in the + * transaction. Do not include the `@` in this + * field. + * @param string|null $verificationTime The date and time of the most recent + * verification of the email address, in + * RFC 3339 date-time format. The time must + * not be before 1970-01-01T00:00:00Z or more + * than two days in the future. + * @param bool|null $wasVerificationSuccessful Whether the most recent verification + * of the email address succeeded. Omit + * this if no verification was attempted. * * @throws InvalidInputException if a value has the wrong type, or if * input validation is enabled and a value is @@ -507,6 +515,8 @@ public function withEmail( array $values = [], ?string $address = null, ?string $domain = null, + ?string $verificationTime = null, + ?bool $wasVerificationSuccessful = null, ): self { if (\count($values) !== 0) { if (\func_num_args() !== 1) { @@ -516,6 +526,12 @@ public function withEmail( } $address = $this->remove($values, 'address'); $domain = $this->remove($values, 'domain'); + $verificationTime = $this->remove($values, 'verification_time'); + $wasVerificationSuccessful = $this->remove( + $values, + 'was_verification_successful', + ['boolean'], + ); $this->verifyEmpty($values); } @@ -535,6 +551,15 @@ public function withEmail( $values['domain'] = $domain; } + if ($verificationTime !== null) { + $this->verifyRfc3339DateTime($verificationTime); + $values['verification_time'] = $verificationTime; + } + + if ($wasVerificationSuccessful !== null) { + $values['was_verification_successful'] = $wasVerificationSuccessful; + } + $new = clone $this; $new->content['email'] = $values; diff --git a/tests/MaxMind/Test/MinFraudTest.php b/tests/MaxMind/Test/MinFraudTest.php index 5a2d1ae..33591e1 100644 --- a/tests/MaxMind/Test/MinFraudTest.php +++ b/tests/MaxMind/Test/MinFraudTest.php @@ -98,6 +98,7 @@ public function testWrongTypeWithoutValidation(string $method, string $field): v 'phone_was_verification_successful', 'was_3d_secure_successful', 'was_authorized', + 'was_verification_successful', ]; if (\in_array($field, $booleans, true)) { $values[] = 'true'; @@ -141,7 +142,7 @@ public static function allFields(): array 'withDevice' => [ 'accept_language', 'ip_address', 'session_age', 'session_id', 'tracking_token', 'user_agent', ], - 'withEmail' => ['address', 'domain'], + 'withEmail' => ['address', 'domain', 'verification_time', 'was_verification_successful'], 'withEvent' => ['party', 'shop_id', 'time', 'transaction_id', 'type'], 'withOrder' => [ 'affiliate_id', 'amount', 'currency', 'discount_code', 'has_gift_message', 'is_gift', @@ -342,7 +343,11 @@ public static function invalidValues(): array 'token' => '4111111111111111', ]], 'withDevice' => ['device', ['ip_address' => 'not an IP', 'session_age' => -1.5, 'session_id' => '']], - 'withEmail' => ['email', ['address' => 'not an email', 'domain' => 'not a domain']], + 'withEmail' => ['email', [ + 'address' => 'not an email', + 'domain' => 'not a domain', + 'verification_time' => 'not a time', + ]], 'withEvent' => ['event', ['party' => 'other', 'time' => 'not a time', 'type' => 'other']], 'withOrder' => ['order', ['amount' => -1.5, 'currency' => 'usd', 'referrer_uri' => 'not a URL']], 'withPayment' => ['payment', ['method' => 'other', 'processor' => 'other']], @@ -481,7 +486,9 @@ public function testFullInsightsRequestUsingNamedArgs(string $class, string $ser ) ->withEmail( address: '977577b140bfb7c516e4746204fbdb01', - domain: 'maxmind.com' + domain: 'maxmind.com', + wasVerificationSuccessful: true, + verificationTime: '2026-10-01T14:30:00Z' ) ->withEvent( party: 'customer', @@ -1208,6 +1215,108 @@ public function testBadShippingPhoneWasVerificationSuccessful(): void )->withShipping(['phone_was_verification_successful' => 'true']); } + /** + * @dataProvider goodTimes + */ + public function testGoodEmailVerificationTimes(string $time): void + { + $result = $this->createMinFraudRequestWithFullResponse( + 'insights', + 0 + )->withEmail(['verification_time' => $time]); + + $this->assertSame( + $time, + $result->jsonSerialize()['content']['email']['verification_time'], + ); + } + + /** + * @dataProvider badTimes + */ + public function testBadEmailVerificationTime(string $time): void + { + $this->expectException(InvalidInputException::class); + $this->expectExceptionMessage('valid RFC 3339'); + + $this->createMinFraudRequestWithFullResponse( + 'insights', + 0 + )->withEmail(['verification_time' => $time]); + } + + public function testEmailWasVerificationSuccessfulFalse(): void + { + $result = $this->createMinFraudRequestWithFullResponse( + 'insights', + 0 + )->withEmail(wasVerificationSuccessful: false); + + $this->assertFalse( + $result->jsonSerialize()['content']['email']['was_verification_successful'], + ); + } + + public function testBadEmailWasVerificationSuccessful(): void + { + $this->expectException(InvalidInputException::class); + $this->expectExceptionMessage( + 'Expected was_verification_successful to be in [boolean] but was string', + ); + + $this->createMinFraudRequestWithFullResponse( + 'insights', + 0 + )->withEmail(['was_verification_successful' => 'true']); + } + + public function testEmailWasVerificationSuccessfulFalseArray(): void + { + $result = $this->createMinFraudRequestWithFullResponse( + 'insights', + 0 + )->withEmail(['was_verification_successful' => false]); + + $this->assertFalse( + $result->jsonSerialize()['content']['email']['was_verification_successful'], + ); + } + + public function testEmailWasVerificationSuccessfulFalseWith(): void + { + $result = $this->createMinFraudRequestWithFullResponse( + 'insights', + 0 + )->with(['email' => ['was_verification_successful' => false]]); + + $this->assertFalse( + $result->jsonSerialize()['content']['email']['was_verification_successful'], + ); + } + + public function testEmailVerificationWithHashing(): void + { + $result = $this->createMinFraudRequestWithFullResponse( + 'insights', + 0, + ['hashEmail' => true], + )->withEmail([ + 'address' => 'test@gmail.com', + 'verification_time' => '2026-10-01T14:30:00Z', + 'was_verification_successful' => false, + ]); + + $this->assertSame( + [ + 'address' => '1aedb8d9dc4751e229a335e371db8058', + 'verification_time' => '2026-10-01T14:30:00Z', + 'was_verification_successful' => false, + 'domain' => 'gmail.com', + ], + $result->jsonSerialize()['content']['email'], + ); + } + /** * @dataProvider badIins */ diff --git a/tests/data/minfraud/full-request.json b/tests/data/minfraud/full-request.json index 837ddad..546c88f 100644 --- a/tests/data/minfraud/full-request.json +++ b/tests/data/minfraud/full-request.json @@ -12,7 +12,9 @@ }, "email": { "address": "977577b140bfb7c516e4746204fbdb01", - "domain": "maxmind.com" + "domain": "maxmind.com", + "was_verification_successful": true, + "verification_time": "2026-10-01T14:30:00Z" }, "billing": { "first_name": "First",