Skip to content

Commit d18df25

Browse files
authored
♻️ update credit card brands (#325)
* feat: add support for Diners Club card brand * feat: extend Mastercard brand validation to include new BIN range * style: fix code style
1 parent 7332abb commit d18df25

File tree

2 files changed

+84
-44
lines changed

2 files changed

+84
-44
lines changed

pydantic_extra_types/payment.py

Lines changed: 73 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ class PaymentCardBrand(str, Enum):
2525
troy = 'Troy'
2626
unionpay = 'UnionPay'
2727
jcb = 'JCB'
28+
diners_club = 'Diners Club'
2829
other = 'other'
2930

3031
def __str__(self) -> str:
@@ -126,6 +127,77 @@ def validate_luhn_check_digit(cls, card_number: str) -> str:
126127
raise PydanticCustomError('payment_card_number_luhn', 'Card number is not luhn valid')
127128
return card_number
128129

130+
@classmethod
131+
def _identify_brand(cls, card_number: str) -> tuple[PaymentCardBrand, list[int]]:
132+
"""Identify the brand and required length for a card number.
133+
134+
Args:
135+
card_number: The card number to identify.
136+
137+
Returns:
138+
A tuple of (brand, required_length)
139+
"""
140+
# VISA
141+
if card_number[0] == '4':
142+
return PaymentCardBrand.visa, [13, 16, 19]
143+
144+
# Mastercard
145+
if (51 <= int(card_number[:2]) <= 55) or (2221 <= int(card_number[:4]) <= 2720):
146+
return PaymentCardBrand.mastercard, [16]
147+
148+
# American Express
149+
if card_number[:2] in {'34', '37'}:
150+
return PaymentCardBrand.amex, [15]
151+
152+
# MIR
153+
if 2200 <= int(card_number[:4]) <= 2204:
154+
return PaymentCardBrand.mir, list(range(16, 20))
155+
156+
# Maestro
157+
if card_number[:4] in {'5018', '5020', '5038', '5893', '6304', '6759', '6761', '6762', '6763'} or card_number[
158+
:6
159+
] in ('676770', '676774'):
160+
return PaymentCardBrand.maestro, list(range(12, 20))
161+
162+
# Discover
163+
if card_number.startswith('65') or 644 <= int(card_number[:3]) <= 649 or card_number.startswith('6011'):
164+
return PaymentCardBrand.discover, list(range(16, 20))
165+
166+
# Verve
167+
if (
168+
506099 <= int(card_number[:6]) <= 506198
169+
or 650002 <= int(card_number[:6]) <= 650027
170+
or 507865 <= int(card_number[:6]) <= 507964
171+
):
172+
return PaymentCardBrand.verve, [16, 18, 19]
173+
174+
# Dankort
175+
if card_number[:4] in {'5019', '4571'}:
176+
return PaymentCardBrand.dankort, [16]
177+
178+
# Troy
179+
if card_number.startswith('9792'):
180+
return PaymentCardBrand.troy, [16]
181+
182+
# UnionPay
183+
if card_number[:2] in {'62', '81'}:
184+
return PaymentCardBrand.unionpay, [16, 19]
185+
186+
# JCB
187+
if 3528 <= int(card_number[:4]) <= 3589:
188+
return PaymentCardBrand.jcb, [16, 19]
189+
190+
# Diners Club
191+
if card_number[:2] in {'30', '36', '38', '39'}:
192+
return PaymentCardBrand.diners_club, list(range(14, 20))
193+
194+
# More Diners Club
195+
if card_number.startswith('55'):
196+
return PaymentCardBrand.diners_club, [16]
197+
198+
# Other / Unknown
199+
return PaymentCardBrand.other, []
200+
129201
@staticmethod
130202
def validate_brand(card_number: str) -> PaymentCardBrand:
131203
"""Validate length based on
@@ -141,50 +213,7 @@ def validate_brand(card_number: str) -> PaymentCardBrand:
141213
Raises:
142214
PydanticCustomError: If the card number is not valid.
143215
"""
144-
brand = PaymentCardBrand.other
145-
146-
if card_number[0] == '4':
147-
brand = PaymentCardBrand.visa
148-
required_length = [13, 16, 19]
149-
elif 51 <= int(card_number[:2]) <= 55:
150-
brand = PaymentCardBrand.mastercard
151-
required_length = [16]
152-
elif card_number[:2] in {'34', '37'}:
153-
brand = PaymentCardBrand.amex
154-
required_length = [15]
155-
elif 2200 <= int(card_number[:4]) <= 2204:
156-
brand = PaymentCardBrand.mir
157-
required_length = list(range(16, 20))
158-
elif card_number[:4] in {'5018', '5020', '5038', '5893', '6304', '6759', '6761', '6762', '6763'} or card_number[
159-
:6
160-
] in (
161-
'676770',
162-
'676774',
163-
):
164-
brand = PaymentCardBrand.maestro
165-
required_length = list(range(12, 20))
166-
elif card_number.startswith('65') or 644 <= int(card_number[:3]) <= 649 or card_number.startswith('6011'):
167-
brand = PaymentCardBrand.discover
168-
required_length = list(range(16, 20))
169-
elif (
170-
506099 <= int(card_number[:6]) <= 506198
171-
or 650002 <= int(card_number[:6]) <= 650027
172-
or 507865 <= int(card_number[:6]) <= 507964
173-
):
174-
brand = PaymentCardBrand.verve
175-
required_length = [16, 18, 19]
176-
elif card_number[:4] in {'5019', '4571'}:
177-
brand = PaymentCardBrand.dankort
178-
required_length = [16]
179-
elif card_number.startswith('9792'):
180-
brand = PaymentCardBrand.troy
181-
required_length = [16]
182-
elif card_number[:2] in {'62', '81'}:
183-
brand = PaymentCardBrand.unionpay
184-
required_length = [16, 19]
185-
elif 3528 <= int(card_number[:4]) <= 3589:
186-
brand = PaymentCardBrand.jcb
187-
required_length = [16, 19]
216+
brand, required_length = PaymentCardNumber._identify_brand(card_number)
188217

189218
valid = len(card_number) in required_length if brand != PaymentCardBrand.other else True
190219

tests/test_types_payment.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,10 @@
2828
VALID_MAESTRO = '6759649826438453'
2929
VALID_TROY = '9792000000000001'
3030
VALID_OTHER = '2000000000000000008'
31+
VALID_DINERS_CLUB_14 = '30500000000000'
32+
VALID_DINERS_CLUB_16 = '3050000000000009'
33+
VALID_DINERS_CLUB_17 = '30500000000000009'
34+
VALID_DINERS_CLUB_19 = '3050000000000000009'
3135
LUHN_INVALID = '4000000000000000'
3236
LEN_INVALID = '40000000000000006'
3337

@@ -117,6 +121,9 @@ def test_validate_luhn_check_digit(card_number: str, valid: bool):
117121
(LEN_INVALID, PaymentCardBrand.visa, False),
118122
(VALID_MAESTRO, PaymentCardBrand.maestro, True),
119123
(VALID_TROY, PaymentCardBrand.troy, True),
124+
(VALID_DINERS_CLUB_14, PaymentCardBrand.diners_club, True),
125+
(VALID_DINERS_CLUB_16, PaymentCardBrand.diners_club, True),
126+
(VALID_DINERS_CLUB_19, PaymentCardBrand.diners_club, True),
120127
(VALID_OTHER, PaymentCardBrand.other, True),
121128
],
122129
)
@@ -144,6 +151,10 @@ def test_length_for_brand(card_number: str, brand: PaymentCardBrand, valid: bool
144151
(VALID_JCB_16, PaymentCardBrand.jcb),
145152
(VALID_OTHER, PaymentCardBrand.other),
146153
(VALID_MAESTRO, PaymentCardBrand.maestro),
154+
(VALID_DINERS_CLUB_14, PaymentCardBrand.diners_club),
155+
(VALID_DINERS_CLUB_16, PaymentCardBrand.diners_club),
156+
(VALID_DINERS_CLUB_17, PaymentCardBrand.diners_club),
157+
(VALID_DINERS_CLUB_19, PaymentCardBrand.diners_club),
147158
(VALID_TROY, PaymentCardBrand.troy),
148159
],
149160
)

0 commit comments

Comments
 (0)