diff --git a/HISTORY.rst b/HISTORY.rst index 139f21b..315c242 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -10,6 +10,9 @@ History ``phone_was_verification_successful``, and ``phone_verification_time`` inputs to the ``/billing`` and ``/shipping`` request objects. They describe the most recent verification of the phone number. +* Added the ``verification_time`` and ``was_verification_successful`` inputs + to the ``/email`` request object. They describe the most recent + verification of the email address. * The RFC 3339 check on ``/event/time`` and ``phone_verification_time`` now rejects more values that the web service rejects, such as February 30, hour 24, an offset of 24 hours or more, and a lowercase ``t`` or ``z``. diff --git a/README.rst b/README.rst index 961ec22..e5c675e 100644 --- a/README.rst +++ b/README.rst @@ -161,7 +161,7 @@ Score, Insights and Factors Example >>> 'shop_id': 's2123', >>> 'type': 'purchase', >>> 'transaction_id': 'txn3134133', - >>> 'time': '2014-04-12T23:20:50.052+00:00' + >>> 'time': '2026-10-05T23:20:50.052+00:00' >>> }, >>> 'account': { >>> 'user_id': '3132', @@ -169,7 +169,9 @@ Score, Insights and Factors Example >>> }, >>> 'email': { >>> 'address': '977577b140bfb7c516e4746204fbdb01', - >>> 'domain': 'maxmind.com' + >>> 'domain': 'maxmind.com', + >>> 'verification_time': '2026-10-01T14:25:00Z', + >>> 'was_verification_successful': True, >>> }, >>> 'billing': { >>> 'first_name': 'Jane', diff --git a/src/minfraud/validation.py b/src/minfraud/validation.py index 59b5c39..821915d 100644 --- a/src/minfraud/validation.py +++ b/src/minfraud/validation.py @@ -393,6 +393,8 @@ def _uri(s: str) -> str: "email": { "address": _email_or_md5, "domain": _hostname, + "verification_time": _rfc3339_datetime, + "was_verification_successful": bool, }, "event": { "party": _event_party, diff --git a/tests/data/full-transaction-request.json b/tests/data/full-transaction-request.json index 985d333..b5073f8 100644 --- a/tests/data/full-transaction-request.json +++ b/tests/data/full-transaction-request.json @@ -3,7 +3,7 @@ "party": "customer", "transaction_id": "txn3134133", "shop_id": "s2123", - "time": "2014-04-12T23:20:50.052+00:00", + "time": "2026-10-05T23:20:50.052+00:00", "type": "purchase" }, "account": { @@ -12,7 +12,9 @@ }, "email": { "address": "977577b140bfb7c516e4746204fbdb01", - "domain": "maxmind.com" + "domain": "maxmind.com", + "verification_time": "2026-10-01T14:25:00Z", + "was_verification_successful": true }, "billing": { "first_name": "First", diff --git a/tests/test_request.py b/tests/test_request.py index a2d0e28..61ae8ab 100644 --- a/tests/test_request.py +++ b/tests/test_request.py @@ -63,6 +63,24 @@ def test_maybe_hash_email(self) -> None: }, }, }, + { + "name": "verification inputs kept", + "input": { + "email": { + "address": "test@maxmind.com", + "verification_time": "2026-10-01T14:25:00Z", + "was_verification_successful": False, + }, + }, + "expected": { + "email": { + "address": "977577b140bfb7c516e4746204fbdb01", + "domain": "maxmind.com", + "verification_time": "2026-10-01T14:25:00Z", + "was_verification_successful": False, + }, + }, + }, { "name": "uppercase and alias", "input": {"email": {"address": "Test+ignored@MaxMind.com"}}, diff --git a/tests/test_validation.py b/tests/test_validation.py index 3207085..6b1c1e6 100644 --- a/tests/test_validation.py +++ b/tests/test_validation.py @@ -82,6 +82,12 @@ def check_bool(self, obj: str, key: str) -> None: for bad in ("", 0, "True"): self.check_invalid_transaction({obj: {key: bad}}) + def check_rfc3339_time(self, obj: str, key: str) -> None: + for good in GOOD_TIMES: + self.check_transaction({obj: {key: good}}) + for bad in BAD_TIMES + NON_STRING_TIMES: + self.check_invalid_transaction({obj: {key: bad}}) + def setup_report(self, report: dict[str, Any]) -> None: if "ip_address" not in report: report["ip_address"] = "1.2.3.4" @@ -186,18 +192,7 @@ def test_phone_was_verification_successful(self) -> None: self.check_bool(self.type, "phone_was_verification_successful") def test_phone_verification_time(self) -> None: - for good in GOOD_TIMES: - self.check_transaction({self.type: {"phone_verification_time": good}}) - for bad in BAD_TIMES: - self.check_invalid_transaction( - {self.type: {"phone_verification_time": bad}}, - ) - - def test_phone_verification_time_not_string(self) -> None: - for bad in NON_STRING_TIMES: - self.check_invalid_transaction( - {self.type: {"phone_verification_time": bad}}, - ) + self.check_rfc3339_time(self.type, "phone_verification_time") class TestBillingAddress(AddressBase): @@ -371,6 +366,12 @@ def test_domain(self) -> None: for bad in ("bad ", " bad.com"): self.check_invalid_transaction({"email": {"domain": bad}}) + def test_verification_time(self) -> None: + self.check_rfc3339_time("email", "verification_time") + + def test_was_verification_successful(self) -> None: + self.check_bool("email", "was_verification_successful") + class TestEvent(ValidationBase, unittest.TestCase): def test_party(self) -> None: @@ -386,14 +387,7 @@ def test_shop_id(self) -> None: self.check_transaction_str_type("event", "shop_id") def test_time(self) -> None: - for good in GOOD_TIMES: - self.check_transaction({"event": {"time": good}}) - for bad in BAD_TIMES: - self.check_invalid_transaction({"event": {"time": bad}}) - - def test_time_not_string(self) -> None: - for bad in NON_STRING_TIMES: - self.check_invalid_transaction({"event": {"time": bad}}) + self.check_rfc3339_time("event", "time") def test_time_impossible_date_message(self) -> None: transaction: dict[str, Any] = {"event": {"time": "2026-02-30T10:00:00Z"}} diff --git a/tests/test_webservice.py b/tests/test_webservice.py index d949642..f55f7b5 100644 --- a/tests/test_webservice.py +++ b/tests/test_webservice.py @@ -281,6 +281,8 @@ def test_200_with_email_hashing(self) -> None: "email": { "address": "977577b140bfb7c516e4746204fbdb01", "domain": "maxmind.com", + "verification_time": "2026-10-01T14:25:00Z", + "was_verification_successful": False, }, }, ).respond_with_data( @@ -292,7 +294,13 @@ def test_200_with_email_hashing(self) -> None: status=200, ) - request = {"email": {"address": "Test+ignore@maxmind.com"}} + request = { + "email": { + "address": "Test+ignore@maxmind.com", + "verification_time": "2026-10-01T14:25:00Z", + "was_verification_successful": False, + }, + } self.run_client(getattr(self.client, self.type)(request, hash_email=True)) # This was fixed in https://github.com/maxmind/minfraud-api-python/pull/78