View source
<?php
namespace Drupal\commerce_payment;
use Drupal\Core\StringTranslation\TranslatableMarkup;
final class CreditCard {
public static $types = [];
public static function getType(string $id) : CreditCardType {
$types = self::getTypes();
if (!isset($types[$id])) {
throw new \InvalidArgumentException(sprintf('Invalid credit card type "%s"', $id));
}
return $types[$id];
}
public static function getTypes() : array {
$definitions = [
'visa' => [
'id' => 'visa',
'label' => new TranslatableMarkup('Visa'),
'number_prefixes' => [
'4',
],
'number_lengths' => [
16,
18,
19,
],
],
'mastercard' => [
'id' => 'mastercard',
'label' => new TranslatableMarkup('Mastercard'),
'number_prefixes' => [
'51-55',
'222100-272099',
],
],
'maestro' => [
'id' => 'maestro',
'label' => new TranslatableMarkup('Maestro'),
'number_prefixes' => [
'5018',
'502',
'503',
'506',
'56',
'58',
'639',
'6220',
'67',
],
'number_lengths' => [
12,
13,
14,
15,
16,
17,
18,
19,
],
],
'amex' => [
'id' => 'amex',
'label' => new TranslatableMarkup('American Express'),
'number_prefixes' => [
'34',
'37',
],
'number_lengths' => [
15,
],
'security_code_length' => 4,
],
'dinersclub' => [
'id' => 'dinersclub',
'label' => new TranslatableMarkup('Diners Club'),
'number_prefixes' => [
'300-305',
'309',
'36',
'38',
'39',
],
'number_lengths' => [
14,
16,
19,
],
],
'discover' => [
'id' => 'discover',
'label' => new TranslatableMarkup('Discover Card'),
'number_prefixes' => [
'6011',
'622126-622925',
'644-649',
'65',
],
'number_lengths' => [
16,
19,
],
],
'jcb' => [
'id' => 'jcb',
'label' => new TranslatableMarkup('JCB'),
'number_prefixes' => [
'3528-3589',
],
'number_lengths' => [
16,
17,
18,
19,
],
],
'unionpay' => [
'id' => 'unionpay',
'label' => new TranslatableMarkup('UnionPay'),
'number_prefixes' => [
'62',
'88',
],
'number_lengths' => [
16,
17,
18,
19,
],
'uses_luhn' => FALSE,
],
];
foreach ($definitions as $id => $definition) {
self::$types[$id] = new CreditCardType($definition);
}
return self::$types;
}
public static function getTypeLabels() : array {
$types = self::getTypes();
$type_labels = array_map(function ($type) {
return $type
->getLabel();
}, $types);
return $type_labels;
}
public static function detectType($number) {
if (!is_numeric($number)) {
return NULL;
}
$types = self::getTypes();
foreach ($types as $type) {
foreach ($type
->getNumberPrefixes() as $prefix) {
if (self::matchPrefix($number, $prefix)) {
return $type;
}
}
}
return NULL;
}
public static function matchPrefix(string $number, string $prefix) : bool {
if (is_numeric($prefix)) {
return substr($number, 0, strlen($prefix)) == $prefix;
}
else {
list($start, $end) = explode('-', $prefix);
$number = substr($number, 0, strlen($start));
return $number >= $start && $number <= $end;
}
}
public static function validateNumber(string $number, CreditCardType $type) : bool {
if (!is_numeric($number)) {
return FALSE;
}
if (!in_array(strlen($number), $type
->getNumberLengths())) {
return FALSE;
}
if ($type
->usesLuhn() && !self::validateLuhn($number)) {
return FALSE;
}
return TRUE;
}
public static function validateLuhn(string $number) : bool {
$total = 0;
foreach (array_reverse(str_split($number)) as $i => $digit) {
$digit = $i % 2 ? $digit * 2 : $digit;
$digit = $digit > 9 ? $digit - 9 : $digit;
$total += $digit;
}
return $total % 10 === 0;
}
public static function validateExpirationDate(string $month, string $year) : bool {
if ($month < 1 || $month > 12) {
return FALSE;
}
if ($year < date('Y')) {
return FALSE;
}
elseif ($year == date('Y') && $month < date('n')) {
return FALSE;
}
return TRUE;
}
public static function calculateExpirationTimestamp(string $month, string $year) : int {
$month_start = strtotime($year . '-' . $month . '-01');
$last_day = date('t', $month_start);
return mktime(23, 59, 59, $month, $last_day, $year);
}
public static function validateSecurityCode(string $security_code, CreditCardType $type) : bool {
if (!is_numeric($security_code)) {
return FALSE;
}
if (strlen($security_code) != $type
->getSecurityCodeLength()) {
return FALSE;
}
return TRUE;
}
public static function getAvsResponseCodeMeanings() : array {
$standard_codes = [
'A' => new TranslatableMarkup('Address'),
'B' => new TranslatableMarkup('International "A"'),
'C' => new TranslatableMarkup('International "N"'),
'D' => new TranslatableMarkup('International "X"'),
'E' => new TranslatableMarkup('Not allowed for MOTO (Internet/Phone) transactions'),
'F' => new TranslatableMarkup('UK-specific "X"'),
'G' => new TranslatableMarkup('Global Unavailable'),
'I' => new TranslatableMarkup('International Unavailable'),
'M' => new TranslatableMarkup('Address'),
'N' => new TranslatableMarkup('No'),
'P' => new TranslatableMarkup('Postal (International "Z")'),
'R' => new TranslatableMarkup('Retry'),
'S' => new TranslatableMarkup('AVS not Supported'),
'U' => new TranslatableMarkup('Unavailable'),
'W' => new TranslatableMarkup('Whole ZIP'),
'X' => new TranslatableMarkup('Exact match'),
'Y' => new TranslatableMarkup('Yes'),
'Z' => new TranslatableMarkup('ZIP'),
];
$amex_codes = [
'A' => new TranslatableMarkup('Card holder address only correct.'),
'D' => new TranslatableMarkup('Card holder name incorrect, postal code matches.'),
'E' => new TranslatableMarkup('Card holder name incorrect, address and postal code match.'),
'F' => new TranslatableMarkup('Card holder name incorrect, address matches.'),
'K' => new TranslatableMarkup('Card holder name matches.'),
'L' => new TranslatableMarkup('Card holder name and postal code match.'),
'M' => new TranslatableMarkup('Card holder name, address and postal code match.'),
'N' => new TranslatableMarkup('No, card holder address and postal code are both incorrect.'),
'O' => new TranslatableMarkup('Card holder name and address match.'),
'R' => new TranslatableMarkup('System unavailable; retry.'),
'S' => new TranslatableMarkup('AVS not supported.'),
'U' => new TranslatableMarkup('Information unavailable.'),
'W' => new TranslatableMarkup('No, card holder name, address and postal code are all incorrect.'),
'Y' => new TranslatableMarkup('Yes, card holder address and postal code are both correct.'),
'Z' => new TranslatableMarkup('Card holder postal code only correct.'),
] + $standard_codes;
return [
'visa' => $standard_codes,
'mastercard' => $standard_codes,
'amex' => $amex_codes,
'discover' => $standard_codes,
'maestro' => [
'0' => new TranslatableMarkup('All the address information matched.'),
'1' => new TranslatableMarkup('None of the address information matched.'),
'2' => new TranslatableMarkup('Part of the address information matched.'),
'3' => new TranslatableMarkup('The merchant did not provide AVS information. Not processed.'),
'4' => new TranslatableMarkup('Address not checked, or acquirer had no response. Service not available.'),
'U' => new TranslatableMarkup('Address not checked, or acquirer had no response. Service not available.'),
],
];
}
}