Code Example: Basic Credit Card Validation

The Story

While working on the Museum of Play, I needed a basic credit card validation script that would check the physical format of a credit card number before hitting the payment processor because Blackbaud had a tendency to lock out randomly while testing, requiring the folks at MoP to call them to remove the lock. Ridiculous.

Jump to:

Usage

bool validate_credit_card ( int $card_number, string $card_company );

Result

Most should be true:
--------------------
bool(true)
bool(true)
bool(true)
bool(true)
bool(true)
bool(true)
string(46) "Merchant does not accept this credit card type"
string(46) "Merchant does not accept this credit card type"
bool(true)
bool(true)
bool(true)
bool(true)
bool(true)
string(46) "Merchant does not accept this credit card type"


All should return an error message:
-----------------------------------
string(32) "No parameters have been provided"
string(23) "No card number provided"
string(21) "No card type provided"
string(17) "Unknown card type"
string(46) "Merchant does not accept this credit card type"
string(37) "Credit card number has invalid format"
string(34) "Credit card number is wrong length"
string(37) "Credit card number has invalid format"
string(72) "Credit card number does not start with a valid prefix for this card type"
string(29) "Credit card number is invalid"


Should return an error message because they failed Luhn:
--------------------------------------------------------
string(29) "Credit card number is invalid"

BasicCreditCardValidation.php

<?php
// test cases are at the bottom of the script

////////////////////////////////////////////////////////////////////////////////
//
//    This script is based off the PHP Credit Card script from
//    http://www.braemoor.co.uk/software/creditcard.php
//
//    The following simple checks are made against a given credit card number and type:
//
//    1. A credit card number has been provided
//    2. The number is a right length for the card
//    3. The number has an appropriate prefix for the card
//    4. The number passes the Luhn algorithm
//
////////////////////////////////////////////////////////////////////////////////

////////////////////////////////////////////////////////////////////////////////
//
//    credit card types
//
////////////////////////////////////////////////////////////////////////////////

define('CC_AMEX',        0);    // American Express
define('CC_DCCB',        1);    // Diners Club Carte Blanche
define('CC_DC',          2);    // Diners Club
define('CC_DISC',        3);    // Discover
define('CC_DCER',        4);    // Diners Club Enroute
define('CC_JCB',         5);    // JCB
define('CC_MCM',         6);    // Maestro (MasterCard debit)
define('CC_MC',          7);    // Mastercard
define('CC_SOLO',        8);    // Solo
define('CC_SWITCH',      9);    // Switch
define('CC_VISA',       10);    // Visa
define('CC_VISAe',      11);    // Visa Electron
define('CC_LC',         12);    // LaserCard
/*
define('CC_FUTURE1',    13);    // reserved for future expansion
define('CC_FUTURE2',    14);    // reserved for future expansion
define('CC_FUTURE3',    15);    // reserved for future expansion
define('CC_FUTURE4',    16);    // reserved for future expansion
*/

////////////////////////////////////////////////////////////////////////////////
//
//    list of credit card type(s) accepted by this merchant
//
////////////////////////////////////////////////////////////////////////////////

$MERCHANT_ACCEPTS = array(CC_AMEX, CC_DISC, CC_MC, CC_VISA, CC_DC);

////////////////////////////////////////////////////////////////////////////////
//
//    credit card definitions
//
////////////////////////////////////////////////////////////////////////////////

//      name:   useful for form selection boxes
//      code:   the short code for this card (for use with constant() )
//    length:   list of possible valid lengths of the card number for this type
//  prefixes:   list of possible prefixes for the card
//  run_luhn:   boolean to indicate whether this card conforms to a Luhn check

$CARDS = array (    CC_AMEX =>
                            array ( 'name'          => 'American Express',
                                    'code'          => 'CC_AMEX',
                                    'length'        => array ('15'),
                                    'prefixes'      => array ('34',
                                                              '37'),
                                    'run_luhn'      => true),
                    CC_DCCB =>
                            array ( 'name'           => 'Diners Club Carte Blanche',
                                    'code'           => 'CC_DCCB',
                                    'length'         => array ('14'),
                                    'prefixes'       => array ('300',
                                                               '301',
                                                               '302',
                                                               '303',
                                                               '304',
                                                               '305'),
                                    'run_luhn'       => true),
                    CC_DC =>
                            array ( 'name'           => 'Diners Club', 
                                    'code'           => 'CC_DC',
                                    'length'         => array ('14', '16'),
                                    'prefixes'       => array ('305',
                                                               '36',
                                                               '38',
                                                               '54',
                                                               '55'),
                                    'run_luhn'       => true),
                    CC_DISC =>
                            array ( 'name'           => 'Discover',
                                    'code'           => 'CC_DISC',
                                    'length'         => array ('16'),
                                    'prefixes'       => array ('6011',
                                                               '622',
                                                               '64',
                                                               '65'),
                                    'run_luhn'       => true),
                    CC_DCER =>
                            array ( 'name'           => 'Diners Club enRoute',
                                    'code'           => 'CC_DCER',
                                    'length'         => array ('15'),
                                    'prefixes'       => array ('2014',
                                                               '2149'),
                                    'run_luhn'       => false),
                    CC_JCB =>
                            array ( 'name'           => 'JCB',
                                    'code'           => 'CC_JCB',
                                    'length'         => array ('16'),
                                    'prefixes'       => array ('35'),
                                    'run_luhn'       => true),
                    CC_MCM =>
                            array ( 'name'           => 'Maestro',
                                    'code'           => 'CC_MCM',
                                    'length'         => array ('12', '13', '14', '15', '16', '18', '19'), 
                                    'prefixes'       => array ('5018',
                                                               '5020',
                                                               '5038',
                                                               '6304',
                                                               '6759',
                                                               '6761'),
                                    'run_luhn'       => true),
                    CC_MC =>
                            array ( 'name'           => 'MasterCard',
                                    'code'           => 'CC_MC',
                                    'length'         => array ('16'),
                                    'prefixes'       => array ('51',
                                                               '52',
                                                               '53',
                                                               '54',
                                                               '55'),
                                    'run_luhn'        => true),
                    CC_SOLO =>
                            array ( 'name'           => 'Solo',
                                    'code'           => 'CC_SOLO',
                                    'length'         => array ('16', '18', '19'),
                                    'prefixes'       => array ('6334',
                                                               '6767'),
                                    'run_luhn'       => true),
                    CC_SWITCH =>
                            array ( 'name'           => 'Switch',
                                    'code'           => 'CC_SWITCH',
                                    'length'         => array ('16', '18', '19'),
                                    'prefixes'       => array ('4903',
                                                               '4905',
                                                               '4911',
                                                               '4936',
                                                               '564182',
                                                               '633110',
                                                               '6333',
                                                               '6759'),
                                    'run_luhn'       => true),
                    CC_VISA =>
                            array ( 'name'           => 'Visa',
                                    'code'           => 'CC_VISA',
                                    'length'         => array ('13', '16'),
                                    'prefixes'       => array ('4'),
                                    'run_luhn'       => true),
                    CC_VISAe =>
                            array ( 'name'           => 'Visa Electron',
                                    'code'           => 'CC_VISAe',
                                    'length'         => '16', 
                                    'prefixes'       => array ('417500',
                                                               '4917',
                                                               '4913',
                                                               '4508',
                                                               '4844'),
                                    'run_luhn'       => true),
                    CC_LC =>
                            array ( 'name'           => 'LaserCard',
                                    'code'           => 'CC_LC',
                                    'length'         => array ('16', '17', '18', '19'),
                                    'prefixes'       => array ('6304',
                                                               '6706',
                                                               '6771',
                                                               '6709'),
                                    'run_luhn'       => true)
);

////////////////////////////////////////////////////////////////////////////////
//
//    credit card error types
//
////////////////////////////////////////////////////////////////////////////////

define('ERROR_NO_PARAMETERS',        "No parameters have been provided");
define('ERROR_NO_NUMBER',            "No card number provided");
define('ERROR_NO_TYPE',              "No card type provided");
define('ERROR_UNKNOWN_CARD_TYPE',    "Unknown card type");
define('ERROR_MERCHANT_NOT_ACCEPT',  "Merchant does not accept this credit card type");
define('ERROR_WRONG_LENGTH',         "Credit card number is wrong length");
define('ERROR_WRONG_FORMAT',         "Credit card number has invalid format");
define('ERROR_WRONG_PREFIX',         "Credit card number does not start with a valid prefix for this card type");
define('ERROR_INVALID_CARD',         "Credit card number is invalid");


function validate_credit_card ($card_number, $card_company)
{
    global $CARDS;
    global $MERCHANT_ACCEPTS;
	
	$card_number = trim($card_number);
	$card_company = trim($card_company);
    
    ////////////////////////////////////////////////////////////////////////////////
    //
    //    make sure card number and card company aren't empty, dur.
    //
    ////////////////////////////////////////////////////////////////////////////////
    
    if (empty($card_number) && empty($card_company))
    {
        return ERROR_NO_PARAMETERS; 
    }
    
    if (empty($card_number))
    {
        return ERROR_NO_NUMBER; 
    }
    
    if (empty($card_company))
    {
        return ERROR_NO_TYPE; 
    }
    
    ////////////////////////////////////////////////////////////////////////////////
    //
    //    is this a valid card type?
    //
    ////////////////////////////////////////////////////////////////////////////////
    $card_type = -1;
    
    for ($i = 0; $i < sizeof($CARDS); $i++)
    {
        if (strtolower($card_company) == strtolower($CARDS[$i]['name']))
        {
            $card_type = $CARDS[$i]['code'];
            break;
        }
    }
    
    // not a valid card type
    if ($card_type == -1)
    {    
        return ERROR_UNKNOWN_CARD_TYPE; 
    }

    ////////////////////////////////////////////////////////////////////////////////
    //
    //    does the merchant accept this credit card?
    //
    ////////////////////////////////////////////////////////////////////////////////
    
    if (!in_array(constant($card_type), $MERCHANT_ACCEPTS))
    {
        return ERROR_MERCHANT_NOT_ACCEPT;
    }

    ////////////////////////////////////////////////////////////////////////////////
    //
    //    is there a numeric credit card number?
    //
    ////////////////////////////////////////////////////////////////////////////////

    // remove any spaces from the credit card number
    $card_number = str_replace (' ', '', $card_number);  
    
    // Check that the number is numeric and of the right sort of length.
    if (!preg_match('/^[0-9]{13,19}$/', $card_number)) 
    {
        return ERROR_WRONG_FORMAT; 
    }

    ////////////////////////////////////////////////////////////////////////////////
    //
    //    does this credit card number conform to the credit card type?
    //
    ////////////////////////////////////////////////////////////////////////////////
    
    // valid length?
    $valid_length = false;
    
    foreach ($CARDS[constant($card_type)]['length'] as $length)
    {
        if (strlen($card_number) == $length)
        {
            $valid_length = true;
            break;
        }
    }
    
    if ($valid_length == false)
    {
        return ERROR_WRONG_LENGTH;
    }
    
    // valid prefix?
    
    $valid_prefix = false;
    
    foreach ($CARDS[constant($card_type)]['prefixes'] as $prefix)
    {
        if (preg_match('/^' . $prefix . '/', $card_number))
        {
            $valid_prefix = true;
            break;
        }
    }
    
    if ($valid_prefix == false)
    {
        return ERROR_WRONG_PREFIX;
    }

    ////////////////////////////////////////////////////////////////////////////////
    //
    //    basic verification passed: now MOD10 / Luhn algorithm
    //
    ////////////////////////////////////////////////////////////////////////////////
    if ($CARDS[constant($card_type)]['run_luhn'])
    {
        $checksum = 0;
        
        ////////////////////////////////////////////////////////////////////////////
        //
        //    Step 1: Reverse credit card number
        //
        //    ex: 378282246310005 becomes 500013642282873
        //
        ////////////////////////////////////////////////////////////////////////////
        
        $card_number_reversed = strrev($card_number);
        
        ////////////////////////////////////////////////////////////////////////////
        //
        //    Step 2: The value of every second digit is doubled, starting with the
        //            second digit.   Add all the numbers together.  If any value is
        //            greater than 9, add them together first (e.g. 7 x 2 = 14 = (1 + 4))
        //
        //    ex: 5    0    0     0     1     3     6     4     2     2     8     2     8     7     3
        //            x2         x2          x2          x2          x2          x2          x2
        //        ---------------------------------------------------------------------------------------
        //        5 + (0) + 0  + (0) +  1  + (6) +  6  + (8) +  2  + (4) +  8  + (4)  + 8  + (14) + 3
        //                                                                                     \
        //        5 +  0  + 0  +  0  +  1  +  6  +  6  +  8  +  2  +  4  +  8  +  4   +  8 + (1 + 4) + 3
        //
        //        Total: 60
        //
        ////////////////////////////////////////////////////////////////////////////
        
        $card_number_array = str_split($card_number_reversed);

        $doubler = 1;
        
        foreach ($card_number_array as $digit)
        {
            // multiply by 1 or 2 on alternative digits
            $calc = $doubler * $digit;
                
            // if the multiplied digit is > 9, add 1 to the checksum and subtract 10 from the digit,
            // then add the digit to the checksum too
            if ($calc > 9)
            {
                $checksum += 1;
                $calc = $calc - 10;
            }
            
            // add the multiplied? digit to the checksum
            $checksum += $calc;
            
            // toggle the doubler
            $doubler = ($doubler === 1) ? (2) : (1);
        }
        
        ////////////////////////////////////////////////////////////////////////////
        //
        //    Step 3: MOD10 the resulting checksum.  If 0, card is valid
        //
        //    ex:    60 MOD 10 = 0
        //        Card is valid
        ////////////////////////////////////////////////////////////////////////////
        
        $modulus = $checksum % 10;
        
        if ($modulus != 0)
        {
            return ERROR_INVALID_CARD;
        }
    } // end Luhn algorithm
        
    // if we get all the way here, the credit card is in the required format.
    return true;
}

////////////////////////////////////////////////////////////////////////////////
//
//    TESTING ONLY
//
////////////////////////////////////////////////////////////////////////////////
?>
<pre>
Most should be true:
--------------------
<?php
$test = array('378282246310005'  => 'American Express', 
              '371449635398431'  => 'American Express', 
              '30569309025904'   => 'Diners Club', 
              '38520000023237'   => 'Diners Club',  
              '6011111111111117' => 'Discover',  
              '6011000990139424' => 'Discover', 
              '3530111333300000' => 'JCB', 
              '3566002020360505' => 'JCB', 
              '5555555555554444' => 'MasterCard', 
              '5105105105105100' => 'MasterCard', 
              '4111111111111111' => 'Visa', 
              '4012888888881881' => 'Visa', 
              '4222222222222'    => 'Visa', 
              '6331101999990016' => 'Switch');

foreach ($test as $cardno => $cardco)
{
	var_dump(validate_credit_card($cardno, $cardco));
}
?>


All should return an error message:
-----------------------------------
<?php 
$test2 = array(' '                  => ' ',                   // parameters empty 
               ''                   => 'American Express',    // no card number 
               '1234567890'         => '',                    // no card company 
               '0987654321'         => 'Fake Card Company',   // invalid card type 
               '30569309025904'     => 'Solo',                // merchant does not accept 
               '12345abcde67890'    => 'American Express',    // wrong format 
               '378282246310005555' => 'American Express',    // too long 
               '40128888881'        => 'Visa',                // too short (results in invalid format) 
               '1234555555554444'   => 'MasterCard',          // wrong prefix 
               '4000000000000000'   => 'Visa');               // failed Luhn algorithm

foreach ($test2 as $cardno => $cardco)
{
	var_dump(validate_credit_card($cardno, $cardco));
}
?>


Should return an error message because they failed Luhn:
--------------------------------------------------------
<?php var_dump(validate_credit_card('4000000000000000', 'Visa')); ?>
</pre>