Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 18 additions & 5 deletions creditcard/card.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import re
from functools import cached_property

from . import luhn
from .exceptions import BrandNotFound
from .luhn import Luhn
from .utils import sanitize

BRAND_REGEX = {
Expand Down Expand Up @@ -69,11 +70,23 @@ class CreditCard:
def __init__(self, number):
self.number = sanitize(number)

def is_valid(self):
return all([len(self.number) in range(13, 19), Luhn.checkdigit(self.number)])

def get_brand(self):
@cached_property
def _brand(self):
Comment thread
felipetrz-maistodos marked this conversation as resolved.
for brand, regex in BRAND_REGEX.items():
if re.match(regex, self.number):
return brand
return None

def is_valid(self):
if not self.number.isdigit():
return False

if self._brand == "softnex":
return len(self.number) == 16 and luhn.check_softnex(self.number)

return len(self.number) in range(13, 19) and luhn.check(self.number)

def get_brand(self):
if brand := self._brand:
Comment thread
felipetrz-maistodos marked this conversation as resolved.
return brand
raise BrandNotFound("Card number does not match any brand")
49 changes: 31 additions & 18 deletions creditcard/luhn.py
Original file line number Diff line number Diff line change
@@ -1,18 +1,31 @@
class Luhn:
@classmethod
def checkdigit(cls, card_number):
""" checks to make sure that the card passes a luhn mod-10 checksum """
_sum = 0
num_digits = len(card_number)
oddeven = num_digits & 1
for i in range(0, num_digits):
try:
digit = int(card_number[i])
except ValueError:
return False
if not ((i & 1) ^ oddeven):
digit = digit * 2
if digit > 9:
digit = digit - 9
_sum = _sum + digit
return (_sum % 10) == 0
def sum_digits(digits):
total = 0
for i, digit in enumerate(digits, start=1):
# Se o índice (iniciando em 1) for ímpar, o valor do dígito é o dígito
# original multiplicado por dois, com os dígitos resultantes somados,
# o que equivale a subtrair 9 se o valor for maior que 9.
# Se for par, o próprio dígito é utilizado, que já está entre 0 e 9.
d = int(digit)
if i % 2 == 1:
d *= 2
if d > 9:
d -= 9
total += d
return total


def check(digits):
total = sum_digits(digits)
return total % 10 == 0


def check_softnex(digits):
total = sum_digits(digits[:15])
# A Softnex diverge do Luhn tradicional no cálculo do último dígito.
# Enquanto o Luhn apenas requer que a soma em módulo 10 seja 0, a Softnex
# modifica o dígito para 1 se o mesmo for 0, o que faz com que o Luhn recuse
# o número resultante.
dv = 10 - total % 10
if dv > 9:
dv = 1
return int(digits[-1]) == dv
3 changes: 1 addition & 2 deletions pytest.ini
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,4 @@
norecursedirs = .git
testpaths = ./tests
pythonpaths = . ./creditcard
addopts = --flake8 --cov=creditcard -vv
flake8-max-line-length = 88
addopts = --cov=creditcard -vv
5 changes: 5 additions & 0 deletions tests/unit/test_card.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,11 @@ def test_is_valid(self):
card = CreditCard("0000000000000a")
assert card.is_valid() is False

# Cartão Softnex que não é válido pelo Luhn, mas deve ser aceito pelo
# algoritmo específico
card = CreditCard("6108000000000041")
assert card.is_valid() is True

@pytest.mark.parametrize(
"brand, number",
[
Expand Down
34 changes: 28 additions & 6 deletions tests/unit/test_luhn.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,31 @@
from creditcard.luhn import Luhn
import pytest

from creditcard import luhn


class TestLuhn:
def test_checkdigit(self):
assert Luhn.checkdigit('4539578763621486') is True
assert Luhn.checkdigit('5369835519963014') is True
assert Luhn.checkdigit('30346836403940') is True
assert Luhn.checkdigit('3034683640394a') is False
@pytest.mark.parametrize(
"number,expected",
[
("4539578763621486", True),
("5369835519963014", True),
("5369835519963013", False),
("30346836403940", True),
("6108000158537041", True),
("6108000000000041", False),
("6108000000000040", True),
],
)
def test_check(self, number, expected):
assert luhn.check(number) is expected

@pytest.mark.parametrize(
"number,expected",
[
("6108000158537041", True),
("6108000000000041", True),
("6108000000000040", False),
],
)
def test_check_softnex(self, number, expected):
assert luhn.check_softnex(number) is expected
Loading