# -*- coding: utf-8 -*-
# Biryani -- A conversion and validation toolbox
# By: Emmanuel Raviart <emmanuel@raviart.com>
#
# Copyright (C) 2009, 2010, 2011, 2012, 2013, 2014, 2015 Emmanuel Raviart
# http://packages.python.org/Biryani/
#
# This file is part of Biryani.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Converters for credit card types, numbers and expiration dates
.. note:: The idea of this module and its algorithms have been borrowed from `FormEncode <http://formencode.org/>`_.
Sample usage:
>>> import pprint
>>> from biryani import baseconv, creditcardconv, custom_conv, states
...
>>> conv = custom_conv(baseconv, creditcardconv)
>>> N_ = lambda message: message
...
>>> def validate_credit_card(value, state = None):
... import datetime
...
... if value is None:
... return None, None
... if state is None:
... state = states.default_state
... errors = {}
... today = datetime.date.today()
... converted_value, error = conv.struct(
... dict(
... credit_card_type = conv.pipe(conv.input_to_credit_card_type, conv.not_none),
... expiration_month = conv.pipe(
... conv.input_to_int,
... conv.test_between(1, 12, error = N_(u'Invalid expiration month')),
... conv.not_none,
... ),
... expiration_year = conv.pipe(
... conv.input_to_int,
... conv.test_greater_or_equal(today.year, error = N_(u'Invalid expiration year')),
... conv.not_none,
... ),
... ),
... default = conv.noop,
... )(value, state = state)
... if error is not None:
... errors.update(error)
... if 'expiration_month' not in errors and 'expiration_year' not in errors:
... expiration_year = converted_value['expiration_year']
... expiration_month = converted_value['expiration_month'] + 1
... if expiration_month > 12:
... expiration_month = 1
... expiration_year += 1
... if datetime.date(expiration_year, expiration_month, 1) <= today:
... errors['expiration_month'] = state._(u'Invalid expiration date')
... if 'credit_card_type' in errors:
... return converted_value, errors
... credit_card_type = converted_value['credit_card_type']
... converted_value, error = conv.struct(
... dict(
... credit_card_number = conv.pipe(
... conv.make_input_to_credit_card_number(credit_card_type),
... conv.not_none,
... ),
... credit_card_security_code = conv.pipe(
... conv.make_input_to_credit_card_security_code(credit_card_type),
... conv.not_none,
... ),
... ),
... default = conv.noop,
... )(converted_value, state = state)
... if error is not None:
... errors.update(error)
... return converted_value, errors or None
>>> pprint.pprint(validate_credit_card(dict(
... credit_card_type = u'Visa',
... credit_card_number = u'4111 1111 1111 1111',
... credit_card_security_code = u'123',
... expiration_month = u'12',
... expiration_year = u'2021',
... )))
({'credit_card_number': u'4111111111111111',
'credit_card_security_code': 123,
'credit_card_type': u'visa',
'expiration_month': 12,
'expiration_year': 2021},
None)
>>> pprint.pprint(validate_credit_card(dict(
... credit_card_type = u'Visa',
... credit_card_number = u'4111-1111-1111-1112',
... credit_card_security_code = u'123',
... expiration_month = u'12',
... expiration_year = u'2021',
... )))
({'credit_card_number': u'4111111111111112',
'credit_card_security_code': 123,
'credit_card_type': u'visa',
'expiration_month': 12,
'expiration_year': 2021},
{'credit_card_number': u'Invalid credit card number (wrong checksum)'})
>>> pprint.pprint(validate_credit_card(dict(
... credit_card_type = u'Visa',
... credit_card_number = u'4111_1111_1111',
... credit_card_security_code = u'1234',
... expiration_month = u'7',
... expiration_year = u'2011',
... )))
({'credit_card_number': u'411111111111',
'credit_card_security_code': u'1234',
'credit_card_type': u'visa',
'expiration_month': 7,
'expiration_year': 2011},
{'credit_card_number': u'Wrong number of digits in credit card number',
'credit_card_security_code': u'Invalid security code for credit card',
'expiration_year': u'Invalid expiration year'})
"""
from .baseconv import cleanup_line, N_, pipe, input_to_int, input_to_slug, test, test_in
from . import states, strings
__all__ = [
'input_to_credit_card_type',
'make_input_to_credit_card_number',
'make_input_to_credit_card_security_code',
'make_str_to_credit_card_number',
]
credit_cards_prefix_and_length = {
"amex": [
('34', 15),
('37', 15),
],
"dinersclub": [
('300', 14),
('301', 14),
('302', 14),
('303', 14),
('304', 14),
('305', 14),
('36', 14),
('38', 14)],
"discover": [
('6011', 16),
],
"jcb": [
('3', 16),
('2131', 15),
('1800', 15),
],
"mastercard": [
('51', 16),
('52', 16),
('53', 16),
('54', 16),
('55', 16),
],
"visa": [
('4', 16),
('4', 13),
],
}
credit_cards_security_code_length = {
"amex": 4,
"discover": 3,
"mastercard": 3,
"visa": 3,
}
# Level-1 Converters
input_to_credit_card_type = pipe(
input_to_slug,
test_in(credit_cards_prefix_and_length.keys(), error = N_(u'Unknown type of credit card')),
)
[docs]def make_str_to_credit_card_number(type):
"""Return a converter that converts a clean string to a credit card number of a given type.
.. note:: For a converter that doesn't require a clean string, see :func:`make_input_to_credit_card_number`.
>>> make_str_to_credit_card_number(u'visa')(u'4111111111111111')
(u'4111111111111111', None)
>>> make_str_to_credit_card_number(u'visa')(u' 4111 1111-1111.,1111 ')
(u'4111111111111111', None)
>>> make_str_to_credit_card_number(u'visa')(u'411111111111111')
(u'411111111111111', u'Wrong number of digits in credit card number')
>>> make_str_to_credit_card_number(u'visa')(u'4111111111111112')
(u'4111111111111112', u'Invalid credit card number (wrong checksum)')
>>> make_str_to_credit_card_number(u'visa')(u'5111111111111111')
(u'5111111111111111', u'Invalid credit card number (unknown prefix)')
>>> make_str_to_credit_card_number(u'visa')(u'')
(u'', u'Credit card number must contain digits')
>>> make_str_to_credit_card_number(u'visa')(u' ')
(u'', u'Credit card number must contain digits')
>>> make_str_to_credit_card_number(u'visa')(None)
(None, None)
"""
def str_to_credit_card_number(credit_card_number, state = None):
if credit_card_number is None:
return credit_card_number, None
if state is None:
state = states.default_state
# Check that credit card number contains only digits.
credit_card_number = strings.slugify(credit_card_number, separator = u'')
if not credit_card_number:
return credit_card_number, state._(u'Credit card number must contain digits')
if not credit_card_number.isdigit():
return credit_card_number, state._(u'Credit card number must contain only digits')
# Check prefix and length.
length_found = False
for prefix, length in credit_cards_prefix_and_length[type]:
if len(credit_card_number) == length:
length_found = True
if credit_card_number.startswith(prefix):
break
else:
if not length_found:
return credit_card_number, state._(
u'Wrong number of digits in credit card number')
return credit_card_number, state._(u'Invalid credit card number (unknown prefix)')
# Check checksum.
checksum = 0
for index, digit in enumerate(reversed(credit_card_number)):
if index & 1:
for digit in unicode(2 * int(digit)):
checksum += int(digit)
else:
checksum += int(digit)
if checksum % 10 != 0:
return credit_card_number, state._(u'Invalid credit card number (wrong checksum)')
return credit_card_number, None
return str_to_credit_card_number
# Level-2 Converters