-- Copyright (c) 2016-present, Facebook, Inc. -- All rights reserved. -- -- This source code is licensed under the BSD-style license found in the -- LICENSE file in the root directory of this source tree. module Duckling.CreditCardNumber.Helpers ( otherCreditCardNumberRegex , visaCreditCardNumberRegex , amexCreditCardNumberRegex , discoverCreditCardNumberRegex , mastercardCreditCardNumberRegex , dinerClubCreditCardNumberRegex , isValidCreditCardNumber , minNumberDigits , maxNumberDigits , creditCard ) where import Data.Text (Text) import Prelude import Data.String import Duckling.CreditCardNumber.Types (CreditCardNumberData(..)) import qualified Duckling.CreditCardNumber.Types as TCreditCardNumber import qualified Data.Text as T import qualified Data.Char as C import qualified Data.Bits as B -- ----------------------------------------------------------------- -- Patterns otherCreditCardNumberRegex :: String otherCreditCardNumberRegex = concat [ "(" , "(?!" , visaCreditCardNumberRegex, ")" , "(?!" , amexCreditCardNumberRegex, ")" , "(?!" , discoverCreditCardNumberRegex, ")" , "(?!" , mastercardCreditCardNumberRegex, ")" , "(?!" , dinerClubCreditCardNumberRegex, ")" , "\\d{" , show minNumberDigits , "," , show maxNumberDigits , "}" , ")" ] -- | Visa credit card regex informed by latest BIN info visaCreditCardNumberRegex :: String visaCreditCardNumberRegex = "(" ++ withoutDashes ++ "|" ++ withDashes ++")" where withoutDashes = "4[0-9]{15}" withDashes = "4[0-9]{3}-[0-9]{4}-[0-9]{4}-[0-9]{4}" -- | American Express credit card regex informed by latest BIN info amexCreditCardNumberRegex :: String amexCreditCardNumberRegex = "(" ++ withoutDashes ++ "|" ++ withDashes ++")" where withoutDashes = "3[47][0-9]{13}" withDashes = "3[47][0-9]{2}-[0-9]{6}-[0-9]{5}" -- | Discover credit card regex informed by latest BIN info discoverCreditCardNumberRegex :: String discoverCreditCardNumberRegex = "(" ++ withoutDashes ++ "|" ++ withDashes ++")" where withoutDashes = "6(?:011|[45][0-9]{2})[0-9]{12}" withDashes = "6(?:011|[45][0-9]{2})-[0-9]{4}-[0-9]{4}-[0-9]{4}" -- | Mastercard credit card regex informed by latest BIN info mastercardCreditCardNumberRegex :: String mastercardCreditCardNumberRegex = "(" ++ withoutDashes ++ "|" ++ withDashes ++")" where withoutDashes = "5[1-5][0-9]{14}" withDashes = "5[1-5][0-9]{2}-[0-9]{4}-[0-9]{4}-[0-9]{4}" -- | Diner Club credit card regex informed by latest BIN info dinerClubCreditCardNumberRegex :: String dinerClubCreditCardNumberRegex = "(" ++ withoutDashes ++ "|" ++ withDashes ++")" where withoutDashes = "3(?:0[0-5]|[68][0-9])[0-9]{11}" withDashes = "3(?:0[0-5]|[68][0-9])[0-9]-[0-9]{6}-[0-9]{4}" -- ----------------------------------------------------------------- -- Validation -- | An implementation of the Luhn algorithm (see -- https://en.wikipedia.org/wiki/Luhn_algorithm) to check if a given credit card -- number is valid isValidCreditCardNumber :: Text -> Bool isValidCreditCardNumber ccNum = T.length ccNum >= minNumberDigits && T.length ccNum <= maxNumberDigits && validCheckSum where validCheckSum :: Bool validCheckSum = T.all C.isDigit ccNum && fst (T.foldr f (0, 0) ccNum) `rem` 10 == 0 where f char (checksum, e) = let val = C.digitToInt char -- every even digit should be doubled d = sumDigits (B.shift val e) in (checksum + d, 1 - e) -- we only need sum of digits for numbers from 0 to 18 sumDigits a | a > 9 = a - 9 | otherwise = a minNumberDigits :: Int minNumberDigits = 8 maxNumberDigits :: Int maxNumberDigits = 19 -- ----------------------------------------------------------------- -- Production creditCard :: Text -> TCreditCardNumber.Issuer -> CreditCardNumberData creditCard ccNum i = CreditCardNumberData { TCreditCardNumber.number = ccNum , TCreditCardNumber.issuer = i }