import calendar
import datetime
from django.core.exceptions import ValidationError
from django.utils.translation import gettext_lazy as _
VALID_COUNTRY_CODES = ("+1", "+52")
[docs]
def validate_e164_phone_number(value: str) -> None:
"""
Raises :py:exc:`~django.core.exceptions.ValidationError` if the value is not a valid `E.164 <https://en.wikipedia.org/wiki/E.164>`_ formatted phone number.
* Country Code: A 2-5 character code with a leading '+' indicating the phone number's destination country.
* Area Code: The first 3 digits of the phone number.
* Subscriber Number: The last 7 digits of the phone number.
:param value: A phone number in `E.164 <https://en.wikipedia.org/wiki/E.164>`_ format.
:type value: str
:raises ~django.core.exceptions.ValidationError: If the phone number wasn't provided.
:raises ~django.core.exceptions.ValidationError: If the phone number didn't start with a '+' character.
:raises ~django.core.exceptions.ValidationError: If the phone number contained any number of spaces.
:raises ~django.core.exceptions.ValidationError: If the phone number contained any number of hyphens.
:raises ~django.core.exceptions.ValidationError: If the phone number was less than 12 characters in length.
:raises ~django.core.exceptions.ValidationError: If the phone number was greater than 15 characters in length.
:raises ~django.core.exceptions.ValidationError: If the country code was invalid.
:raises ~django.core.exceptions.ValidationError: If the area code wasn't exactly 3 characters in length.
:raises ~django.core.exceptions.ValidationError: If the area code wasn't a digit.
:raises ~django.core.exceptions.ValidationError: If the subscriber number (non-area code number) wasn't exactly 7 characters in length.
:raises ~django.core.exceptions.ValidationError: If the subscriber number (non-area code number) wasn't a digit.
:returns: Nothing.
:rtype: None
"""
if not value:
raise ValidationError(
_("This field is required, got '%(value)s'"),
code="invalid",
params={"value": value},
)
if not value.startswith("+"):
raise ValidationError(
_("E.164 phone number must begin with a '+', got '%(char)s'."),
code="invalid",
params={"char": value[0]},
)
if " " in value:
raise ValidationError(
_("E.164 phone number cannot contain spaces, got '%(value)s'."),
code="invalid",
params={"value": value},
)
if "-" in value:
raise ValidationError(
_("E.164 phone number cannot contain hyphens, got '%(value)s'."),
code="invalid",
params={"value": value},
)
if len(value) < 12:
raise ValidationError(
_(
"E.164 phone number cannot be less than 12 characters in length, got %(len)s."
),
code="invalid",
params={"len": len(value)},
)
if len(value) > 15:
raise ValidationError(
_(
"E.164 phone number cannot be greater than 15 characters in length, got %(len)s."
),
code="invalid",
params={"len": len(value)},
)
country_code = value[:-10]
area_code = value[-10:-7]
subscriber_number = value[-7:]
if len(country_code) < 2 or len(country_code) > 5:
raise ValidationError(
_(
"E.164 phone number country code must be between 2 and 5 characters in length, got %(len)s."
),
code="invalid",
params={"len": len(country_code)},
)
if country_code not in VALID_COUNTRY_CODES:
raise ValidationError(
_(
"E.164 phone number cannot contain an invalid country code, got '%(country_code)s'."
),
code="invalid",
params={"country_code": country_code},
)
if not len(area_code) == 3:
raise ValidationError(
_(
"E.164 phone number must contain a 3-digit area code, got '%(area_code)s'."
),
code="invalid",
params={"area_code": area_code},
)
if not area_code.isdigit():
raise ValidationError(
_(
"E.164 phone number must have a valid area code, got '%(area_code)s'."
),
code="invalid",
params={"area_code": area_code},
)
if not len(subscriber_number) == 7:
raise ValidationError(
_(
"E.164 phone number must contain a 7-digit subscriber number, got '%(subscriber_number)s'."
),
code="invalid",
params={"subscriber_number": subscriber_number},
)
if not subscriber_number.isdigit():
raise ValidationError(
_(
"E.164 phone number must have a valid subscriber number, got '%(subscriber_number)s'."
),
code="invalid",
params={"subscriber_number": subscriber_number},
)
[docs]
def validate_credit_card_number(value: str) -> None:
"""
Raises :py:exc:`~django.core.exceptions.ValidationError` if the value is an invalid credit card number.
Uses the `Luhn algorithm <https://en.wikipedia.org/wiki/Luhn_algorithm>`_ to validate the credit card number.
:param value: A credit card number.
:type value: str
:raises ~django.core.exceptions.ValidationError: If the credit card number contained non-digit characters.
:raises ~django.core.exceptions.ValidationError: If the credit card number failed the Luhn algorithm check.
:returns: Nothing.
:rtype: None
"""
if not value.isdigit():
raise ValidationError(
_("Credit card number can only contain digits. Got '%(value)s'."),
code="invalid",
params={"value": value},
)
card_number = [int(num) for num in reversed(value)]
even_digits = card_number[1::2]
odd_digits = card_number[0::2]
checksum = 0
checksum += sum(
[
digit * 2 if digit * 2 <= 9 else (digit * 2) % 9 or 9
for digit in even_digits
]
)
checksum += sum([digit for digit in odd_digits])
if checksum % 10 != 0:
raise ValidationError(_("Invalid credit card number."), code="invalid")
[docs]
def validate_credit_card_expiry_month(value: str) -> None:
"""
Raises :py:exc:`~django.core.exceptions.ValidationError` if the value is an invalid credit card expiration date month.
:param value: A credit card expiration month.
:type value: str
:raises ~django.core.exceptions.ValidationError: If the expiration month contained non-digit characters.
:raises ~django.core.exceptions.ValidationError: If the expiration month was negative.
:raises ~django.core.exceptions.ValidationError: If the expiration month was an invalid (non-existent) month.
:returns: Nothing.
:rtype: None
"""
if not value.isdigit():
raise ValidationError(
_("Expiration month can only contain digits, got '%(value)s'."),
code="invalid",
params={"value": value},
)
if not int(value) > 0:
raise ValidationError(
_(
"Expiration month can only be a positive value, got '%(value)s'."
),
code="invalid",
params={"value": value},
)
try:
calendar.Month(int(value))
except ValueError:
raise ValidationError(
_("Expiration month must be between 1-12, got '%(value)s'."),
code="invalid",
params={"value": value},
)
[docs]
def validate_credit_card_expiry_year(value: str) -> None:
"""
Raises :py:exc:`~django.core.exceptions.ValidationError` if the value is an invalid credit card expiration date year.
:param value: A credit card expiration year.
:type value: str
:raises ~django.core.exceptions.ValidationError: If the expiration year contained non-digit characters.
:raises ~django.core.exceptions.ValidationError: If the expiration year was negative.
:raises ~django.core.exceptions.ValidationError: If the expiration year was a year in the past.
:returns: Nothing.
:rtype: None
"""
if not value.isdigit():
raise ValidationError(
_("Expiration year can only contain digits, got '%(value)s'."),
code="invalid",
params={"value": value},
)
if not int(value) > 0:
raise ValidationError(
_(
"Expiration year can only be a positive value, got '%(value)s'."
),
code="invalid",
params={"value": value},
)
input_year = datetime.datetime.strptime(value, "%y").year
this_year = datetime.datetime.now().year
if not input_year >= this_year:
raise ValidationError(
_("Expiration year cannot be in the past, got '%(value)s'."),
code="invalid",
params={"value": value},
)