diff --git a/HISTORY.rst b/HISTORY.rst index 4a6bc22..f707936 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -3,6 +3,14 @@ History ------- +2.11.0 ++++++++++++++++++++ + +* Updated the validation for the Report Transactions API to make the + ``ip_address`` parameter optional. Now the ``tag`` and at least one of the + following parameters must be supplied: ``ip_address``, ``maxmind_id``, + ``minfraud_id``, ``transaction_id``. + 2.10.0 (2024-04-16) +++++++++++++++++++ diff --git a/README.rst b/README.rst index e4b8bc2..144761d 100644 --- a/README.rst +++ b/README.rst @@ -98,7 +98,8 @@ The method takes a dictionary representing the report to be sent to the web service. The structure of this dictionary should be in `the format specified in the REST API documentation `__. The -``ip_address`` and ``tag`` fields are required. All other fields are optional. +required fields are ``tag`` and one or more of the following: ``ip_address``, +``maxmind_id``, ``minfraud_id``, ``transaction_id``. Request Validation (for all request methods) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/minfraud/validation.py b/minfraud/validation.py index 7116787..dd25f74 100644 --- a/minfraud/validation.py +++ b/minfraud/validation.py @@ -15,7 +15,7 @@ from typing import Optional from email_validator import validate_email # type: ignore -from voluptuous import All, Any, In, Match, Range, Required, Schema +from voluptuous import All, Any, In, Match, MultipleInvalid, Range, Required, Schema from voluptuous.error import UrlInvalid # Pylint doesn't like the private function type naming for the callable @@ -379,14 +379,48 @@ def _uuid(s: str) -> str: raise ValueError -validate_report = Schema( +NIL_UUID = str(uuid.UUID(int=0)) + + +def _non_empty_uuid(s: str) -> str: + if _uuid(s) == NIL_UUID: + raise ValueError + return s + + +def _transaction_id(s: Optional[str]) -> str: + if isinstance(s, str) and len(s) > 0: + return s + raise ValueError + + +_validate_report_schema = Schema( { "chargeback_code": str, - Required("ip_address"): _ip_address, + "ip_address": _ip_address, "maxmind_id": _maxmind_id, - "minfraud_id": _uuid, + "minfraud_id": _non_empty_uuid, "notes": str, Required("tag"): _tag, - "transaction_id": str, + "transaction_id": _transaction_id, }, ) + + +def _validate_at_least_one_identifier_field(report): + optional_fields = ["ip_address", "maxmind_id", "minfraud_id", "transaction_id"] + if not any(field in report for field in optional_fields): + # We return MultipleInvalid instead of ValueError to be consistent with what + # voluptuous returns. + raise MultipleInvalid( + "The report must contain at least one of the following fields: " + "'ip_address', 'maxmind_id', 'minfraud_id', 'transaction_id'." + ) + return True + + +def validate_report(report): + """Validate minFraud Transaction Report fields.""" + _validate_report_schema(report) + _validate_at_least_one_identifier_field(report) + return True diff --git a/tests/test_validation.py b/tests/test_validation.py index 62a8953..7c94cff 100644 --- a/tests/test_validation.py +++ b/tests/test_validation.py @@ -51,11 +51,17 @@ def setup_report(self, report): def check_invalid_report(self, report): self.setup_report(report) + self.check_invalid_report_no_setup(report) + + def check_invalid_report_no_setup(self, report): with self.assertRaises(MultipleInvalid, msg=f"{report} is invalid"): validate_report(report) def check_report(self, report): self.setup_report(report) + self.check_report_no_setup(report) + + def check_report_no_setup(self, report): try: validate_report(report) except MultipleInvalid as e: @@ -414,6 +420,7 @@ def test_minfraud_id(self): "12345678-123412341234-12345678901", "12345678-1234-1234-1234-1234567890123", "12345678-1234-1234-1234-12345678901g", + "00000000-0000-0000-0000-000000000000", "", ): self.check_invalid_report({"minfraud_id": bad}) @@ -431,3 +438,13 @@ def test_tag(self): self.check_report({"tag": good}) for bad in ("risky_business", "", None): self.check_invalid_report({"tag": bad}) + + def test_report_valid_identifier(self): + self.check_invalid_report_no_setup({"tag": "chargeback"}) + + self.check_report_no_setup({"tag": "chargeback", "ip_address": "1.1.1.1"}) + self.check_report_no_setup( + {"tag": "chargeback", "minfraud_id": "58fa38d8-4b87-458b-a22b-f00eda1aa20d"} + ) + self.check_report_no_setup({"tag": "chargeback", "maxmind_id": "12345678"}) + self.check_report_no_setup({"tag": "chargeback", "transaction_id": "abc123"})