summaryrefslogtreecommitdiff
path: root/payments/fields.py
blob: 1c1290a4375306c93a45aef6f69cd1c428d1b191 (plain)
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
133
134
135
136
137
138
139
from __future__ import unicode_literals
from calendar import monthrange
from datetime import date
import re

from django import forms
from django.core import validators
from django.utils.translation import ugettext_lazy as _

from .core import get_credit_card_issuer
from .utils import get_month_choices, get_year_choices
from .widgets import CreditCardExpiryWidget, CreditCardNumberWidget


class CreditCardNumberField(forms.CharField):

    widget = CreditCardNumberWidget(
        attrs={'autocomplete': 'cc-number', 'required': 'required'})
    default_error_messages = {
        'invalid': _('Please enter a valid card number'),
        'invalid_type': _('We accept only %(valid_types)s')}

    def __init__(self, valid_types=None, *args, **kwargs):
        self.valid_types = valid_types
        kwargs['max_length'] = kwargs.pop('max_length', 32)
        super(CreditCardNumberField, self).__init__(*args, **kwargs)

    def to_python(self, value):
        if value is not None:
            value = re.sub('[\s-]+', '', value)
        return super(CreditCardNumberField, self).to_python(value)

    def validate(self, value):
        card_type, issuer_name = get_credit_card_issuer(value)
        if value in validators.EMPTY_VALUES and self.required:
            raise forms.ValidationError(self.error_messages['required'])
        if value and not self.cart_number_checksum_validation(self, value):
            raise forms.ValidationError(self.error_messages['invalid'])
        if (value and not self.valid_types is None
                and not card_type in self.valid_types):
            valid_types = map(issuer_name, self.valid_types)
            error_message = self.error_messages['invalid_type'] % {
                'valid_types': ', '.join(valid_types)
            }
            raise forms.ValidationError(error_message)

    @staticmethod
    def cart_number_checksum_validation(cls, number):
        digits = []
        even = False
        if not number.isdigit():
            return False
        for digit in reversed(number):
            digit = ord(digit) - ord('0')
            if even:
                digit *= 2
                if digit >= 10:
                    digit = digit % 10 + digit // 10
            digits.append(digit)
            even = not even
        return sum(digits) % 10 == 0 if digits else False



class CreditCardExpiryField(forms.MultiValueField):

    default_error_messages = {
        'invalid_month': 'Enter a valid month.',
        'invalid_year': 'Enter a valid year.'}

    def __init__(self, *args, **kwargs):
        errors = self.default_error_messages.copy()
        if 'error_messages' in kwargs:
            errors.update(kwargs['error_messages'])

        fields = (
            forms.ChoiceField(
                choices=get_month_choices(),
                error_messages={'invalid': errors['invalid_month']},
                widget=forms.Select(
                    attrs={'autocomplete': 'cc-exp-month',
                           'required': 'required'})),
            forms.ChoiceField(
                choices=get_year_choices(),
                error_messages={'invalid': errors['invalid_year']},
                widget=forms.Select(
                    attrs={'autocomplete': 'cc-exp-year',
                           'required': 'required'})),
        )

        super(CreditCardExpiryField, self).__init__(fields, *args, **kwargs)
        self.widget = CreditCardExpiryWidget(widgets=[fields[0].widget,
                                                      fields[1].widget])

    def clean(self, value):
        exp = super(CreditCardExpiryField, self).clean(value)
        if exp and date.today() > exp:
            raise forms.ValidationError(
                "The expiration date you entered is in the past.")
        return exp

    def compress(self, data_list):
        if data_list:
            if data_list[1] in forms.fields.EMPTY_VALUES:
                error = self.error_messages['invalid_year']
                raise forms.ValidationError(error)
            if data_list[0] in forms.fields.EMPTY_VALUES:
                error = self.error_messages['invalid_month']
                raise forms.ValidationError(error)
            year = int(data_list[1])
            month = int(data_list[0])
            # find last day of the month
            day = monthrange(year, month)[1]
            return date(year, month, day)
        return None


class CreditCardVerificationField(forms.CharField):

    widget = forms.TextInput(
        attrs={'autocomplete': 'cc-csc'})
    default_error_messages = {
        'invalid': _('Enter a valid security number.')}

    def __init__(self, *args, **kwargs):
        kwargs['max_length'] = kwargs.pop('max_length', 4)
        super(CreditCardVerificationField, self).__init__(*args, **kwargs)

    def validate(self, value):
        if value in validators.EMPTY_VALUES and self.required:
            raise forms.ValidationError(self.error_messages['required'])
        if value and not re.match('^[0-9]{3,4}$', value):
            raise forms.ValidationError(self.error_messages['invalid'])


class CreditCardNameField(forms.CharField):

    widget = forms.TextInput(
        attrs={'autocomplete': 'cc-name', 'required': 'required'})