I'm trying to figure out how to detect the type of credit card based purely on its number. Does anyone know of a definitive, reliable way to find this?
The credit/debit card number is referred to as a PAN, or Primary Account Number. The first six digits of the PAN are taken from the IIN, or Issuer Identification Number, belonging to the issuing bank (IINs were previously known as BIN — Bank Identification Numbers — so you may see references to that terminology in some documents). These six digits are subject to an international standard, ISO/IEC 7812, and can be used to determine the type of card from the number.
Unfortunately the actual ISO/IEC 7812 database is not publicly available, however, there are unofficial lists, both commercial and free, including on Wikipedia.
Anyway, to detect the type from the number, you can use a regular expression like the ones below: Credit for original expressions
Visa: ^4[0-9]{6,}$ Visa card numbers start with a 4.
MasterCard: ^5[1-5][0-9]{5,}|222[1-9][0-9]{3,}|22[3-9][0-9]{4,}|2[3-6][0-9]{5,}|27[01][0-9]{4,}|2720[0-9]{3,}$ Before 2016, MasterCard numbers start with the numbers 51 through 55, but this will only detect MasterCard credit cards; there are other cards issued using the MasterCard system that do not fall into this IIN range. In 2016, they will add numbers in the range (222100-272099).
American Express: ^3[47][0-9]{5,}$ American Express card numbers start with 34 or 37.
Diners Club: ^3(?:0[0-5]|[68][0-9])[0-9]{4,}$ Diners Club card numbers begin with 300 through 305, 36 or 38. There are Diners Club cards that begin with 5 and have 16 digits. These are a joint venture between Diners Club and MasterCard and should be processed like a MasterCard.
Discover: ^6(?:011|5[0-9]{2})[0-9]{3,}$ Discover card numbers begin with 6011 or 65.
JCB: ^(?:2131|1800|35[0-9]{3})[0-9]{3,}$ JCB cards begin with 2131, 1800 or 35.
Unfortunately, there are a number of card types processed with the MasterCard system that do not live in MasterCard’s IIN range (numbers starting 51...55); the most important case is that of Maestro cards, many of which have been issued from other banks’ IIN ranges and so are located all over the number space. As a result, it may be best to assume that any card that is not of some other type you accept must be a MasterCard.
Important: card numbers do vary in length; for instance, Visa has in the past issued cards with 13 digit PANs and cards with 16 digit PANs. Visa’s documentation currently indicates that it may issue or may have issued numbers with between 12 and 19 digits. Therefore, you should not check the length of the card number, other than to verify that it has at least 7 digits (for a complete IIN plus one check digit, which should match the value predicted by the Luhn algorithm).
One further hint: before processing a cardholder PAN, strip any whitespace and punctuation characters from the input. Why? Because it’s typically much easier to enter the digits in groups, similar to how they’re displayed on the front of an actual credit card, i.e.
4444 4444 4444 4444
is much easier to enter correctly than
4444444444444444
There’s really no benefit in chastising the user because they’ve entered characters you don't expect here.
This also implies making sure that your entry fields have room for at least 24 characters, otherwise users who enter spaces will run out of room. I’d recommend that you make the field wide enough to display 32 characters and allow up to 64; that gives plenty of headroom for expansion.
Here's an image that gives a little more insight:
UPDATE (2016): Mastercard is to implement new BIN ranges starting Ach Payment.
In javascript:
function detectCardType(number) {
var re = {
electron: /^(4026|417500|4405|4508|4844|4913|4917)\d+$/,
maestro: /^(5018|5020|5038|5612|5893|6304|6759|6761|6762|6763|0604|6390)\d+$/,
dankort: /^(5019)\d+$/,
interpayment: /^(636)\d+$/,
unionpay: /^(62|88)\d+$/,
visa: /^4[0-9]{12}(?:[0-9]{3})?$/,
mastercard: /^5[1-5][0-9]{14}$/,
amex: /^3[47][0-9]{13}$/,
diners: /^3(?:0[0-5]|[68][0-9])[0-9]{11}$/,
discover: /^6(?:011|5[0-9]{2})[0-9]{12}$/,
jcb: /^(?:2131|1800|35\d{3})\d{11}$/
}
for(var key in re) {
if(re[key].test(number)) {
return key
}
}
}
Unit test:
describe('CreditCard', function() {
describe('#detectCardType', function() {
var cards = {
'8800000000000000': 'UNIONPAY',
'4026000000000000': 'ELECTRON',
'4175000000000000': 'ELECTRON',
'4405000000000000': 'ELECTRON',
'4508000000000000': 'ELECTRON',
'4844000000000000': 'ELECTRON',
'4913000000000000': 'ELECTRON',
'4917000000000000': 'ELECTRON',
'5019000000000000': 'DANKORT',
'5018000000000000': 'MAESTRO',
'5020000000000000': 'MAESTRO',
'5038000000000000': 'MAESTRO',
'5612000000000000': 'MAESTRO',
'5893000000000000': 'MAESTRO',
'6304000000000000': 'MAESTRO',
'6759000000000000': 'MAESTRO',
'6761000000000000': 'MAESTRO',
'6762000000000000': 'MAESTRO',
'6763000000000000': 'MAESTRO',
'0604000000000000': 'MAESTRO',
'6390000000000000': 'MAESTRO',
'3528000000000000': 'JCB',
'3589000000000000': 'JCB',
'3529000000000000': 'JCB',
'6360000000000000': 'INTERPAYMENT',
'4916338506082832': 'VISA',
'4556015886206505': 'VISA',
'4539048040151731': 'VISA',
'4024007198964305': 'VISA',
'4716175187624512': 'VISA',
'5280934283171080': 'MASTERCARD',
'5456060454627409': 'MASTERCARD',
'5331113404316994': 'MASTERCARD',
'5259474113320034': 'MASTERCARD',
'5442179619690834': 'MASTERCARD',
'6011894492395579': 'DISCOVER',
'6011388644154687': 'DISCOVER',
'6011880085013612': 'DISCOVER',
'6011652795433988': 'DISCOVER',
'6011375973328347': 'DISCOVER',
'345936346788903': 'AMEX',
'377669501013152': 'AMEX',
'373083634595479': 'AMEX',
'370710819865268': 'AMEX',
'371095063560404': 'AMEX'
};
Object.keys(cards).forEach(function(number) {
it('should detect card ' + number + ' as ' + cards[number], function() {
Basket.detectCardType(number).should.equal(cards[number]);
});
});
});
});
Updated: 15th June 2016 (as an ultimate solution currently)
Please note that I even give vote up for the one is top voted, but to make it clear these are the regexps actually works i tested it with thousands of real BIN codes. The most important is to use start strings (^) otherwise it will give false results in real world!
JCB ^(?:2131|1800|35)[0-9]{0,}$ Start with: 2131, 1800, 35 (3528-3589)
American Express ^3[47][0-9]{0,}$ Start with: 34, 37
Diners Club ^3(?:0[0-59]{1}|[689])[0-9]{0,}$ Start with: 300-305, 309, 36, 38-39
Visa ^4[0-9]{0,}$ Start with: 4
MasterCard ^(5[1-5]|222[1-9]|22[3-9]|2[3-6]|27[01]|2720)[0-9]{0,}$ Start with: 2221-2720, 51-55
Maestro ^(5[06789]|6)[0-9]{0,}$ Maestro always growing in the range: 60-69, started with / not something else, but starting 5 must be encoded as mastercard anyway. Maestro cards must be detected in the end of the code because some others has in the range of 60-69. Please look at the code.
Discover ^(6011|65|64[4-9]|62212[6-9]|6221[3-9]|622[2-8]|6229[01]|62292[0-5])[0-9]{0,}$ Discover quite difficult to code, start with: 6011, 622126-622925, 644-649, 65
In javascript I use this function. This is good when u assign it to an onkeyup event and it give result as soon as possible.
function cc_brand_id(cur_val) {
// the regular expressions check for possible matches as you type, hence the OR operators based on the number of chars
// regexp string length {0} provided for soonest detection of beginning of the card numbers this way it could be used for BIN CODE detection also
//JCB
jcb_regex = new RegExp('^(?:2131|1800|35)[0-9]{0,}$'); //2131, 1800, 35 (3528-3589)
// American Express
amex_regex = new RegExp('^3[47][0-9]{0,}$'); //34, 37
// Diners Club
diners_regex = new RegExp('^3(?:0[0-59]{1}|[689])[0-9]{0,}$'); //300-305, 309, 36, 38-39
// Visa
visa_regex = new RegExp('^4[0-9]{0,}$'); //4
// MasterCard
mastercard_regex = new RegExp('^(5[1-5]|222[1-9]|22[3-9]|2[3-6]|27[01]|2720)[0-9]{0,}$'); //2221-2720, 51-55
maestro_regex = new RegExp('^(5[06789]|6)[0-9]{0,}$'); //always growing in the range: 60-69, started with / not something else, but starting 5 must be encoded as mastercard anyway
//Discover
discover_regex = new RegExp('^(6011|65|64[4-9]|62212[6-9]|6221[3-9]|622[2-8]|6229[01]|62292[0-5])[0-9]{0,}$');
////6011, 622126-622925, 644-649, 65
// get rid of anything but numbers
cur_val = cur_val.replace(/\D/g, '');
// checks per each, as their could be multiple hits
//fix: ordering matter in detection, otherwise can give false results in rare cases
var sel_brand = "unknown";
if (cur_val.match(jcb_regex)) {
sel_brand = "jcb";
} else if (cur_val.match(amex_regex)) {
sel_brand = "amex";
} else if (cur_val.match(diners_regex)) {
sel_brand = "diners_club";
} else if (cur_val.match(visa_regex)) {
sel_brand = "visa";
} else if (cur_val.match(mastercard_regex)) {
sel_brand = "mastercard";
} else if (cur_val.match(discover_regex)) {
sel_brand = "discover";
} else if (cur_val.match(maestro_regex)) {
if (cur_val[0] == '5') { //started 5 must be mastercard
sel_brand = "mastercard";
} else {
sel_brand = "maestro"; //maestro is all 60-69 which is not something else, thats why this condition in the end
}
}
return sel_brand;
}
Here you can play with it:
http://jsfiddle.net/upN3L/69/
For PHP use this function, this detects some sub VISA/MC cards too:
/**
* Obtain a brand constant from a PAN
*
* #param string $pan Credit card number
* #param bool $include_sub_types Include detection of sub visa brands
* #return string
*/
public static function getCardBrand($pan, $include_sub_types = false)
{
//maximum length is not fixed now, there are growing number of CCs has more numbers in length, limiting can give false negatives atm
//these regexps accept not whole cc numbers too
//visa
$visa_regex = "/^4[0-9]{0,}$/";
$vpreca_regex = "/^428485[0-9]{0,}$/";
$postepay_regex = "/^(402360|402361|403035|417631|529948){0,}$/";
$cartasi_regex = "/^(432917|432930|453998)[0-9]{0,}$/";
$entropay_regex = "/^(406742|410162|431380|459061|533844|522093)[0-9]{0,}$/";
$o2money_regex = "/^(422793|475743)[0-9]{0,}$/";
// MasterCard
$mastercard_regex = "/^(5[1-5]|222[1-9]|22[3-9]|2[3-6]|27[01]|2720)[0-9]{0,}$/";
$maestro_regex = "/^(5[06789]|6)[0-9]{0,}$/";
$kukuruza_regex = "/^525477[0-9]{0,}$/";
$yunacard_regex = "/^541275[0-9]{0,}$/";
// American Express
$amex_regex = "/^3[47][0-9]{0,}$/";
// Diners Club
$diners_regex = "/^3(?:0[0-59]{1}|[689])[0-9]{0,}$/";
//Discover
$discover_regex = "/^(6011|65|64[4-9]|62212[6-9]|6221[3-9]|622[2-8]|6229[01]|62292[0-5])[0-9]{0,}$/";
//JCB
$jcb_regex = "/^(?:2131|1800|35)[0-9]{0,}$/";
//ordering matter in detection, otherwise can give false results in rare cases
if (preg_match($jcb_regex, $pan)) {
return "jcb";
}
if (preg_match($amex_regex, $pan)) {
return "amex";
}
if (preg_match($diners_regex, $pan)) {
return "diners_club";
}
//sub visa/mastercard cards
if ($include_sub_types) {
if (preg_match($vpreca_regex, $pan)) {
return "v-preca";
}
if (preg_match($postepay_regex, $pan)) {
return "postepay";
}
if (preg_match($cartasi_regex, $pan)) {
return "cartasi";
}
if (preg_match($entropay_regex, $pan)) {
return "entropay";
}
if (preg_match($o2money_regex, $pan)) {
return "o2money";
}
if (preg_match($kukuruza_regex, $pan)) {
return "kukuruza";
}
if (preg_match($yunacard_regex, $pan)) {
return "yunacard";
}
}
if (preg_match($visa_regex, $pan)) {
return "visa";
}
if (preg_match($mastercard_regex, $pan)) {
return "mastercard";
}
if (preg_match($discover_regex, $pan)) {
return "discover";
}
if (preg_match($maestro_regex, $pan)) {
if ($pan[0] == '5') { //started 5 must be mastercard
return "mastercard";
}
return "maestro"; //maestro is all 60-69 which is not something else, thats why this condition in the end
}
return "unknown"; //unknown for this system
}
public string GetCreditCardType(string CreditCardNumber)
{
Regex regVisa = new Regex("^4[0-9]{12}(?:[0-9]{3})?$");
Regex regMaster = new Regex("^5[1-5][0-9]{14}$");
Regex regExpress = new Regex("^3[47][0-9]{13}$");
Regex regDiners = new Regex("^3(?:0[0-5]|[68][0-9])[0-9]{11}$");
Regex regDiscover = new Regex("^6(?:011|5[0-9]{2})[0-9]{12}$");
Regex regJCB = new Regex("^(?:2131|1800|35\\d{3})\\d{11}$");
if (regVisa.IsMatch(CreditCardNumber))
return "VISA";
else if (regMaster.IsMatch(CreditCardNumber))
return "MASTER";
else if (regExpress.IsMatch(CreditCardNumber))
return "AEXPRESS";
else if (regDiners.IsMatch(CreditCardNumber))
return "DINERS";
else if (regDiscover.IsMatch(CreditCardNumber))
return "DISCOVERS";
else if (regJCB.IsMatch(CreditCardNumber))
return "JCB";
else
return "invalid";
}
Here is the function to check Credit card type using Regex , c#
Check this out:
http://www.breakingpar.com/bkp/home.nsf/0/87256B280015193F87256CC70060A01B
function isValidCreditCard(type, ccnum) {
/* Visa: length 16, prefix 4, dashes optional.
Mastercard: length 16, prefix 51-55, dashes optional.
Discover: length 16, prefix 6011, dashes optional.
American Express: length 15, prefix 34 or 37.
Diners: length 14, prefix 30, 36, or 38. */
var re = new Regex({
"visa": "/^4\d{3}-?\d{4}-?\d{4}-?\d",
"mc": "/^5[1-5]\d{2}-?\d{4}-?\d{4}-?\d{4}$/",
"disc": "/^6011-?\d{4}-?\d{4}-?\d{4}$/",
"amex": "/^3[47]\d{13}$/",
"diners": "/^3[068]\d{12}$/"
}[type.toLowerCase()])
if (!re.test(ccnum)) return false;
// Remove all dashes for the checksum checks to eliminate negative numbers
ccnum = ccnum.split("-").join("");
// Checksum ("Mod 10")
// Add even digits in even length strings or odd digits in odd length strings.
var checksum = 0;
for (var i = (2 - (ccnum.length % 2)); i <= ccnum.length; i += 2) {
checksum += parseInt(ccnum.charAt(i - 1));
}
// Analyze odd digits in even length strings or even digits in odd length strings.
for (var i = (ccnum.length % 2) + 1; i < ccnum.length; i += 2) {
var digit = parseInt(ccnum.charAt(i - 1)) * 2;
if (digit < 10) { checksum += digit; } else { checksum += (digit - 9); }
}
if ((checksum % 10) == 0) return true;
else return false;
}
recently I needed such functionality, I was porting Zend Framework Credit Card Validator to ruby.
ruby gem: https://github.com/Fivell/credit_card_validations
zend framework: https://github.com/zendframework/zf2/blob/master/library/Zend/Validator/CreditCard.php
They both use INN ranges for detecting type. Here you can read about INN
According to this you can detect credit card alternatively (without regexps,but declaring some rules about prefixes and possible length)
So we have next rules for most used cards
######## most used brands #########
visa: [
{length: [13, 16], prefixes: ['4']}
],
mastercard: [
{length: [16], prefixes: ['51', '52', '53', '54', '55']}
],
amex: [
{length: [15], prefixes: ['34', '37']}
],
######## other brands ########
diners: [
{length: [14], prefixes: ['300', '301', '302', '303', '304', '305', '36', '38']},
],
#There are Diners Club (North America) cards that begin with 5. These are a joint venture between Diners Club and MasterCard, and are processed like a MasterCard
# will be removed in next major version
diners_us: [
{length: [16], prefixes: ['54', '55']}
],
discover: [
{length: [16], prefixes: ['6011', '644', '645', '646', '647', '648',
'649', '65']}
],
jcb: [
{length: [16], prefixes: ['3528', '3529', '353', '354', '355', '356', '357', '358', '1800', '2131']}
],
laser: [
{length: [16, 17, 18, 19], prefixes: ['6304', '6706', '6771']}
],
solo: [
{length: [16, 18, 19], prefixes: ['6334', '6767']}
],
switch: [
{length: [16, 18, 19], prefixes: ['633110', '633312', '633304', '633303', '633301', '633300']}
],
maestro: [
{length: [12, 13, 14, 15, 16, 17, 18, 19], prefixes: ['5010', '5011', '5012', '5013', '5014', '5015', '5016', '5017', '5018',
'502', '503', '504', '505', '506', '507', '508',
'6012', '6013', '6014', '6015', '6016', '6017', '6018', '6019',
'602', '603', '604', '605', '6060',
'677', '675', '674', '673', '672', '671', '670',
'6760', '6761', '6762', '6763', '6764', '6765', '6766', '6768', '6769']}
],
# Luhn validation are skipped for union pay cards because they have unknown generation algoritm
unionpay: [
{length: [16, 17, 18, 19], prefixes: ['622', '624', '625', '626', '628'], skip_luhn: true}
],
dankrot: [
{length: [16], prefixes: ['5019']}
],
rupay: [
{length: [16], prefixes: ['6061', '6062', '6063', '6064', '6065', '6066', '6067', '6068', '6069', '607', '608'], skip_luhn: true}
]
}
Then by searching prefix and comparing length you can detect credit card brand. Also don't forget about luhn algoritm (it is descibed here http://en.wikipedia.org/wiki/Luhn).
UPDATE
updated list of rules can be found here https://raw.githubusercontent.com/Fivell/credit_card_validations/master/lib/data/brands.yaml
Here's Complete C# or VB code for all kinds of CC related things on codeproject.
IsValidNumber
GetCardTypeFromNumber
GetCardTestNumber
PassesLuhnTest
This article has been up for a couple years with no negative comments.
Compact javascript version
var getCardType = function (number) {
var cards = {
visa: /^4[0-9]{12}(?:[0-9]{3})?$/,
mastercard: /^5[1-5][0-9]{14}$/,
amex: /^3[47][0-9]{13}$/,
diners: /^3(?:0[0-5]|[68][0-9])[0-9]{11}$/,
discover: /^6(?:011|5[0-9]{2})[0-9]{12}$/,
jcb: /^(?:2131|1800|35\d{3})\d{11}$/
};
for (var card in cards) {
if (cards[card].test(number)) {
return card;
}
}
};
Anatoliy's answer in PHP:
public static function detectCardType($num)
{
$re = array(
"visa" => "/^4[0-9]{12}(?:[0-9]{3})?$/",
"mastercard" => "/^5[1-5][0-9]{14}$/",
"amex" => "/^3[47][0-9]{13}$/",
"discover" => "/^6(?:011|5[0-9]{2})[0-9]{12}$/",
);
if (preg_match($re['visa'],$num))
{
return 'visa';
}
else if (preg_match($re['mastercard'],$num))
{
return 'mastercard';
}
else if (preg_match($re['amex'],$num))
{
return 'amex';
}
else if (preg_match($re['discover'],$num))
{
return 'discover';
}
else
{
return false;
}
}
Here is a php class function returns CCtype by CCnumber.
This code not validates the card or not runs Luhn algorithm only try to find credit card type based on table in this page. basicly uses CCnumber length and CCcard prefix to determine CCcard type.
<?php
class CreditcardType
{
public static $creditcardTypes = [
[
'Name' => 'American Express',
'cardLength' => [15],
'cardPrefix' => ['34', '37'],
], [
'Name' => 'Maestro',
'cardLength' => [12, 13, 14, 15, 16, 17, 18, 19],
'cardPrefix' => ['5018', '5020', '5038', '6304', '6759', '6761', '6763'],
], [
'Name' => 'Mastercard',
'cardLength' => [16],
'cardPrefix' => ['51', '52', '53', '54', '55'],
], [
'Name' => 'Visa',
'cardLength' => [13, 16],
'cardPrefix' => ['4'],
], [
'Name' => 'JCB',
'cardLength' => [16],
'cardPrefix' => ['3528', '3529', '353', '354', '355', '356', '357', '358'],
], [
'Name' => 'Discover',
'cardLength' => [16],
'cardPrefix' => ['6011', '622126', '622127', '622128', '622129', '62213','62214', '62215', '62216', '62217', '62218', '62219','6222', '6223', '6224', '6225', '6226', '6227', '6228','62290', '62291', '622920', '622921', '622922', '622923','622924', '622925', '644', '645', '646', '647', '648','649', '65'],
], [
'Name' => 'Solo',
'cardLength' => [16, 18, 19],
'cardPrefix' => ['6334', '6767'],
], [
'Name' => 'Unionpay',
'cardLength' => [16, 17, 18, 19],
'cardPrefix' => ['622126', '622127', '622128', '622129', '62213', '62214','62215', '62216', '62217', '62218', '62219', '6222', '6223','6224', '6225', '6226', '6227', '6228', '62290', '62291','622920', '622921', '622922', '622923', '622924', '622925'],
], [
'Name' => 'Diners Club',
'cardLength' => [14],
'cardPrefix' => ['300', '301', '302', '303', '304', '305', '36'],
], [
'Name' => 'Diners Club US',
'cardLength' => [16],
'cardPrefix' => ['54', '55'],
], [
'Name' => 'Diners Club Carte Blanche',
'cardLength' => [14],
'cardPrefix' => ['300', '305'],
], [
'Name' => 'Laser',
'cardLength' => [16, 17, 18, 19],
'cardPrefix' => ['6304', '6706', '6771', '6709'],
],
];
public static function getType($CCNumber)
{
$CCNumber = trim($CCNumber);
$type = 'Unknown';
foreach (CreditcardType::$creditcardTypes as $card) {
if (! in_array(strlen($CCNumber), $card['cardLength'])) {
continue;
}
$prefixes = '/^(' . implode('|', $card['cardPrefix']) . ')/';
if (preg_match($prefixes, $CCNumber) == 1) {
$type = $card['Name'];
break;
}
}
return $type;
}
}
The first numbers of the credit card can be used to approximate the vendor:
Visa: 49,44 or 47
Visa electron: 42, 45, 48, 49
MasterCard: 51
Amex:34
Diners: 30, 36, 38
JCB: 35
In Card Range Recognition (CRR), a drawback with algorithms that use a series of regex or other hard-coded ranges, is that the BINs/IINs do change over time in my experience. The co-branding of cards is an ongoing complication. Different Card Acquirers / merchants may need you treat the same card differently, depending on e.g. geolocation.
Additionally, in the last few years with e.g. UnionPay cards in wider circulation, existing models do not cope with new ranges that sometimes interleave with broader ranges that they supersede.
Knowing the geography your system needs to cover may help, as some ranges are restricted to use in particular countries. For example, ranges 62 include some AAA sub-ranges in the US, but if your merchant base is outside the US, you may be able to treat all 62 as UnionPay.
You may be also asked to treat a card differently based on merchant location. E.g. to treat certain UK cards as debit domestically, but as credit internationally.
There are very useful set of rules maintained by one major Acquiring Bank. E.g. https://www.barclaycard.co.uk/business/files/BIN-Rules-EIRE.pdf and https://www.barclaycard.co.uk/business/files/BIN-Rules-UK.pdf. (Valid links as of June 2017, thanks to the user who provided a link to updated reference.) But be aware of the caveat that, while these CRR rules may represent the Card Issuing universe as it applies to the merchants acquired by that entity, it does not include e.g. ranges identified as CUP/UPI.
These comments apply to magnetic stripe (MagStripe) or PKE (Pan Key Entry) scenarios. The situation is different again in the ICC/EMV world.
Update: Other answers on this page (and also the linked WikiPedia page) have JCB as always 16 long. However, in my company we have a dedicated team of engineers who certify our POS devices and software across multiple acquiring banks and geographies. The most recent Certification Pack of cards this team have from JCB, had a pass case for a 19 long PAN.
Do not try to detect credit card type as part of processing a payment. You are risking of declining valid transactions.
If you need to provide information to your payment processor (e.g. PayPal credit card object requires to name the card type), then guess it from the least information available, e.g.
$credit_card['pan'] = preg_replace('/[^0-9]/', '', $credit_card['pan']);
$inn = (int) mb_substr($credit_card['pan'], 0, 2);
// #see http://en.wikipedia.org/wiki/List_of_Bank_Identification_Numbers#Overview
if ($inn >= 40 && $inn <= 49) {
$type = 'visa';
} else if ($inn >= 51 && $inn <= 55) {
$type = 'mastercard';
} else if ($inn >= 60 && $inn <= 65) {
$type = 'discover';
} else if ($inn >= 34 && $inn <= 37) {
$type = 'amex';
} else {
throw new \UnexpectedValueException('Unsupported card type.');
}
This implementation (using only the first two digits) is enough to identify all of the major (and in PayPal's case all of the supported) card schemes. In fact, you might want to skip the exception altogether and default to the most popular card type. Let the payment gateway/processor tell you if there is a validation error in response to your request.
The reality is that your payment gateway does not care about the value you provide.
Swift 2.1 Version of Usman Y's answer.
Use a print statement to verify so call by some string value
print(self.validateCardType(self.creditCardField.text!))
func validateCardType(testCard: String) -> String {
let regVisa = "^4[0-9]{12}(?:[0-9]{3})?$"
let regMaster = "^5[1-5][0-9]{14}$"
let regExpress = "^3[47][0-9]{13}$"
let regDiners = "^3(?:0[0-5]|[68][0-9])[0-9]{11}$"
let regDiscover = "^6(?:011|5[0-9]{2})[0-9]{12}$"
let regJCB = "^(?:2131|1800|35\\d{3})\\d{11}$"
let regVisaTest = NSPredicate(format: "SELF MATCHES %#", regVisa)
let regMasterTest = NSPredicate(format: "SELF MATCHES %#", regMaster)
let regExpressTest = NSPredicate(format: "SELF MATCHES %#", regExpress)
let regDinersTest = NSPredicate(format: "SELF MATCHES %#", regDiners)
let regDiscoverTest = NSPredicate(format: "SELF MATCHES %#", regDiscover)
let regJCBTest = NSPredicate(format: "SELF MATCHES %#", regJCB)
if regVisaTest.evaluateWithObject(testCard){
return "Visa"
}
else if regMasterTest.evaluateWithObject(testCard){
return "MasterCard"
}
else if regExpressTest.evaluateWithObject(testCard){
return "American Express"
}
else if regDinersTest.evaluateWithObject(testCard){
return "Diners Club"
}
else if regDiscoverTest.evaluateWithObject(testCard){
return "Discover"
}
else if regJCBTest.evaluateWithObject(testCard){
return "JCB"
}
return ""
}
Stripe has provided this fantastic javascript library for card scheme detection. Let me add few code snippets and show you how to use it.
Firstly Include it to your web page as
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/jquery.payment/1.2.3/jquery.payment.js " ></script>
Secondly use the function cardType for detecting the card scheme.
$(document).ready(function() {
var type = $.payment.cardType("4242 4242 4242 4242"); //test card number
console.log(type);
});
Here are the reference links for more examples and demos.
Stripe blog for jquery.payment.js
Github repository
In swift you can create an enum to detect the credit card type.
enum CreditCardType: Int { // Enum which encapsulates different card types and method to find the type of card.
case Visa
case Master
case Amex
case Discover
func validationRegex() -> String {
var regex = ""
switch self {
case .Visa:
regex = "^4[0-9]{6,}$"
case .Master:
regex = "^5[1-5][0-9]{5,}$"
case .Amex:
regex = "^3[47][0-9]{13}$"
case .Discover:
regex = "^6(?:011|5[0-9]{2})[0-9]{12}$"
}
return regex
}
func validate(cardNumber: String) -> Bool {
let predicate = NSPredicate(format: "SELF MATCHES %#", validationRegex())
return predicate.evaluateWithObject(cardNumber)
}
// Method returns the credit card type for given card number
static func cardTypeForCreditCardNumber(cardNumber: String) -> CreditCardType? {
var creditCardType: CreditCardType?
var index = 0
while let cardType = CreditCardType(rawValue: index) {
if cardType.validate(cardNumber) {
creditCardType = cardType
break
} else {
index++
}
}
return creditCardType
}
}
Call the method CreditCardType.cardTypeForCreditCardNumber("#card number") which returns CreditCardType enum value.
My solution with jQuery:
function detectCreditCardType() {
var type = new Array;
type[1] = '^4[0-9]{12}(?:[0-9]{3})?$'; // visa
type[2] = '^5[1-5][0-9]{14}$'; // mastercard
type[3] = '^6(?:011|5[0-9]{2})[0-9]{12}$'; // discover
type[4] = '^3[47][0-9]{13}$'; // amex
var ccnum = $('.creditcard').val().replace(/[^\d.]/g, '');
var returntype = 0;
$.each(type, function(idx, re) {
var regex = new RegExp(re);
if(regex.test(ccnum) && idx>0) {
returntype = idx;
}
});
return returntype;
}
In case 0 is returned, credit card type is undetected.
"creditcard" class should be added to the credit card input field.
I searched around quite a bit for credit card formatting and phone number formatting. Found lots of good tips but nothing really suited my exact desires so I created this bit of code. You use it like this:
var sf = smartForm.formatCC(myInputString);
var cardType = sf.cardType;
A javascript improve of #Anatoliy answer
function getCardType (number) {
const numberFormated = number.replace(/\D/g, '')
var patterns = {
VISA: /^4[0-9]{12}(?:[0-9]{3})?$/,
MASTER: /^5[1-5][0-9]{14}$/,
AMEX: /^3[47][0-9]{13}$/,
ELO: /^((((636368)|(438935)|(504175)|(451416)|(636297))\d{0,10})|((5067)|(4576)|(4011))\d{0,12})$/,
AURA: /^(5078\d{2})(\d{2})(\d{11})$/,
JCB: /^(?:2131|1800|35\d{3})\d{11}$/,
DINERS: /^3(?:0[0-5]|[68][0-9])[0-9]{11}$/,
DISCOVERY: /^6(?:011|5[0-9]{2})[0-9]{12}$/,
HIPERCARD: /^(606282\d{10}(\d{3})?)|(3841\d{15})$/,
ELECTRON: /^(4026|417500|4405|4508|4844|4913|4917)\d+$/,
MAESTRO: /^(5018|5020|5038|5612|5893|6304|6759|6761|6762|6763|0604|6390)\d+$/,
DANKORT: /^(5019)\d+$/,
INTERPAYMENT: /^(636)\d+$/,
UNIONPAY: /^(62|88)\d+$/,
}
for (var key in patterns) {
if (patterns[key].test(numberFormated)) {
return key
}
}
}
console.log(getCardType("4539 5684 7526 2091"))
Swift 5+
extension String {
func isMatch(_ Regex: String) -> Bool {
do {
let regex = try NSRegularExpression(pattern: Regex)
let results = regex.matches(in: self, range: NSRange(self.startIndex..., in: self))
return results.map {
String(self[Range($0.range, in: self)!])
}.count > 0
} catch {
return false
}
}
func getCreditCardType() -> String? {
let VISA_Regex = "^4[0-9]{6,}$"
let MasterCard_Regex = "^5[1-5][0-9]{5,}|222[1-9][0-9]{3,}|22[3-9][0-9]{4,}|2[3-6][0-9]{5,}|27[01][0-9]{4,}|2720[0-9]{3,}$"
let AmericanExpress_Regex = "^3[47][0-9]{5,}$"
let DinersClub_Regex = "^3(?:0[0-5]|[68][0-9])[0-9]{4,}$"
let Discover_Regex = "^6(?:011|5[0-9]{2})[0-9]{3,}$"
let JCB_Regex = "^(?:2131|1800|35[0-9]{3})[0-9]{3,}$"
if self.isMatch(VISA_Regex) {
return "VISA"
} else if self.isMatch(MasterCard_Regex) {
return "MasterCard"
} else if self.isMatch(AmericanExpress_Regex) {
return "AmericanExpress"
} else if self.isMatch(DinersClub_Regex) {
return "DinersClub"
} else if self.isMatch(Discover_Regex) {
return "Discover"
} else if self.isMatch(JCB_Regex) {
return "JCB"
} else {
return nil
}
}
}
Use.
"1234123412341234".getCreditCardType()
// abobjects.com, parvez ahmad ab bulk mailer
use below script
function isValidCreditCard2(type, ccnum) {
if (type == "Visa") {
// Visa: length 16, prefix 4, dashes optional.
var re = /^4\d{3}?\d{4}?\d{4}?\d{4}$/;
} else if (type == "MasterCard") {
// Mastercard: length 16, prefix 51-55, dashes optional.
var re = /^5[1-5]\d{2}?\d{4}?\d{4}?\d{4}$/;
} else if (type == "Discover") {
// Discover: length 16, prefix 6011, dashes optional.
var re = /^6011?\d{4}?\d{4}?\d{4}$/;
} else if (type == "AmEx") {
// American Express: length 15, prefix 34 or 37.
var re = /^3[4,7]\d{13}$/;
} else if (type == "Diners") {
// Diners: length 14, prefix 30, 36, or 38.
var re = /^3[0,6,8]\d{12}$/;
}
if (!re.test(ccnum)) return false;
return true;
/*
// Remove all dashes for the checksum checks to eliminate negative numbers
ccnum = ccnum.split("-").join("");
// Checksum ("Mod 10")
// Add even digits in even length strings or odd digits in odd length strings.
var checksum = 0;
for (var i=(2-(ccnum.length % 2)); i<=ccnum.length; i+=2) {
checksum += parseInt(ccnum.charAt(i-1));
}
// Analyze odd digits in even length strings or even digits in odd length strings.
for (var i=(ccnum.length % 2) + 1; i<ccnum.length; i+=2) {
var digit = parseInt(ccnum.charAt(i-1)) * 2;
if (digit < 10) { checksum += digit; } else { checksum += (digit-9); }
}
if ((checksum % 10) == 0) return true; else return false;
*/
}
jQuery.validator.addMethod("isValidCreditCard", function(postalcode, element) {
return isValidCreditCard2($("#cardType").val(), $("#cardNum").val());
}, "<br>credit card is invalid");
Type</td>
<td class="text"> <form:select path="cardType" cssclass="fields" style="border: 1px solid #D5D5D5;padding: 0px 0px 0px 0px;width: 130px;height: 22px;">
<option value="SELECT">SELECT</option>
<option value="MasterCard">Mastercard</option>
<option value="Visa">Visa</option>
<option value="AmEx">American Express</option>
<option value="Discover">Discover</option>
</form:select> <font color="#FF0000">*</font>
$("#signupForm").validate({
rules:{
companyName:{required: true},
address1:{required: true},
city:{required: true},
state:{required: true},
zip:{required: true},
country:{required: true},
chkAgree:{required: true},
confPassword:{required: true},
lastName:{required: true},
firstName:{required: true},
ccAddress1:{required: true},
ccZip:{
postalcode : true
},
phone:{required: true},
email:{
required: true,
email: true
},
userName:{
required: true,
minlength: 6
},
password:{
required: true,
minlength: 6
},
cardNum:{
isValidCreditCard : true
},
Just a little spoon feeding:
$("#CreditCardNumber").focusout(function () {
var regVisa = /^4[0-9]{12}(?:[0-9]{3})?$/;
var regMasterCard = /^5[1-5][0-9]{14}$/;
var regAmex = /^3[47][0-9]{13}$/;
var regDiscover = /^6(?:011|5[0-9]{2})[0-9]{12}$/;
if (regVisa.test($(this).val())) {
$("#CCImage").html("<img height='40px' src='#Url.Content("~/images/visa.png")'>");
}
else if (regMasterCard.test($(this).val())) {
$("#CCImage").html("<img height='40px' src='#Url.Content("~/images/mastercard.png")'>");
}
else if (regAmex.test($(this).val())) {
$("#CCImage").html("<img height='40px' src='#Url.Content("~/images/amex.png")'>");
}
else if (regDiscover.test($(this).val())) {
$("#CCImage").html("<img height='40px' src='#Url.Content("~/images/discover.png")'>");
}
else {
$("#CCImage").html("NA");
}
});
Here is an example of some boolean functions written in Python that return True if the card is detected as per the function name.
def is_american_express(cc_number):
"""Checks if the card is an american express. If us billing address country code, & is_amex, use vpos
https://en.wikipedia.org/wiki/Bank_card_number#cite_note-GenCardFeatures-3
:param cc_number: unicode card number
"""
return bool(re.match(r'^3[47][0-9]{13}$', cc_number))
def is_visa(cc_number):
"""Checks if the card is a visa, begins with 4 and 12 or 15 additional digits.
:param cc_number: unicode card number
"""
# Standard Visa is 13 or 16, debit can be 19
if bool(re.match(r'^4', cc_number)) and len(cc_number) in [13, 16, 19]:
return True
return False
def is_mastercard(cc_number):
"""Checks if the card is a mastercard. Begins with 51-55 or 2221-2720 and 16 in length.
:param cc_number: unicode card number
"""
if len(cc_number) == 16 and cc_number.isdigit(): # Check digit, before cast to int
return bool(re.match(r'^5[1-5]', cc_number)) or int(cc_number[:4]) in range(2221, 2721)
return False
def is_discover(cc_number):
"""Checks if the card is discover, re would be too hard to maintain. Not a supported card.
:param cc_number: unicode card number
"""
if len(cc_number) == 16:
try:
# return bool(cc_number[:4] == '6011' or cc_number[:2] == '65' or cc_number[:6] in range(622126, 622926))
return bool(cc_number[:4] == '6011' or cc_number[:2] == '65' or 622126 <= int(cc_number[:6]) <= 622925)
except ValueError:
return False
return False
def is_jcb(cc_number):
"""Checks if the card is a jcb. Not a supported card.
:param cc_number: unicode card number
"""
# return bool(re.match(r'^(?:2131|1800|35\d{3})\d{11}$', cc_number)) # wikipedia
return bool(re.match(r'^35(2[89]|[3-8][0-9])[0-9]{12}$', cc_number)) # PawelDecowski
def is_diners_club(cc_number):
"""Checks if the card is a diners club. Not a supported card.
:param cc_number: unicode card number
"""
return bool(re.match(r'^3(?:0[0-6]|[68][0-9])[0-9]{11}$', cc_number)) # 0-5 = carte blance, 6 = international
def is_laser(cc_number):
"""Checks if the card is laser. Not a supported card.
:param cc_number: unicode card number
"""
return bool(re.match(r'^(6304|670[69]|6771)', cc_number))
def is_maestro(cc_number):
"""Checks if the card is maestro. Not a supported card.
:param cc_number: unicode card number
"""
possible_lengths = [12, 13, 14, 15, 16, 17, 18, 19]
return bool(re.match(r'^(50|5[6-9]|6[0-9])', cc_number)) and len(cc_number) in possible_lengths
# Child cards
def is_visa_electron(cc_number):
"""Child of visa. Checks if the card is a visa electron. Not a supported card.
:param cc_number: unicode card number
"""
return bool(re.match(r'^(4026|417500|4508|4844|491(3|7))', cc_number)) and len(cc_number) == 16
def is_total_rewards_visa(cc_number):
"""Child of visa. Checks if the card is a Total Rewards Visa. Not a supported card.
:param cc_number: unicode card number
"""
return bool(re.match(r'^41277777[0-9]{8}$', cc_number))
def is_diners_club_carte_blanche(cc_number):
"""Child card of diners. Checks if the card is a diners club carte blance. Not a supported card.
:param cc_number: unicode card number
"""
return bool(re.match(r'^30[0-5][0-9]{11}$', cc_number)) # github PawelDecowski, jquery-creditcardvalidator
def is_diners_club_carte_international(cc_number):
"""Child card of diners. Checks if the card is a diners club international. Not a supported card.
:param cc_number: unicode card number
"""
return bool(re.match(r'^36[0-9]{12}$', cc_number)) # jquery-creditcardvalidator
The first six digits of a card number (including the initial MII
digit) are known as the issuer identification number (IIN). These
identify the card issuing institution that issued the card to the card
holder. The rest of the number is allocated by the card issuer. The
card number's length is its number of digits. Many card issuers print
the entire IIN and account number on their card.
Based on the above facts I would like to keep a snippet of JAVA code to identify card brand.
Sample card types
public static final String AMERICAN_EXPRESS = "American Express";
public static final String DISCOVER = "Discover";
public static final String JCB = "JCB";
public static final String DINERS_CLUB = "Diners Club";
public static final String VISA = "Visa";
public static final String MASTERCARD = "MasterCard";
public static final String UNKNOWN = "Unknown";
Card Prefixes
// Based on http://en.wikipedia.org/wiki/Bank_card_number#Issuer_identification_number_.28IIN.29
public static final String[] PREFIXES_AMERICAN_EXPRESS = {"34", "37"};
public static final String[] PREFIXES_DISCOVER = {"60", "62", "64", "65"};
public static final String[] PREFIXES_JCB = {"35"};
public static final String[] PREFIXES_DINERS_CLUB = {"300", "301", "302", "303", "304", "305", "309", "36", "38", "39"};
public static final String[] PREFIXES_VISA = {"4"};
public static final String[] PREFIXES_MASTERCARD = {
"2221", "2222", "2223", "2224", "2225", "2226", "2227", "2228", "2229",
"223", "224", "225", "226", "227", "228", "229",
"23", "24", "25", "26",
"270", "271", "2720",
"50", "51", "52", "53", "54", "55"
};
Check to see if the input number has any of the given prefixes.
public String getBrand(String number) {
String evaluatedType;
if (StripeTextUtils.hasAnyPrefix(number, PREFIXES_AMERICAN_EXPRESS)) {
evaluatedType = AMERICAN_EXPRESS;
} else if (StripeTextUtils.hasAnyPrefix(number, PREFIXES_DISCOVER)) {
evaluatedType = DISCOVER;
} else if (StripeTextUtils.hasAnyPrefix(number, PREFIXES_JCB)) {
evaluatedType = JCB;
} else if (StripeTextUtils.hasAnyPrefix(number, PREFIXES_DINERS_CLUB)) {
evaluatedType = DINERS_CLUB;
} else if (StripeTextUtils.hasAnyPrefix(number, PREFIXES_VISA)) {
evaluatedType = VISA;
} else if (StripeTextUtils.hasAnyPrefix(number, PREFIXES_MASTERCARD)) {
evaluatedType = MASTERCARD;
} else {
evaluatedType = UNKNOWN;
}
return evaluatedType;
}
Finally, The Utility method
/**
* Check to see if the input number has any of the given prefixes.
*
* #param number the number to test
* #param prefixes the prefixes to test against
* #return {#code true} if number begins with any of the input prefixes
*/
public static boolean hasAnyPrefix(String number, String... prefixes) {
if (number == null) {
return false;
}
for (String prefix : prefixes) {
if (number.startsWith(prefix)) {
return true;
}
}
return false;
}
Reference
Stripe Card Builder
Try this for kotlin. Add Regex and add to the when statement.
private fun getCardType(number: String): String {
val visa = Regex("^4[0-9]{12}(?:[0-9]{3})?$")
val mastercard = Regex("^5[1-5][0-9]{14}$")
val amx = Regex("^3[47][0-9]{13}$")
return when {
visa.matches(number) -> "Visa"
mastercard.matches(number) -> "Mastercard"
amx.matches(number) -> "American Express"
else -> "Unknown"
}
}
The regular expression rules that match the respective card vendors:
(4\d{12}(?:\d{3})?) for VISA.
(5[1-5]\d{14}) for MasterCard.
(3[47]\d{13}) for AMEX.
((?:5020|5038|6304|6579|6761)\d{12}(?:\d\d)?) for Maestro.
(3(?:0[0-5]|[68][0-9])[0-9]{11}) for Diners Club.
(6(?:011|5[0-9]{2})[0-9]{12}) for Discover.
(35[2-8][89]\d\d\d{10}) for JCB.
follow Luhn’s algorithm
private boolean validateCreditCardNumber(String str) {
int[] ints = new int[str.length()];
for (int i = 0; i < str.length(); i++) {
ints[i] = Integer.parseInt(str.substring(i, i + 1));
}
for (int i = ints.length - 2; i >= 0; i = i - 2) {
int j = ints[i];
j = j * 2;
if (j > 9) {
j = j % 10 + 1;
}
ints[i] = j;
}
int sum = 0;
for (int i = 0; i < ints.length; i++) {
sum += ints[i];
}
if (sum % 10 == 0) {
return true;
} else {
return false;
}
}
then call this method
Edittext mCreditCardNumberEt;
mCreditCardNumberEt.addTextChangedListener(new TextWatcher() {
#Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
}
#Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
int cardcount= s.toString().length();
if(cardcount>=16) {
boolean cardnumbervalid= validateCreditCardNumber(s.toString());
if(cardnumbervalid) {
cardvalidtesting.setText("Valid Card");
cardvalidtesting.setTextColor(ContextCompat.getColor(context,R.color.green));
}
else {
cardvalidtesting.setText("Invalid Card");
cardvalidtesting.setTextColor(ContextCompat.getColor(context,R.color.red));
}
}
else if(cardcount>0 &&cardcount<16) {
cardvalidtesting.setText("Invalid Card");
cardvalidtesting.setTextColor(ContextCompat.getColor(context,R.color.red));
}
else {
cardvalidtesting.setText("");
}
}
#Override
public void afterTextChanged(Editable s) {
}
});
Another api solution at rapidapi Bank Card Bin Num Check there are 250K+ issued card type.
Only one GET rest api request and get card issuer info like:
{ "bin_number": 535177, "bank": "Finansbank A.S.", "scheme": "MASTERCARD", "type": "Debit", "country": "Turkey" }
Related
I am working on a wordpress website of a restoraunt that has Gift Cards bought in form via PayPal API in PHP, I'm not the one that wrote code on rediricting URL but it's only calling API using data sent to it with $_SESSION variable.
class GCS_Card_Action extends \ElementorPro\Modules\Forms\Classes\Action_Base {
public function get_name() {
return "gift_card";
}
public function get_label() {
return __('Gift Card', 'gift-cards-spending');
}
public function run($record, $ajax_handler) {
#session_start();
$_SESSION["giftCardFormData"]=json_encode($_POST);
$form_data = json_decode($_SESSION['giftCardFormData'], true);
$currency_symbol = $form_data['form_fields']['currency'];
if ($currency_symbol == '€') {
$currency = 'EUR';
} elseif ($currency_symbol == '£') {
$currency = 'GBP';
} elseif ($currency_symbol == '$') {
$currency = 'USD';
}
$form_data['form_fields']['currency'] = $currency;
$amount = floatval($form_data['form_fields']['amount']);
$price = abs($amount);
if ($currency != "JPY") {
$price = number_format($price, 2, ".", "");
} else {
$price = number_format($price, 0, ".", "");
}
$paypal_request = [
"intent" => "sale",
"payer" => [
"payment_method" => "paypal"
],
"transactions" => [
[
"amount" => [
"total" => $price,
"currency" => $currency
],
"item_list" => [
"items" => [
[
"name" => $form_data['form_fields']['name'],
"price" => $price,
"currency" => $currency,
"quantity" => 1
]
]
]
]
],
"redirect_urls" => [
"return_url" => "X",
"cancel_url" => "X"
]
];
//$request = array_merge($paypal_request, $form_data);
$request = $paypal_request;
$_SESSION['paypal_request'] = json_encode($request);
$redirUrl="/ig-payment";
$ajax_handler->add_response_data('redirect_url', $redirUrl);
}
public function register_settings_section($widget) {
}
public function on_export($element) {
}
}
This code is ran after submitting the form, sends data to PayPal API that prompts user to login with account and process payment.
I get these three errors
{
"name": "VALIDATION_ERROR",
"message": "Invalid request - see details",
"debug_id": "14a52a47cf143",
"information_link": "https://developer.paypal.com/docs/api/payments/#errors",
"details": [
{
"field": "transactions[0].amount.total",
"location": "body",
"issue": "Currency amount must be non-negative number, contain exactly 2 decimal places separated by '.' (JPY contains 0 decimal places), optional thousands separator ',', limited to 7 digits before the decimal point and currency which is a valid ISO Currency Code"
},
{
"field": "transactions[0].item_list.items[0].price",
"location": "body",
"issue": "Currency amount must be non-negative number, contain exactly 2 decimal places separated by '.' (JPY contains 0 decimal places), optional thousands separator ',', limited to 7 digits before the decimal point and currency which is a valid ISO Currency Code"
},
{
"field": "transactions[0].amount.currency",
"location": "body",
"issue": "Required field missing"
}
]
}
As you can see transactions[0].amount.currency is there in a structure but it throws Required field missing error
And for transactions[0].amount.total and transactions[0].item_list.items[0].price i get
Currency amount must be non-negative number, contain exactly 2 decimal places separated by '.' (JPY contains 0 decimal places), optional thousands separator ',', limited to 7 digits before the decimal point and currency which is a valid ISO Currency Code
Even if i formatted price variable using
if ($currency != "JPY") {
$price = number_format($price, 2, ".", "");
} else {
$price = number_format($price, 0, ".", "");
}
If i try to echo $_SESSION['paypal_request'] out i get this output
{
"intent": "sale",
"payer": { "payment_method": "paypal" },
"transactions": [
{
"amount": { "total": "25.00", "currency": "GBP" },
"item_list": {
"items": [
{
"name": "EMAIL",
"price": "25.00",
"currency": "GBP",
"quantity": 1
}
]
}
}
],
"redirect_urls": {
"return_url": "X",
"cancel_url": "X"
}
}
I tried searching for this error and could only found examples of Vue and React, tried translating it to PHP but it just wont work.
It looks like the price is actually becoming an invalid number like zero for some reason. You can probably find PayPal's logs in the developer dashboard: https://developer.paypal.com/dashboard/dashboard/sandbox
You are using the old v1/payments API, which is deprecated. I would recommend changing it to use the current v2/checkout/orders API.
For payer approval, rather than redirecting away from your site and back you can pair that API with the JS SDK: https://developer.paypal.com/demo/checkout/#/pattern/server
Turns out that code on the receiving page was changed by someone to do everything with data and I didn't need to change symbols or anything, just send $_POST to that page, which was the state of code when I first started working on it.
But also, when someone edited that code, they made $_SESSION read the wrong variable and it only returned an empty array.
I'm trying to integrate with some Partner API.
They only accept json with float type for amount.
Example:
OK
{"amount":0.0000005}
Error
{"amount":"0.0000005"}
{"amount":5.0E-7}
If the value greater or equal 1, then it's OK scenario always. But in my case I have values > 0, and < 1.
Code Example:
$arr = ['amount' => 0.0000005];
$str = json_encode($arr);
echo $str;
Output:
{"amount":5.0e-7}
I want the output to look like this:
{"amount":0.0000005}
Is it possible in php? May be some hacks & tricks?
The cleanest I can think of is to traverse through the data, recursively substituting small numbers with a placeholder; then, after JSON encoding, replace the placeholders in the final JSON string with the number formatted how you want it.
The surprisingly difficult part is formatting the float itself; I found this existing question about how to do that with some working but not very elegant implementations. For brevity, I've left that part as a TODO below.
class JsonMangler
{
private const THRESHOLD = 0.0001;
private const PLACEHOLDER = '__PLACEHOLDER__';
private array $mangledData = [];
private array $substitutions = [];
private int $placeholderIncrement = 0;
public function __construct(array $realData) {
// Start the recursive function
$this->mangledData = $this->mangle($realData);
}
private function mangle(array $realData): array {
$mangledData = [];
foreach ( $realData as $key => $realValue ) {
if ( is_float($realValue) && $realValue < self::THRESHOLD) {
// Substitute small floats with a placeholder
$substituteValue = self::PLACEHOLDER . ($this->placeholderIncrement++);
$mangledData[$key] = $substituteValue;
// Placeholder will appear in quotes in the JSON, which we want to replace away
$this->substitutions["\"$substituteValue\""] = $this->formatFloat($realValue);
}
elseif ( is_array($realValue) ) {
// Recurse through the data
$mangledData[$key] = $this->mangle($realValue);
}
else {
// Retain everything else
$mangledData[$key] = $realValue;
}
}
return $mangledData;
}
/**
* Format a float into a string without any exponential notation
*/
private function formatFloat(float $value): string
{
// This is surprisingly hard to do; see https://stackoverflow.com/q/22274437/157957
return 'TODO';
}
public function getJson(int $jsonEncodeFlags = 0): string
{
$mangledJson = json_encode($this->mangledData, $jsonEncodeFlags);
return str_replace(array_keys($this->substitutions), array_values($this->substitutions), $mangledJson);
}
}
Using this implementation for formatFloat, the following test:
$example = [
'amount' => 1.5,
'small_amount' => 0.0001,
'tiny_amount' => 0.0000005,
'subobject' => [
'sub_value' => 42.5,
'tiny_sub_value' => 0.0000425,
'array' => [
1.23,
0.0000123
]
]
];
echo (new JsonMangler($example))->getJson(JSON_PRETTY_PRINT);
Results in the following output:
{
"amount": 1.5,
"small_amount": 0.0001,
"tiny_amount": 0.0000005,
"subobject": {
"sub_value": 42.5,
"tiny_sub_value": 0.0000425,
"array": [
1.23,
0.0000123
]
}
}
The only way to keep it the long way is to convert it to a string instead. But then it is not a number anymore!
$arr = ['amount' => number_format(0.0000005, 7)];
$str = json_encode($arr);
giving
{"amount":"0.0000005"}
Javascript itself would use the scientific notation:
j = {"amount":"0.0000005"};
parseFloat(j.amount);
5e-7
A hack would be to remove the quotes.
$quoteless = preg_replace('/:"(\d+.\d+)"/', ':$1', $str);
echo $quoteless;
will give
{"amount":0.0000005}
I different lists with measurements of the same dimension but a bit mixed units like
"1 m, 200 mm, 1 ft"
or maybe also
"1 °C, 273 K" and so on.
Now I want to sort them by absolute order
"200 mm, 1 ft, 1 m" and "273 K, 1 °C"
I am wondering if this a an already solved problem, as I do not want to reinvent the wheel. I am afraid, this might be some kind of "shopping for PHP extensions" questions, but I already found some helpful packages:
https://github.com/PhpUnitsOfMeasure/php-units-of-measure can do all kind of conversation between units of measure.
I already have created code to separate unit and number.
So what I am thinking, to "brute force" the unit to a certain dimension of those:
https://github.com/PhpUnitsOfMeasure/php-units-of-measure/tree/master/source/PhysicalQuantity
Next I could pick the first dimension and convert everything to the first "main" SI unit and sort it.
Right?
Generally, what you need to do is convert these units to some common measurement, but only for the purpose of sorting.
Use usort() and a custom callback function. In your callback, do the conversion for the purpose of comparison.
Be sure to keep the original unit when returning the result though, or rounding errors will creep in.
That is the solution I came up with, based on the suggestions
public function testCompareLength()
{
$this->assertLessThan(0, $this->objectDe->compareFunction('100 mm', '1 m'));
}
public function testCompareTemperature()
{
$this->assertLessThan(0, $this->objectDe->compareFunction('1 K', '0 °C'));
$this->assertGreaterThan(0, $this->objectDe->compareFunction('0 °C', '1 K'));
$this->assertEquals(0, $this->objectDe->compareFunction('-273 °C', '0 K'));
}
/**
* #param $numberString
*
* #return array
*/
public function parseNumber($numberString): array
{
$values = preg_split('/(?<=[0-9.,])(?=[^0-9,.]+)/i', $numberString);
$float = $values[0];
$unit = $values[1] ?? '';
$decPos = strpos($float, '.');
if ($decPos === false) {
$precision = 0;
} else {
$precision = strlen($float) - $decPos - 1;
}
return ['float' => $float, 'unit' => $unit, 'precision' => $precision];
}
private function heuristicMeasureFactory($measure)
{
$prioritizedDimensions = [
Temperature::class,
Length::class,
];
$unit = trim($measure['unit']);
foreach ($prioritizedDimensions as $class) {
foreach ($class::getUnitDefinitions() as $definition) {
if ($definition->getName() == $unit) {
return new $class($measure['float'], $unit);
}
}
}
// now process aliases
foreach ($prioritizedDimensions as $class) {
foreach ($class::getUnitDefinitions() as $definition) {
foreach ($definition->aliases as $alias) {
if ($alias == $unit) {
return new $class($measure['float'], $unit);
}
}
}
}
return null; // NaN
}
/**
* Sort apples and oranges -- kind of. Not.
*
* Compares two strings which represend a measurement of the same physical dimension
*/
public function compareFunction($a, $b)
{
$definitions = Temperature::getUnitDefinitions();
$aParsed = $this->parseNumber($a);
$aVal = $this->heuristicMeasureFactory($aParsed);
$bParsed = $this->parseNumber($b);
$bVal = $this->heuristicMeasureFactory($bParsed);
if ($aVal == null || $bVal == null) {
return strnatcmp($aVal, $bVal); // fallback to string comparision
}
return bccomp($aVal->subtract($bVal)->toNativeUnit(), 0, 36);
}
So recently I was working on fixing an older system which is using keys from an array to retrieve certain data.
This is the array with several brands of cars, the key is used to filter certain things so the URL would look like http://example.com/page?brands=1&foo=bar
public static $carBrandGroups = [
0 => 'Volvo',
1 => 'BMW',
2 => 'Renault',
3 => 'Tesla',
4 => 'Opel',
5 => 'Peugeot',
6 => 'Toyota',
7 => 'Mercedes',
8 => 'Honda',
9 => 'Fiat',
]
Now the system works and retrieves everything except when 0 is passed which is logical since 0 is considered empty. I believe there must be a way, is there?
The function which passes additional information is shown below:
public static function getCarBrandGroup($modelNumber)
{
if ($modelNumber == 999) {
return 'Other';
}
if (isset(self::$carBrandGroups[$modelNumber[0]])) {
return self::$carBrandGroups[$modelNumber[0]];
}
return 'Unknown';
}
The modelNumber is three digits so everything that starts with 0 is a volvo model, so 001 is Volvo V40, 101 is BMW X6 and so on...
Like I stated above, everything works except for 0, is there a way to make this work and seen as a value?
Thank you in advance.
I'll asume the $modelNumber is a string with 3 characters in it. In your example you're passing $modelNumber[0] and it should result in an error.
Option 1: split your string into an array
public static function getCarBrandGroup($modelNumber)
{
if ($modelNumber == 999) {
return 'Other';
}
// Split your string into an array
$modelNumber = str_split($modelNumber);
if (isset(self::$carBrandGroups[$modelNumber[0]])) {
return self::$carBrandGroups[$modelNumber[0]];
}
return 'Unknown';
}
Option 2: use the first character to get the required model
public static function getCarBrandGroup($modelNumber)
{
if ($modelNumber == 999) {
return 'Other';
}
// Get the first character using substr
$modelNumber = substr($modelNumber, 0, 1);
if (isset(self::$carBrandGroups[$modelNumber])) {
return self::$carBrandGroups[$modelNumber];
}
return 'Unknown';
}
Option 3: Or if you like shorthand & fancy PHP7 operators:
public static function getCarBrandGroup($modelNumber)
{
if ($modelNumber == 999) {
return 'Other';
}
return self::$carBrandGroups[substr($modelNumber, 0, 1)] ?? 'Unknown';
}
You're most likely getting passed integer 1 instead of string 001. If you know that each code is supposed to have exactly three digits, then you can force a zero-padded reformat and cast to string at the same time by using sprintf():
public static function getCarBrandGroup($modelNumber)
{
$modelNumber = sprintf('%03d', $modelNumber);
if ($modelNumber === '999') {
return 'Other';
}
if (isset(self::$carBrandGroups[$modelNumber[0]])) {
return self::$carBrandGroups[$modelNumber[0]];
}
return 'Unknown';
}
Now, you can pass either (string) "001" or (int) 1.
I want to create custom error messages for fields in a form.
In these fields you are required to enter a number range that has to be in between 1 - 999999999999999999999.99, if its below 1 or over 99999 etc. i want a certain message to show.
This is the code i have for the moment but all i am getting is a blank white screen when i try to open the form.
$fee = new Zend_Form_Element_Text('job_salary');
$fee->setLabel("Salary ");
$fee->setAttrib('class', 'input-block-level');
$fee->addDecorator("ViewHelper");
$fee->addValidator('Float');
//$fee->addFilter("Alnum");
$fee->addValidator(new Zend_Validate_JobValidator(), true);
$fee = $salary->getMessageTemplates();
$fee->setMessage(
Zend_Validate_JobValidator::NOT_BETWEEN_SALARY
);
$fee->setRequired(true);
$this->addElement($fee);
This is the function for one of the fields.
I then created a separate validator class rather than using the Zend one.
const MSG_NUMERIC = 'msgNumeric';
const NOT_BETWEEN = 'notBetween';
const NOT_BETWEEN_SALARY = 'notBetween';
const NOT_BETWEEN_STRICT = 'notBetweenStrict';
public $minimum = 1;
public $maximum = 99999999999999999999999999999.99;
protected $_messageVariables = array(
'min' => 'minimum',
'max' => 'maximum'
);
protected $_messageTemplates = array(
self::MSG_NUMERIC => "'%value%' is not numeric",
self::NOT_BETWEEN => "'%value%' is not between '%min%' and '%max%' inclusively - Please fill in the correct amount for fees",
self::NOT_BETWEEN_SALARY => "'%value'is not between '%min%' and '%max%', inclusively - Please fill in the correct amount for this position ",
self::NOT_BETWEEN_STRICT => "'%value%' is not strictly between '%min%' and '%max%' inclusively - Please fill in the fee for this placement"
);
public function isValid($value) {
$this->_setValue($value);
if (!is_numeric($value)) {
$this->_error(self::MSG_NUMERIC);
return false;
}
if ($value < $this->minimum) {
$this->_error(self::NOT_BETWEEN);
return false;
}
if ($value < $this->minimum) {
$this->_error(self::NOT_BETWEEN_SALARY);
return false;
}
if ($value > $this->maximum) {
$this->_error(self::NOT_BETWEEN_STRICT);
return false;
}
return true;
}
}