@@ -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
0 commit comments