From 2792b9a043071353fee42c6760c0da0b27070909 Mon Sep 17 00:00:00 2001 From: Gregory Oschwald Date: Fri, 9 Oct 2026 20:42:50 +0000 Subject: [PATCH] Add billing phone verification inputs Add the phone_verification_method, phone_was_verification_successful, and phone_verification_time inputs to the billing object. They describe the most recent verification of the billing phone number. Validation accepts delivered_code, network, and other for the method, a boolean for the result, and an RFC 3339 date-time for the time. The shipping object does not accept these inputs. Co-Authored-By: Claude Opus 5.5 --- HISTORY.rst | 8 +++++++ README.rst | 3 +++ src/minfraud/validation.py | 20 ++++++++++++------ tests/data/full-transaction-request.json | 5 ++++- tests/test_validation.py | 27 ++++++++++++++++++++++++ 5 files changed, 56 insertions(+), 7 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index ee3bd70..35667f8 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -3,6 +3,14 @@ History ------- +3.4.0 +++++++++++++++++++ + +* Added the ``phone_verification_method``, + ``phone_was_verification_successful``, and ``phone_verification_time`` + inputs to the ``/billing`` request object. They describe the most recent + verification of the billing phone number. + 3.3.0 (2026-07-21) ++++++++++++++++++ diff --git a/README.rst b/README.rst index 56dece5..bdecb28 100644 --- a/README.rst +++ b/README.rst @@ -183,6 +183,9 @@ Score, Insights and Factors Example >>> 'postal': '06510', >>> 'phone_country_code': '1', >>> 'phone_number': '123-456-7890', + >>> 'phone_verification_method': 'delivered_code', + >>> 'phone_verification_time': '2026-10-01T14:30:00Z', + >>> 'phone_was_verification_successful': True, >>> }, >>> 'shipping': { >>> 'first_name': 'John', diff --git a/src/minfraud/validation.py b/src/minfraud/validation.py index 18d7a1b..14d6006 100644 --- a/src/minfraud/validation.py +++ b/src/minfraud/validation.py @@ -79,6 +79,11 @@ def _hostname(hostname: str) -> str: raise ValueError +_rfc3339_datetime = Match( + r"(?a)\A\d{4}-\d{2}-\d{2}[Tt]\d{2}:\d{2}:\d{2}(\.\d+)?(?:[Zz]|[+-]\d{2}:\d{2})\Z", +) + + _delivery_speed = In(["same_day", "overnight", "expedited", "standard"]) _address = { @@ -99,6 +104,14 @@ def _hostname(hostname: str) -> str: _shipping_address["delivery_speed"] = _delivery_speed +_phone_verification_method = In(["delivered_code", "network", "other"]) + +_billing_address = _address.copy() + +_billing_address["phone_verification_method"] = _phone_verification_method +_billing_address["phone_verification_time"] = _rfc3339_datetime +_billing_address["phone_was_verification_successful"] = bool + _payment_method = In( [ "bank_debit", @@ -296,11 +309,6 @@ def _credit_card_token(s: str) -> str: raise ValueError -_rfc3339_datetime = Match( - r"(?a)\A\d{4}-\d{2}-\d{2}[Tt]\d{2}:\d{2}:\d{2}(\.\d+)?(?:[Zz]|[+-]\d{2}:\d{2})\Z", -) - - _event_party = In(["agent", "customer"]) _event_type = In( @@ -339,7 +347,7 @@ def _uri(s: str) -> str: "user_id": str, "username_md5": _md5, }, - "billing": _address, + "billing": _billing_address, "payment": { "method": _payment_method, "processor": _payment_processor, diff --git a/tests/data/full-transaction-request.json b/tests/data/full-transaction-request.json index 40fd47e..625713d 100644 --- a/tests/data/full-transaction-request.json +++ b/tests/data/full-transaction-request.json @@ -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_verification_time": "2026-10-01T14:30:00Z", + "phone_was_verification_successful": true }, "shipping": { "first_name": "ShipFirst", diff --git a/tests/test_validation.py b/tests/test_validation.py index 30e77ad..22a9984 100644 --- a/tests/test_validation.py +++ b/tests/test_validation.py @@ -144,10 +144,37 @@ def test_phone_country_code(self) -> None: class TestBillingAddress(AddressBase): type = "billing" + def test_phone_verification_method(self) -> None: + for good in ("delivered_code", "network", "other"): + self.check_transaction({"billing": {"phone_verification_method": good}}) + for bad in ("sms", 1, ""): + self.check_invalid_transaction( + {"billing": {"phone_verification_method": bad}}, + ) + + def test_phone_was_verification_successful(self) -> None: + self.check_bool("billing", "phone_was_verification_successful") + + def test_phone_verification_time(self) -> None: + for good in ("2015-05-08T16:07:56+00:00", "2015-05-08T16:07:56Z"): + self.check_transaction({"billing": {"phone_verification_time": good}}) + for bad in ("2015-05-08T16:07:56", "2015-05-08 16:07:56Z"): + self.check_invalid_transaction( + {"billing": {"phone_verification_time": bad}}, + ) + class TestShippingAddress(AddressBase): type = "shipping" + def test_billing_only_phone_verification_inputs(self) -> None: + for key, value in ( + ("phone_verification_method", "network"), + ("phone_verification_time", "2015-05-08T16:07:56Z"), + ("phone_was_verification_successful", True), + ): + self.check_invalid_transaction({"shipping": {key: value}}) + def test_delivery_speed(self) -> None: for speed in ("same_day", "overnight", "expedited", "standard"): self.check_transaction({self.type: {"delivery_speed": speed}})