Last active
April 27, 2016 01:10
-
-
Save radzhome/00148a7f195d697a40ed07531809020a to your computer and use it in GitHub Desktop.
Credit card utilities, useful functions
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
"""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