Newforms Validation of Credit Card Numbers

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
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
"""
    Provides functions & Fields for validating credit card numbers
    Thanks to David Shaw for the Luhn Checksum code 
    http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/172845)
"""


import re        
from django import newforms as forms
import datetime
        
def ValidateLuhnChecksum(number_as_string):
    """ checks to make sure that the card passes a luhn mod-10 checksum """

    sum = 0
    num_digits = len(number_as_string)
    oddeven = num_digits & 1

    for i in range(0, num_digits):
        digit = int(number_as_string[i])

        if not (( i & 1 ) ^ oddeven ):
            digit = digit * 2
        if digit > 9:
            digit = digit - 9

        sum = sum + digit
        
    return ( (sum % 10) == 0 )

# Regex for valid card numbers
CC_PATTERNS = {
    'mastercard':   '^5[12345]([0-9]{14})$',
    'visa':         '^4([0-9]{12,15})$',
}

def ValidateCharacters(number):
    """ Checks to make sure string only contains valid characters """
    return re.compile('^[0-9 ]*$').match(number) != None
        
def StripToNumbers(number):
    """ remove spaces from the number """
    if ValidateCharacters(number):
        result = ''
        rx = re.compile('^[0-9]$')
        for d in number:
            if rx.match(d):
                result += d
        return result
    else:
        raise Exception('Number has invalid digits')

def ValidateDigits(type, number):
    """ Checks to make sure that the Digits match the CC pattern """
    regex = CC_PATTERNS.get(type.lower(), False)
    if regex:
        return re.compile(regex).match(number) != None
    else:
        return False

def ValidateCreditCard(clean, number):
    """ Check that a credit card number matches the type and validates the Luhn Checksum """
    clean = clean.strip().lower()
    if ValidateCharacters(number):
        number = StripToNumbers(number)
        if CC_PATTERNS.has_key(clean):
            return ValidateDigits(clean, number)
            return ValidateLuhnChecksum(number)
    return False

class CreditCardNumberField(forms.CharField):
    """ A newforms field for a creditcard number """
    def clean(self, value):
        
        value = forms.CharField.clean(self, value)
        if not ValidateCharacters(value):
            raise forms.ValidationError('Can only contain numbers and spaces.')
        value = StripToNumbers(value)
        if not ValidateLuhnChecksum(value):
            raise forms.ValidationError('Not a valid credit card number.')
        
        return value


class CreditCardExpiryField(forms.CharField):
    """ A newforms field for a creditcard expiry date """
    def clean(self, value):     
        value = forms.CharField.clean(self, value.strip())
        
        # Just check MM/YY Pattern
        r = re.compile('^([0-9][0-9])/([0-9][0-9])$')
        m = r.match(value)
        if m == None:
            raise forms.ValidationError('Must be in the format MM/YY. i.e. "11/10" for Nov 2010.')
        
        # Check that the month is 1-12
        month = int(m.groups()[0])
        if month < 1 or month > 12:
            raise forms.ValidationError('Month must be in the range 1 - 12.')
        
        # Check that the year is not too far into the future
        year = int(m.groups()[1])
        curr_year = datetime.datetime.now().year % 100
        max_year = curr_year + 10
        if year > max_year or year < curr_year:
            raise forms.ValidationError('Year must be in the range %s - %s.' % (str(curr_year).zfill(2), str(max_year).zfill(2),))

        return value   

# An example Form based on ModelForm.
class PaymentForm(forms.ModelForm):    
    cc_number = creditcards.CreditCardNumberField(required=False)
    cc_expiry = creditcards.CreditCardExpiryField()
   
    class Meta:
        model = Payment 
    
    """
        This function checks that the card number matches the card type.  
        If you don't want to do this, comment out this function.
    """
    def clean(self):
        if self.cleaned_data:
            if len(self.cleaned_data.items()) == len(self.fields):      
                if self.cleaned_data['method'] == 'cc':
                    the_type = self.cleaned_data.get('cc_type', '')
                    number = self.cleaned_data.get('cc_number', '')
                    if not ValidateDigits(the_type, number):
                        raise forms.ValidationError('Card Number is not a valid ' + the_type.upper() + ' card number.')
                    if not self.instance.is_payment_valid():
                        raise forms.ValidationError('Credit card payment could not be processed.  Reason is %s.  Check that card details are correct and try again.  If you still receive this error, check with your financial institution.' % (self.instance.gateway_resptxt))
        return self.cleaned_data

More like this

  1. Credit Card With Newforms by MasonM 5 years, 8 months ago
  2. Spanish National Identification Number Field (DNI) by cues7a 2 years, 10 months ago
  3. UKPhoneNumberField GB v3 (improved) by g1smd 1 year, 7 months ago
  4. Custom DateField To Handle Credit Card Exp Date. Format: MM/YY by pjs 5 years, 9 months ago
  5. CleanCharField by DvD 6 years, 6 months ago

Comments

mk (on May 20, 2008):

Please note: If you want to use that snippet with mod_wsgi, you need to remove all print statements.

#

humphreymurray (on May 27, 2008):

Print statements removed... They were just there for when i was debugging :-)

#

donspaulding (on May 28, 2008):

You really ought to rename the type variable in your PaymentForm's clean method, as it's a reserved keyword in Python.

#

humphreymurray (on June 22, 2008):

thanx donspaulding... fixed

#

anger (on November 17, 2011):

there is an error on string 68 i think the second return must be before 'if'

#

(Forgotten your password?)