-- 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 :: String
otherCreditCardNumberRegex =
  [String] -> String
forall (t :: * -> *) a. Foldable t => t [a] -> [a]
concat [ String
"("
         , String
"(?!" , String
visaCreditCardNumberRegex, String
")"
         , String
"(?!" , String
amexCreditCardNumberRegex, String
")"
         , String
"(?!" , String
discoverCreditCardNumberRegex, String
")"
         , String
"(?!" , String
mastercardCreditCardNumberRegex, String
")"
         , String
"(?!" , String
dinerClubCreditCardNumberRegex, String
")"
         , String
"\\d{" , Int -> String
forall a. Show a => a -> String
show Int
minNumberDigits , String
"," , Int -> String
forall a. Show a => a -> String
show Int
maxNumberDigits , String
"}"
         , String
")"
         ]

-- | Visa credit card regex informed by latest BIN info
visaCreditCardNumberRegex :: String
visaCreditCardNumberRegex :: String
visaCreditCardNumberRegex = String
"(" String -> String -> String
forall a. [a] -> [a] -> [a]
++ String
withoutDashes String -> String -> String
forall a. [a] -> [a] -> [a]
++ String
"|" String -> String -> String
forall a. [a] -> [a] -> [a]
++ String
withDashes String -> String -> String
forall a. [a] -> [a] -> [a]
++String
")"
  where
    withoutDashes :: String
withoutDashes = String
"4[0-9]{15}"
    withDashes :: String
withDashes = String
"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 :: String
amexCreditCardNumberRegex = String
"(" String -> String -> String
forall a. [a] -> [a] -> [a]
++ String
withoutDashes String -> String -> String
forall a. [a] -> [a] -> [a]
++ String
"|" String -> String -> String
forall a. [a] -> [a] -> [a]
++ String
withDashes String -> String -> String
forall a. [a] -> [a] -> [a]
++String
")"
  where
    withoutDashes :: String
withoutDashes = String
"3[47][0-9]{13}"
    withDashes :: String
withDashes = String
"3[47][0-9]{2}-[0-9]{6}-[0-9]{5}"

-- | Discover credit card regex informed by latest BIN info
discoverCreditCardNumberRegex :: String
discoverCreditCardNumberRegex :: String
discoverCreditCardNumberRegex = String
"(" String -> String -> String
forall a. [a] -> [a] -> [a]
++ String
withoutDashes String -> String -> String
forall a. [a] -> [a] -> [a]
++ String
"|" String -> String -> String
forall a. [a] -> [a] -> [a]
++ String
withDashes String -> String -> String
forall a. [a] -> [a] -> [a]
++String
")"
  where
    withoutDashes :: String
withoutDashes = String
"6(?:011|[45][0-9]{2})[0-9]{12}"
    withDashes :: String
withDashes = String
"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 :: String
mastercardCreditCardNumberRegex =
  String
"(" String -> String -> String
forall a. [a] -> [a] -> [a]
++ String
withoutDashes String -> String -> String
forall a. [a] -> [a] -> [a]
++ String
"|" String -> String -> String
forall a. [a] -> [a] -> [a]
++ String
withDashes String -> String -> String
forall a. [a] -> [a] -> [a]
++String
")"
  where
    withoutDashes :: String
withoutDashes = String
"5[1-5][0-9]{14}"
    withDashes :: String
withDashes = String
"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 :: String
dinerClubCreditCardNumberRegex = String
"(" String -> String -> String
forall a. [a] -> [a] -> [a]
++ String
withoutDashes String -> String -> String
forall a. [a] -> [a] -> [a]
++ String
"|" String -> String -> String
forall a. [a] -> [a] -> [a]
++ String
withDashes String -> String -> String
forall a. [a] -> [a] -> [a]
++String
")"
  where
    withoutDashes :: String
withoutDashes = String
"3(?:0[0-5]|[68][0-9])[0-9]{11}"
    withDashes :: String
withDashes = String
"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 :: Text -> Bool
isValidCreditCardNumber Text
ccNum =
  Text -> Int
T.length Text
ccNum Int -> Int -> Bool
forall a. Ord a => a -> a -> Bool
>= Int
minNumberDigits Bool -> Bool -> Bool
&&
  Text -> Int
T.length Text
ccNum Int -> Int -> Bool
forall a. Ord a => a -> a -> Bool
<= Int
maxNumberDigits Bool -> Bool -> Bool
&&
  Bool
validCheckSum
  where
  validCheckSum :: Bool
  validCheckSum :: Bool
validCheckSum =
    (Char -> Bool) -> Text -> Bool
T.all Char -> Bool
C.isDigit Text
ccNum Bool -> Bool -> Bool
&&
    (Int, Int) -> Int
forall a b. (a, b) -> a
fst ((Char -> (Int, Int) -> (Int, Int))
-> (Int, Int) -> Text -> (Int, Int)
forall a. (Char -> a -> a) -> a -> Text -> a
T.foldr Char -> (Int, Int) -> (Int, Int)
f (Int
0, Int
0) Text
ccNum) Int -> Int -> Int
forall a. Integral a => a -> a -> a
`rem` Int
10 Int -> Int -> Bool
forall a. Eq a => a -> a -> Bool
== Int
0
    where
    f :: Char -> (Int, Int) -> (Int, Int)
f Char
char (Int
checksum, Int
e) =
      let
        val :: Int
val = Char -> Int
C.digitToInt Char
char
        -- every even digit should be doubled
        d :: Int
d = Int -> Int
forall p. (Ord p, Num p) => p -> p
sumDigits (Int -> Int -> Int
forall a. Bits a => a -> Int -> a
B.shift Int
val Int
e)
      in (Int
checksum Int -> Int -> Int
forall a. Num a => a -> a -> a
+ Int
d, Int
1 Int -> Int -> Int
forall a. Num a => a -> a -> a
- Int
e)
    -- we only need sum of digits for numbers from 0 to 18
    sumDigits :: p -> p
sumDigits p
a
      | p
a p -> p -> Bool
forall a. Ord a => a -> a -> Bool
> p
9 = p
a p -> p -> p
forall a. Num a => a -> a -> a
- p
9
      | Bool
otherwise = p
a

minNumberDigits :: Int
minNumberDigits :: Int
minNumberDigits = Int
8

maxNumberDigits :: Int
maxNumberDigits :: Int
maxNumberDigits = Int
19

-- -----------------------------------------------------------------
-- Production

creditCard :: Text -> TCreditCardNumber.Issuer -> CreditCardNumberData
creditCard :: Text -> Issuer -> CreditCardNumberData
creditCard Text
ccNum Issuer
i =
  CreditCardNumberData :: Text -> Issuer -> CreditCardNumberData
CreditCardNumberData { number :: Text
TCreditCardNumber.number = Text
ccNum
                       , issuer :: Issuer
TCreditCardNumber.issuer = Issuer
i
                       }