diff --git a/HISTORY.rst b/HISTORY.rst index def0727..0d9969d 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -23,6 +23,21 @@ History * ``payvision`` * ``trustly`` * ``windcave`` +* The ``/credit_card/last_4_digits`` input has been deprecated in favor of + ``/credit_card/last_digits`` and will be removed in a future release. + ``last_digits``/``last_4_digits`` also now supports two digit values in + addition to the previous four digit values. +* Eight digit ``/credit_card/issuer_id_number`` inputs are now supported in + addition to the previously accepted six digit ``issuer_id_number``. In most + cases, you should send the last four digits for ``last_digits``. If you send + an ``issuer_id_number`` that contains an eight digit IIN, and if the credit + card brand is not one of the following, you should send the last two digits + for ``last_digits``: + * ``Discover`` + * ``JCB`` + * ``Mastercard`` + * ``UnionPay`` + * ``Visa`` 2.5.0 (2021-09-20) ++++++++++++++++++ diff --git a/README.rst b/README.rst index 0edc773..0ff0ebb 100644 --- a/README.rst +++ b/README.rst @@ -191,10 +191,10 @@ Score, Insights and Factors Example >>> 'bank_phone_country_code': '1', >>> 'avs_result': 'Y', >>> 'bank_phone_number': '123-456-1234', - >>> 'last_4_digits': '7643', + >>> 'last_digits': '7643', >>> 'cvv_result': 'N', >>> 'bank_name': 'Bank of No Hope', - >>> 'issuer_id_number': '411111' + >>> 'issuer_id_number': '411111', >>> 'was_3d_secure_successful': True >>> }, >>> 'payment': { diff --git a/minfraud/request.py b/minfraud/request.py index d5fd712..f98314c 100644 --- a/minfraud/request.py +++ b/minfraud/request.py @@ -5,6 +5,7 @@ """ +import warnings import hashlib from typing import Any, Dict from voluptuous import MultipleInvalid @@ -53,6 +54,9 @@ def prepare_transaction( if hash_email: maybe_hash_email(cleaned_request) + if cleaned_request.get("credit_card", None): + clean_credit_card(cleaned_request) + return cleaned_request @@ -65,6 +69,17 @@ def _copy_and_clean(data: Any) -> Any: return data +def clean_credit_card(credit_card): + """Clean the credit_card input of a transaction request""" + last4 = credit_card.pop("last_4_digits", None) + if last4: + warnings.warn( + "last_4_digits has been deprecated in favor of last_digits", + DeprecationWarning, + ) + credit_card["last_digits"] = last4 + + def maybe_hash_email(transaction): """Hash email address in transaction, if present""" try: diff --git a/minfraud/validation.py b/minfraud/validation.py index 423122a..4e23e93 100644 --- a/minfraud/validation.py +++ b/minfraud/validation.py @@ -7,7 +7,6 @@ """ - import ipaddress import re import uuid @@ -250,9 +249,9 @@ def _hostname(hostname: str) -> str: _single_char = Match("^[A-Za-z0-9]$") -_iin = Match("^[0-9]{6}$") +_iin = Match("^(?:[0-9]{6}|[0-9]{8})$") -_credit_card_last_4 = Match("^[0-9]{4}$") +_credit_card_last_digits = Match("^(?:[0-9]{2}|[0-9]{4})$") def _credit_card_token(s: str) -> str: @@ -311,7 +310,8 @@ def _uri(s: str) -> str: "bank_phone_number": str, "cvv_result": _single_char, "issuer_id_number": _iin, - "last_4_digits": _credit_card_last_4, + "last_digits": _credit_card_last_digits, + "last_4_digits": _credit_card_last_digits, "token": _credit_card_token, "was_3d_secure_successful": bool, }, diff --git a/tests/data/full-transaction-request.json b/tests/data/full-transaction-request.json index 430554c..b6a1f90 100644 --- a/tests/data/full-transaction-request.json +++ b/tests/data/full-transaction-request.json @@ -47,7 +47,7 @@ }, "credit_card": { "issuer_id_number": "411111", - "last_4_digits": "7643", + "last_digits": "7643", "bank_name": "Bank of No Hope", "bank_phone_country_code": "1", "bank_phone_number": "123-456-1234", diff --git a/tests/test_request.py b/tests/test_request.py index 1d56a7f..b55ee69 100644 --- a/tests/test_request.py +++ b/tests/test_request.py @@ -1,6 +1,6 @@ import unittest -from minfraud.request import maybe_hash_email +from minfraud.request import maybe_hash_email, clean_credit_card class TestRequest(unittest.TestCase): @@ -146,3 +146,48 @@ def test_maybe_hash_email(self): maybe_hash_email(transaction) self.assertEqual(test["expected"], transaction) + + def test_clean_credit_card(self): + tests = [ + { + "name": "deprecated last_4_digits is cleaned to last_digits", + "input": { + "issuer_id_number": "123456", + "last_4_digits": "1234", + }, + "expected": { + "issuer_id_number": "123456", + "last_digits": "1234", + }, + }, + { + "name": "6 digit iin, 4 digit last_digits", + "input": { + "issuer_id_number": "123456", + "last_digits": "1234", + }, + "expected": { + "issuer_id_number": "123456", + "last_digits": "1234", + }, + }, + { + "name": "8 digit iin, 2 digit last_digits", + "input": { + "issuer_id_number": "12345678", + "last_digits": "34", + }, + "expected": { + "issuer_id_number": "12345678", + "last_digits": "34", + }, + }, + ] + + for test in tests: + with self.subTest(test["name"]): + transaction = test["input"] + + clean_credit_card(transaction) + + self.assertEqual(test["expected"], transaction) diff --git a/tests/test_validation.py b/tests/test_validation.py index 633fca8..a61b72d 100644 --- a/tests/test_validation.py +++ b/tests/test_validation.py @@ -143,16 +143,34 @@ def test_delivery_speed(self): class TestCreditCard(ValidationBase, unittest.TestCase): def test_issuer_id_number(self): - for iin in ("123456", "532313"): + for iin in ("123456", "532313", "88888888"): self.check_transaction({"credit_card": {"issuer_id_number": iin}}) for invalid in ("12345", "1234567", 123456, "12345a"): self.check_invalid_transaction( {"credit_card": {"issuer_id_number": invalid}} ) + def test_last_digits(self): + for last_digits in ("1234", "9323", "34"): + self.check_transaction({"credit_card": {"last_digits": last_digits}}) + for invalid in ("12345", "123", 1234, "123a"): + self.check_invalid_transaction({"credit_card": {"last_digits": invalid}}) + self.check_transaction( + {"credit_card": {"issuer_id_number": "88888888", "last_digits": "12"}} + ) + self.check_transaction( + {"credit_card": {"issuer_id_number": "88888888", "last_digits": "1234"}} + ) + self.check_transaction( + {"credit_card": {"issuer_id_number": "666666", "last_digits": "1234"}} + ) + self.check_transaction( + {"credit_card": {"issuer_id_number": "666666", "last_digits": "34"}} + ) + def test_last_4_digits(self): - for iin in ("1234", "9323"): - self.check_transaction({"credit_card": {"last_4_digits": iin}}) + for last_digits in ("1234", "9323", "34"): + self.check_transaction({"credit_card": {"last_4_digits": last_digits}}) for invalid in ("12345", "123", 1234, "123a"): self.check_invalid_transaction({"credit_card": {"last_4_digits": invalid}})