Skip to content

pydantic_extra_types.payment

Represents and validates payment cards (such as a debit or credit card), including validation for payment card number and issuer.

PaymentCardBrand

Bases: str, Enum

Enumeration of payment card brands.

PaymentCardBrand can be one of the following based on the BIN:

  • PaymentCardBrand.amex
  • PaymentCardBrand.mastercard
  • PaymentCardBrand.visa
  • PaymentCardBrand.other

PaymentCardNumber

PaymentCardNumber(card_number)

Bases: str

A payment card number.

Attributes:

Name Type Description
strip_whitespace bool

Whether to strip whitespace from the input value.

min_length int

The minimum length of the card number.

max_length int

The maximum length of the card number.

bin str

The first 6 digits of the card number.

last4 str

The last 4 digits of the card number.

brand PaymentCardBrand

The brand of the card.

Source code in pydantic_extra_types/payment.py
55
56
57
58
59
60
61
62
def __init__(self, card_number: str):
    self.validate_digits(card_number)

    card_number = self.validate_luhn_check_digit(card_number)

    self.bin = card_number[:6]
    self.last4 = card_number[-4:]
    self.brand = self.validate_brand(card_number)

masked property

masked: str

The masked card number.

validate classmethod

validate(__input_value, _)

Validate the PaymentCardNumber instance.

Parameters:

Name Type Description Default
__input_value str

The input value to validate.

required
_ core_schema.ValidationInfo

The validation info.

required

Returns:

Type Description
PaymentCardNumber

The validated PaymentCardNumber instance.

Source code in pydantic_extra_types/payment.py
73
74
75
76
77
78
79
80
81
82
83
84
@classmethod
def validate(cls, __input_value: str, _: core_schema.ValidationInfo) -> PaymentCardNumber:
    """Validate the `PaymentCardNumber` instance.

    Args:
        __input_value: The input value to validate.
        _: The validation info.

    Returns:
        The validated `PaymentCardNumber` instance.
    """
    return cls(__input_value)

validate_digits classmethod

validate_digits(card_number)

Validate that the card number is all digits.

Parameters:

Name Type Description Default
card_number str

The card number to validate.

required

Raises:

Type Description
PydanticCustomError

If the card number is not all digits.

Source code in pydantic_extra_types/payment.py
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
@classmethod
def validate_digits(cls, card_number: str) -> None:
    """Validate that the card number is all digits.

    Args:
        card_number: The card number to validate.

    Raises:
        PydanticCustomError: If the card number is not all digits.
    """
    if not card_number.isdigit():
        raise PydanticCustomError('payment_card_number_digits', 'Card number is not all digits')

validate_luhn_check_digit classmethod

validate_luhn_check_digit(card_number)

Validate the payment card number. Based on the Luhn algorithm.

Parameters:

Name Type Description Default
card_number str

The card number to validate.

required

Returns:

Type Description
str

The validated card number.

Raises:

Type Description
PydanticCustomError

If the card number is not valid.

Source code in pydantic_extra_types/payment.py
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
@classmethod
def validate_luhn_check_digit(cls, card_number: str) -> str:
    """Validate the payment card number.
    Based on the [Luhn algorithm](https://en.wikipedia.org/wiki/Luhn_algorithm).

    Args:
        card_number: The card number to validate.

    Returns:
        The validated card number.

    Raises:
        PydanticCustomError: If the card number is not valid.
    """
    sum_ = int(card_number[-1])
    length = len(card_number)
    parity = length % 2
    for i in range(length - 1):
        digit = int(card_number[i])
        if i % 2 == parity:
            digit *= 2
        if digit > 9:
            digit -= 9
        sum_ += digit
    valid = sum_ % 10 == 0
    if not valid:
        raise PydanticCustomError('payment_card_number_luhn', 'Card number is not luhn valid')
    return card_number

validate_brand staticmethod

validate_brand(card_number)

Validate length based on BIN for major brands.

Parameters:

Name Type Description Default
card_number str

The card number to validate.

required

Returns:

Type Description
PaymentCardBrand

The validated card brand.

Raises:

Type Description
PydanticCustomError

If the card number is not valid.

Source code in pydantic_extra_types/payment.py
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
@staticmethod
def validate_brand(card_number: str) -> PaymentCardBrand:
    """Validate length based on
    [BIN](https://en.wikipedia.org/wiki/Payment_card_number#Issuer_identification_number_(IIN))
    for major brands.

    Args:
        card_number: The card number to validate.

    Returns:
        The validated card brand.

    Raises:
        PydanticCustomError: If the card number is not valid.
    """
    if card_number[0] == '4':
        brand = PaymentCardBrand.visa
    elif 51 <= int(card_number[:2]) <= 55:
        brand = PaymentCardBrand.mastercard
    elif card_number[:2] in {'34', '37'}:
        brand = PaymentCardBrand.amex
    elif 2200 <= int(card_number[:4]) <= 2204:
        brand = PaymentCardBrand.mir
    else:
        brand = PaymentCardBrand.other

    required_length: None | int | str = None
    if brand in PaymentCardBrand.mastercard:
        required_length = 16
        valid = len(card_number) == required_length
    elif brand == PaymentCardBrand.visa:
        required_length = '13, 16 or 19'
        valid = len(card_number) in {13, 16, 19}
    elif brand == PaymentCardBrand.amex:
        required_length = 15
        valid = len(card_number) == required_length
    elif brand == PaymentCardBrand.mir:
        required_length = 'in range from 16 to 19'
        valid = len(card_number) in range(16, 20)
    else:
        valid = True

    if not valid:
        raise PydanticCustomError(
            'payment_card_number_brand',
            'Length for a {brand} card must be {required_length}',
            {'brand': brand, 'required_length': required_length},
        )
    return brand