Skip to content

Instantly share code, notes, and snippets.

@radzhome
Last active April 27, 2016 01:10
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save radzhome/00148a7f195d697a40ed07531809020a to your computer and use it in GitHub Desktop.
Save radzhome/00148a7f195d697a40ed07531809020a to your computer and use it in GitHub Desktop.
Credit card utilities, useful functions
"""Card utilities for use with Payment Gateway classes, transaction processing"""
from __future__ import unicode_literals
import re
import logging
import datetime
VISA_CC = 'Visa' # Visa
MASTERCARD_CC = 'MasterCard' # MasterCard
AMEX_CC = 'American Express' # American Express
ACCEPTED_CARDS = [VISA_CC, MASTERCARD_CC, AMEX_CC]
JCB_CC = 'JCB'
DISCOVER_CC = 'DISCOVER'
DINERS_CC = 'DINERS'
MAESTRO_CC = 'MAESTRO'
LASER_CC = 'LASER'
OTHER_CC = '' # UNKNOWN
VPOS_CARD_TYPE = {
'AE': "American Express",
'AP': "American Express Corporate Purchase Card",
'BC': "Bank Card",
'DC': "Diners Club",
'GC': "GAP Inc. Card",
'JC': "JCB Card",
'LY': "Loyalty Card",
'MS': "Maestro Card",
'MC': "MasterCard",
'MX': "Mondex Card",
'PL': "PLC Card",
'PUR': "Generic Corporate Purchase Card",
'SD': "SafeDebit Card",
'SO': "SOLO Card",
'ST': "STYLE Card",
'SW': "SWITCH Card",
'VD': "Visa Debit Card",
'VC': "Visa Card",
'VP': "Visa Corporate Purchase Card",
}
def is_american_express(cc_number):
"""Checks if the card is an american express. If us billing address country code, & is_amex, use vpos
https://en.wikipedia.org/wiki/Bank_card_number#cite_note-GenCardFeatures-3
:param cc_number: unicode card number
"""
return bool(re.match(r'^3[47][0-9]{13}$', cc_number))
def is_visa(cc_number):
"""Checks if the card is a visa, begins with 4 and 12 or 15 additional digits.
:param cc_number: unicode card number
"""
# Standard Visa is 13 or 16, debit can be 19
if bool(re.match(r'^4', cc_number)) and len(cc_number) in [13, 16, 19]:
return True
return False
def is_mastercard(cc_number):
"""Checks if the card is a mastercard. Begins with 51-55 or 2221-2720 and 16 in length.
:param cc_number: unicode card number
"""
if len(cc_number) == 16 and cc_number.isdigit(): # Check digit, before cast to int
return bool(re.match(r'^5[1-5]', cc_number)) or int(cc_number[:4]) in range(2221, 2721)
return False
def is_discover(cc_number):
"""Checks if the card is discover, re would be too hard to maintain. Not a supported card.
:param cc_number: unicode card number
"""
if len(cc_number) == 16:
try:
# return bool(cc_number[:4] == '6011' or cc_number[:2] == '65' or cc_number[:6] in range(622126, 622926))
return bool(cc_number[:4] == '6011' or cc_number[:2] == '65' or 622126 <= int(cc_number[:6]) <= 622925)
except ValueError:
return False
return False
def is_jcb(cc_number):
"""Checks if the card is a jcb. Not a supported card.
:param cc_number: unicode card number
"""
# return bool(re.match(r'^(?:2131|1800|35\d{3})\d{11}$', cc_number)) # wikipedia
return bool(re.match(r'^35(2[89]|[3-8][0-9])[0-9]{12}$', cc_number)) # PawelDecowski
def is_diners_club(cc_number):
"""Checks if the card is a diners club. Not a supported card.
:param cc_number: unicode card number
"""
return bool(re.match(r'^3(?:0[0-6]|[68][0-9])[0-9]{11}$', cc_number)) # 0-5 = carte blance, 6 = international
def is_laser(cc_number):
"""Checks if the card is laser. Not a supported card.
:param cc_number: unicode card number
"""
return bool(re.match(r'^(6304|670[69]|6771)', cc_number))
def is_maestro(cc_number):
"""Checks if the card is maestro. Not a supported card.
:param cc_number: unicode card number
"""
possible_lengths = [12, 13, 14, 15, 16, 17, 18, 19]
return bool(re.match(r'^(50|5[6-9]|6[0-9])', cc_number)) and len(cc_number) in possible_lengths
# Child cards
def is_visa_electron(cc_number):
"""Child of visa. Checks if the card is a visa electron. Not a supported card.
:param cc_number: unicode card number
"""
return bool(re.match(r'^(4026|417500|4508|4844|491(3|7))', cc_number)) and len(cc_number) == 16
def is_total_rewards_visa(cc_number):
"""Child of visa. Checks if the card is a Total Rewards Visa. Not a supported card.
:param cc_number: unicode card number
"""
return bool(re.match(r'^41277777[0-9]{8}$', cc_number))
def is_diners_club_carte_blanche(cc_number):
"""Child card of diners. Checks if the card is a diners club carte blance. Not a supported card.
:param cc_number: unicode card number
"""
return bool(re.match(r'^30[0-5][0-9]{11}$', cc_number)) # github PawelDecowski, jquery-creditcardvalidator
def is_diners_club_carte_international(cc_number):
"""Child card of diners. Checks if the card is a diners club international. Not a supported card.
:param cc_number: unicode card number
"""
return bool(re.match(r'^36[0-9]{12}$', cc_number)) # jquery-creditcardvalidator
def get_card_type_by_number(cc_number):
"""Return card type constant by credit card number
:param cc_number: unicode card number
"""
if is_visa(cc_number):
return VISA_CC
if is_mastercard(cc_number):
return MASTERCARD_CC
if is_american_express(cc_number):
return AMEX_CC
if is_discover(cc_number):
return DISCOVER_CC
if is_jcb(cc_number):
return JCB_CC
if is_diners_club(cc_number):
return DINERS_CC
if is_laser(cc_number): # Check before maestro, as its inner range
return LASER_CC
if is_maestro(cc_number):
return MAESTRO_CC
return OTHER_CC
def get_card_type_by_name(cc_type):
"""Return card type constant by string
:param cc_type: dirty string card type or name
"""
cc_type_str = cc_type.replace(' ', '').replace('-', '').lower()
# Visa
if 'visa' in cc_type_str:
return VISA_CC
# MasterCard
if 'mc' in cc_type_str or 'mastercard' in cc_type_str:
return MASTERCARD_CC
# American Express
if cc_type_str in ('americanexpress', 'amex'):
return AMEX_CC
# Discover
if 'discover' in cc_type_str:
return DISCOVER_CC
# JCB
if 'jcb' in cc_type_str:
return JCB_CC
# Diners
if 'diners' in cc_type_str:
return DINERS_CC
# Maestro
if 'maestro' in cc_type_str:
return MAESTRO_CC
# Laser
if 'laser' in cc_type_str:
return LASER_CC
# Other Unsupported Cards Dankort, Union, Cartebleue, Airplus etc..
return OTHER_CC
def credit_card_luhn_checksum(card_number):
"""Credit card luhn checksum
:param card_number: unicode card number
"""
def digits_of(cc):
return [int(_digit) for _digit in str(cc)]
digits = digits_of(card_number)
odd_digits = digits[-1::-2]
even_digits = digits[-2::-2]
checksum = sum(odd_digits)
for digit in even_digits:
checksum += sum(digits_of(digit * 2))
return checksum % 10
def is_valid_cvv(card_type, cvv):
"""Validates the cvv based on card type
:param cvv: card cvv security code
:param card_type: string card type
"""
if card_type == AMEX_CC:
return len(str(cvv)) == 4
else:
return len(str(cvv)) == 3
def is_cc_luhn_valid(card_number):
"""Returns true if the luhn code is 0
:param card_number: unicode string for card_number, cannot be other type.
"""
is_valid_cc = card_number.isdecimal() and credit_card_luhn_checksum(card_number) == 0
if not is_valid_cc:
logging.error("Invalid Credit Card Number, fails luhn: {}".format(card_number))
return is_valid_cc
def is_valid_cc_expiry(expiry_month, expiry_year):
"""Returns true if the card expiry is not good.
Edge case: It's end of month, the expiry is on the current month and user is in a different timezone.
:param expiry_year: unicode two digit year
:param expiry_month: unicode two digit month
"""
try:
today = datetime.date.today()
cur_month, cur_year = today.month, int(str(today.year)[2:])
expiry_month, expiry_year = int(expiry_month), int(expiry_year)
is_invalid_year = expiry_year < cur_year
is_invalid_month = False
if not is_invalid_year:
is_invalid_month = ((expiry_month < cur_month and cur_year == expiry_year) or
expiry_month not in range(1, 13))
if is_invalid_year or is_invalid_month:
logging.info("Invalid credit card expiry {}/{}.".format(expiry_month, expiry_year))
return False
except ValueError:
logging.error("Could not calculate valid expiry for month year {}/{}.".format(expiry_month, expiry_year))
return False
return True
def is_supported_credit_card(card_type):
"""Checks if card type is in accepted cards
:param card_type: string card type
"""
if card_type in ACCEPTED_CARDS:
return True
logging.error("Card type not supported, {}.".format(card_type))
return False # (OTHER_CC, DISCOVER_CC)
def cc_card_to_mask(cc_number, show_first=6, show_last=4):
"""Returns masked credit card number
:param show_last: beginning of card, chars not to mask
:param show_first: end of card, chars not to mask
:param cc_number: unicode card number
"""
cc_number = str(cc_number)
if cc_number:
return "{}{}{}".format(cc_number[:show_first],
"X" * (len(cc_number) - show_first - show_last),
cc_number[show_last * -1:])
else:
return ""
def string_to_full_mask(cc_field):
"""Returns credit card field or any string converted to a full mask.
I.e. cvv, expiry month, expiry year, password.
:param cc_field: a generic credit card field, other than cc card no
"""
try:
cc_field = cc_field.strip()
return "X" * len(cc_field)
except (TypeError, AttributeError):
return ""
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment