|
| 1 | +/** |
| 2 | + * NOTE: This pattern can check certain aspects of a credit card number, but not |
| 3 | + * all. Specifically, credit card numbers often use a [Luhn |
| 4 | + * checksum](https://en.wikipedia.org/wiki/Luhn_algorithm) to verify that the |
| 5 | + * number is valid. This pattern does not check that checksum, so it will accept |
| 6 | + * some invalid credit card numbers. |
| 7 | + * |
| 8 | + * If you want Luhn checksum validation, you can use the |
| 9 | + * [`schemata-ts`](https://github.com/jacob-alford/schemata-ts) library. |
| 10 | + */ |
| 11 | +import { pipe } from 'fp-ts/function' |
| 12 | + |
| 13 | +import { |
| 14 | + between, |
| 15 | + char, |
| 16 | + characterClass, |
| 17 | + exactString, |
| 18 | + exactly, |
| 19 | + or, |
| 20 | + sequence, |
| 21 | + subgroup, |
| 22 | + then, |
| 23 | +} from '../base' |
| 24 | +import { digit } from '../character-classes' |
| 25 | +import { oneOf } from '../combinators' |
| 26 | + |
| 27 | +// source: https://en.wikipedia.org/w/index.php?title=Payment_card_number&oldid=1110892430 |
| 28 | +// afaict the 13-digit variant has not been a thing for years, but maybe there |
| 29 | +// are still some valid cards floating around? |
| 30 | +// /(^4(\d{12}|\d{15})$)/ |
| 31 | +const visa = pipe( |
| 32 | + char('4'), |
| 33 | + then(pipe(exactly(12)(digit), or(exactly(15)(digit)), subgroup)), |
| 34 | +) |
| 35 | + |
| 36 | +// source: https://web.archive.org/web/20180514224309/https://www.mastercard.us/content/dam/mccom/global/documents/mastercard-rules.pdf |
| 37 | +// /(^(5[1-5]\d{4}|222[1-9]\d{2}|22[3-9]\d{3}|2[3-6]\d{4}|27[01]\d{3}|2720\d{2})\d{10}$)/ |
| 38 | +const mastercard = pipe( |
| 39 | + subgroup( |
| 40 | + pipe( |
| 41 | + sequence(char('5'), characterClass(false, ['1', '5']), exactly(4)(digit)), |
| 42 | + or( |
| 43 | + sequence( |
| 44 | + exactString('222'), |
| 45 | + characterClass(false, ['1', '9']), |
| 46 | + exactly(2)(digit), |
| 47 | + ), |
| 48 | + ), |
| 49 | + or( |
| 50 | + sequence( |
| 51 | + exactString('22'), |
| 52 | + characterClass(false, ['3', '9']), |
| 53 | + exactly(3)(digit), |
| 54 | + ), |
| 55 | + ), |
| 56 | + or( |
| 57 | + sequence( |
| 58 | + exactString('2'), |
| 59 | + characterClass(false, ['3', '6']), |
| 60 | + exactly(4)(digit), |
| 61 | + ), |
| 62 | + ), |
| 63 | + or( |
| 64 | + sequence( |
| 65 | + exactString('27'), |
| 66 | + characterClass(false, '0', '1'), |
| 67 | + exactly(3)(digit), |
| 68 | + ), |
| 69 | + ), |
| 70 | + or(sequence(exactString('2720'), exactly(2)(digit))), |
| 71 | + ), |
| 72 | + ), |
| 73 | + then(exactly(10)(digit)), |
| 74 | +) |
| 75 | + |
| 76 | +// source: https://web.archive.org/web/20210504163517/https://www.americanexpress.com/content/dam/amex/hk/en/staticassets/merchant/pdf/support-and-services/useful-information-and-downloads/GuidetoCheckingCardFaces.pdf |
| 77 | +// /(^3[47]\d{13}$)/ |
| 78 | +const amex = sequence( |
| 79 | + char('3'), |
| 80 | + characterClass(false, '4', '7'), |
| 81 | + exactly(13)(digit), |
| 82 | +) |
| 83 | + |
| 84 | +// US/Canada DCI cards will match as Mastercard (source: https://web.archive.org/web/20081204135437/http://www.mastercard.com/in/merchant/en/solutions_resources/dinersclub.html) |
| 85 | +// Others match regex below (source: https://web.archive.org/web/20170822221741/https://www.discovernetwork.com/downloads/IPP_VAR_Compliance.pdf) |
| 86 | +// /^(3(0([0-5]\d{5}|95\d{4})|[89]\d{6})\d{8,11}|36\d{6}\d{6,11})$/ |
| 87 | +const dinersClub = pipe( |
| 88 | + sequence( |
| 89 | + char('3'), |
| 90 | + subgroup( |
| 91 | + pipe( |
| 92 | + sequence( |
| 93 | + char('0'), |
| 94 | + subgroup( |
| 95 | + pipe( |
| 96 | + sequence(characterClass(false, ['0', '5']), exactly(5)(digit)), |
| 97 | + or(sequence(exactString('95'), exactly(4)(digit))), |
| 98 | + ), |
| 99 | + ), |
| 100 | + ), |
| 101 | + or(sequence(characterClass(false, '8', '9'), exactly(6)(digit))), |
| 102 | + ), |
| 103 | + ), |
| 104 | + between(8, 11)(digit), |
| 105 | + ), |
| 106 | + or(sequence(exactString('36'), exactly(6)(digit), between(6, 11)(digit))), |
| 107 | + subgroup, |
| 108 | +) |
| 109 | + |
| 110 | +// source: https://web.archive.org/web/20170822221741/https://www.discovernetwork.com/downloads/IPP_VAR_Compliance.pdf |
| 111 | +// /(^(6011(0[5-9]\d{2}|[2-4]\d{3}|74\d{2}|7[7-9]\d{2}|8[6-9]\d{2}|9\d{3})|64[4-9]\d{5}|650[0-5]\d{4}|65060[1-9]\d{2}|65061[1-9]\d{2}|6506[2-9]\d{3}|650[7-9]\d{4}|65[1-9]\d{5})\d{8,11}$)/, |
| 112 | +const discover = pipe( |
| 113 | + oneOf( |
| 114 | + pipe( |
| 115 | + exactString('6011'), |
| 116 | + then( |
| 117 | + subgroup( |
| 118 | + oneOf( |
| 119 | + sequence( |
| 120 | + char('0'), |
| 121 | + characterClass(false, ['5', '9']), |
| 122 | + exactly(2)(digit), |
| 123 | + ), |
| 124 | + sequence(characterClass(false, ['2', '4']), exactly(3)(digit)), |
| 125 | + sequence(exactString('74'), exactly(2)(digit)), |
| 126 | + sequence( |
| 127 | + exactString('7'), |
| 128 | + characterClass(false, ['7', '9']), |
| 129 | + exactly(2)(digit), |
| 130 | + ), |
| 131 | + sequence( |
| 132 | + exactString('8'), |
| 133 | + characterClass(false, ['6', '9']), |
| 134 | + exactly(2)(digit), |
| 135 | + ), |
| 136 | + sequence(exactString('9'), exactly(3)(digit)), |
| 137 | + ), |
| 138 | + ), |
| 139 | + ), |
| 140 | + ), |
| 141 | + sequence( |
| 142 | + exactString('64'), |
| 143 | + characterClass(false, ['4', '9']), |
| 144 | + exactly(5)(digit), |
| 145 | + ), |
| 146 | + sequence( |
| 147 | + exactString('650'), |
| 148 | + characterClass(false, ['0', '5']), |
| 149 | + exactly(4)(digit), |
| 150 | + ), |
| 151 | + sequence( |
| 152 | + exactString('65060'), |
| 153 | + characterClass(false, ['1', '9']), |
| 154 | + exactly(2)(digit), |
| 155 | + ), |
| 156 | + sequence( |
| 157 | + exactString('65061'), |
| 158 | + characterClass(false, ['1', '9']), |
| 159 | + exactly(2)(digit), |
| 160 | + ), |
| 161 | + sequence( |
| 162 | + exactString('6506'), |
| 163 | + characterClass(false, ['2', '9']), |
| 164 | + exactly(3)(digit), |
| 165 | + ), |
| 166 | + sequence( |
| 167 | + exactString('650'), |
| 168 | + characterClass(false, ['7', '9']), |
| 169 | + exactly(4)(digit), |
| 170 | + ), |
| 171 | + sequence( |
| 172 | + exactString('65'), |
| 173 | + characterClass(false, ['1', '9']), |
| 174 | + exactly(5)(digit), |
| 175 | + ), |
| 176 | + ), |
| 177 | + subgroup, |
| 178 | + then(between(8, 11)(digit)), |
| 179 | +) |
| 180 | + |
| 181 | +// /^(352[89]\d{4}|35[3-8]\d{5})\d{8,11}$/ |
| 182 | +const jcb = pipe( |
| 183 | + sequence( |
| 184 | + exactString('352'), |
| 185 | + characterClass(false, '8', '9'), |
| 186 | + exactly(4)(digit), |
| 187 | + ), |
| 188 | + or( |
| 189 | + sequence( |
| 190 | + exactString('35'), |
| 191 | + characterClass(false, ['3', '8']), |
| 192 | + exactly(5)(digit), |
| 193 | + ), |
| 194 | + ), |
| 195 | + subgroup, |
| 196 | + then(between(8, 11)(digit)), |
| 197 | +) |
| 198 | + |
| 199 | +// Rupay |
| 200 | +// some are JCB co-branded so will match as JCB above |
| 201 | +// for the rest, best source I could find is just wikipedia: |
| 202 | +// https://en.wikipedia.org/w/index.php?title=Payment_card_number&oldid=1110892430 |
| 203 | +// /^((60|65|81|82)\d{14}|508\d{14})$/ |
| 204 | +const rupay = subgroup( |
| 205 | + oneOf( |
| 206 | + sequence( |
| 207 | + subgroup( |
| 208 | + oneOf( |
| 209 | + exactString('60'), |
| 210 | + exactString('65'), |
| 211 | + exactString('81'), |
| 212 | + exactString('82'), |
| 213 | + ), |
| 214 | + ), |
| 215 | + exactly(14)(digit), |
| 216 | + ), |
| 217 | + sequence(exactString('508'), exactly(14)(digit)), |
| 218 | + ), |
| 219 | +) |
| 220 | + |
| 221 | +// /^62(2(12[6-9]\d{2}|1[3-9]\d{3}|[2-8]\d|9[01]\d{3}|92[0-5]\d{2})|[4-6]\d{5}|8[2-8]\d{4})\d{8,11}$/ |
| 222 | +const unionPay = sequence( |
| 223 | + exactString('62'), |
| 224 | + subgroup( |
| 225 | + oneOf( |
| 226 | + sequence( |
| 227 | + char('2'), |
| 228 | + subgroup( |
| 229 | + oneOf( |
| 230 | + sequence( |
| 231 | + exactString('12'), |
| 232 | + characterClass(false, ['6', '9']), |
| 233 | + exactly(2)(digit), |
| 234 | + ), |
| 235 | + sequence( |
| 236 | + char('1'), |
| 237 | + characterClass(false, ['3', '9']), |
| 238 | + exactly(3)(digit), |
| 239 | + ), |
| 240 | + sequence(characterClass(false, ['2', '8']), digit), |
| 241 | + sequence( |
| 242 | + exactString('9'), |
| 243 | + characterClass(false, '0', '1'), |
| 244 | + exactly(3)(digit), |
| 245 | + ), |
| 246 | + sequence( |
| 247 | + exactString('92'), |
| 248 | + characterClass(false, ['0', '5']), |
| 249 | + exactly(2)(digit), |
| 250 | + ), |
| 251 | + ), |
| 252 | + ), |
| 253 | + ), |
| 254 | + sequence(characterClass(false, ['4', '6']), exactly(5)(digit)), |
| 255 | + sequence( |
| 256 | + exactString('8'), |
| 257 | + characterClass(false, ['2', '8']), |
| 258 | + exactly(4)(digit), |
| 259 | + ), |
| 260 | + ), |
| 261 | + ), |
| 262 | + between(8, 11)(digit), |
| 263 | +) |
| 264 | + |
| 265 | +/** |
| 266 | + * @since 1.1.0 |
| 267 | + * @category Pattern |
| 268 | + */ |
| 269 | +export const creditCard = oneOf( |
| 270 | + visa, |
| 271 | + mastercard, |
| 272 | + amex, |
| 273 | + dinersClub, |
| 274 | + discover, |
| 275 | + jcb, |
| 276 | + rupay, |
| 277 | + unionPay, |
| 278 | +) |
0 commit comments