Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Credit Card Validation Support #217

Open
wants to merge 24 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
c911c77
added credit card number validation support
quacklabs Mar 8, 2019
437bd63
added cardparser
quacklabs Mar 8, 2019
15d04f7
Fixed validation return error on creditCardRule
quacklabs Mar 8, 2019
0a77de4
fixed card returning invalid due to spaces
quacklabs Mar 8, 2019
9c1bb96
fixed card returning invalid due to spaces
quacklabs Mar 8, 2019
bdc64c1
fixed card returning invalid due to spaces
quacklabs Mar 8, 2019
6389105
Changed Readme to notify of CardParser Library
quacklabs Mar 8, 2019
df7748f
created Unit tests for card number validation
quacklabs Mar 8, 2019
73d89ae
Fixed credit card number validation build test
quacklabs Mar 8, 2019
27c2742
testing card validation
quacklabs Mar 8, 2019
168a5b0
resolved group linking causing build errors
quacklabs Mar 8, 2019
bd3eb11
fixed Unit tests and folder group for xcode
quacklabs Mar 9, 2019
4aaa5e7
Merge pull request #1 from quacklabs/card_validation
quacklabs Mar 9, 2019
f99a46c
Card Expiry Date in full (eg: 10/12) rule added
quacklabs Mar 15, 2019
91ef7c3
minor fixes in comments
quacklabs Mar 15, 2019
4bf4306
Merge pull request #2 from quacklabs/card_validation
quacklabs Mar 15, 2019
c0e5af7
removed comment line
quacklabs Mar 18, 2019
8aacb35
Merge pull request #3 from quacklabs/card_validation
quacklabs Mar 18, 2019
fe3a1db
fixed month not validating past November
quacklabs Mar 18, 2019
86714d2
Merge pull request #4 from quacklabs/card_validation
quacklabs Mar 18, 2019
4d61e5c
added checks for blank card expiry date causing crash
quacklabs Mar 18, 2019
65e98b2
Merge pull request #5 from quacklabs/card_validation
quacklabs Mar 18, 2019
dc2ada4
fixed endIndex error in expiry date validation
quacklabs Mar 18, 2019
039b2cd
Merge pull request #6 from quacklabs/card_validation
quacklabs Mar 18, 2019
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,11 @@ If you are using Carthage you will need to add this to your `Cartfile`
```bash
github "jpotts18/SwiftValidator"
```
## Dependencies

CardNumberRule uses CardParser from => https://github.com/Raizlabs/CardParser.git
Cards can be validated to ensure they conform to LUHN algorigthm.


## Usage

Expand Down
249 changes: 249 additions & 0 deletions SwiftValidator/Parsers/CardParser.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,249 @@

//
// CardParser.swift
//
// Created by Jason Clark on 6/28/16.
// Copyright © 2016 Raizlabs. All rights reserved.
//
//MARK: - CardType
enum CardType {
case amex
case diners
case discover
case jcb
case mastercard
case visa
case verve

static let allValues: [CardType] = [.visa, .mastercard, .amex, .diners, .discover, .jcb, .verve]

private var validationRequirements: ValidationRequirement {

let prefix: [PrefixContainable], length: [Int]

switch self {
/* // IIN prefixes and length requriements retreived from https://en.wikipedia.org/wiki/Bank_card_number on June 28, 2016 */

case .amex:
prefix = ["34", "37"]
length = [15]
break

case .diners:
prefix = ["300"..."305", "309", "36", "38"..."39"]
length = [14]
break

case .discover:
prefix = ["6011", "65", "644"..."649", "622126"..."622925"]
length = [16]
break

case .jcb:
prefix = ["3528"..."3589"]
length = [16]
break

case .mastercard:
prefix = ["51"..."55", "2221"..."2720"]
length = [16]
break

case .visa:
prefix = ["4"]
length = [13, 16, 19]
break

case .verve:
prefix = ["5060", "5061", "5078", "5079", "6500"]
length = [16, 19]
break

}

return ValidationRequirement(prefixes: prefix, lengths: length)
}



var segmentGroupings: [Int] {
switch self {
case .amex:
return [4, 6, 5]
case .diners:
return [4, 6, 4]
case .verve:
return [4, 4, 4, 6]
default:
return [4, 4, 4, 4]
}
}

var maxLength: Int {
return validationRequirements.lengths.max() ?? 16
}

var cvvLength: Int {
switch self {
case .amex:
return 4
default:
return 3
}
}

func isValid(_ accountNumber: String) -> Bool {
return validationRequirements.isValid(accountNumber) && CardType.luhnCheck(accountNumber)
}

func isPrefixValid(_ accountNumber: String) -> Bool {
return validationRequirements.isPrefixValid(accountNumber)
}

}


//MARK: Validation requirements and rules

fileprivate extension CardType {

struct ValidationRequirement {
let prefixes: [PrefixContainable]
let lengths: [Int]

func isValid(_ accountNumber: String) -> Bool {
return isLengthValid(accountNumber) && isPrefixValid(accountNumber)
}

func isPrefixValid(_ accountNumber: String) -> Bool {

guard prefixes.count > 0 else { return true }
return prefixes.contains { $0.hasCommonPrefix(with: accountNumber) }
}

func isLengthValid(_ accountNumber: String) -> Bool {
guard lengths.count > 0 else { return true }
return lengths.contains { accountNumber.length == $0 }
}
}

// from: https://gist.github.com/cwagdev/635ce973e8e86da0403a
static func luhnCheck(_ cardNumber: String) -> Bool {

guard let _ = Int64(cardNumber) else {
//if string is not convertible to int, return false
return false
}
let numberOfChars = cardNumber.count
let numberToCheck = numberOfChars % 2 == 0 ? cardNumber : "0" + cardNumber

let digits = numberToCheck.map { Int(String($0)) }

let sum = digits.enumerated().reduce(0) { (sum, val: (offset: Int, element: Int?)) in
if (val.offset + 1) % 2 == 1 {
let element = val.element!
return sum + (element == 9 ? 9 : (element * 2) % 9)
}
// else
return sum + val.element!
}
let validates = sum % 10 == 0
print("card valid")
return validates
}

}


//MARK: - CardState
enum CardState {
case identified(CardType)
case indeterminate([CardType])
case invalid
}


extension CardState: Equatable {}
func ==(lhs: CardState, rhs: CardState) -> Bool {
switch (lhs, rhs) {
case (.invalid, .invalid): return true
case (let .indeterminate(cards1), let .indeterminate(cards2)): return cards1 == cards2
case (let .identified(card1), let .identified(card2)): return card1 == card2
default: return false
}
}


extension CardState {

init(fromNumber number: String) {
if let card = CardType.allValues.first(where: { $0.isValid(number) }) {
print("card found \(card)")
self = .identified(card)
}
else {
self = .invalid
}
}

init(fromPrefix prefix: String) {
let possibleTypes = CardType.allValues.filter { $0.isPrefixValid(prefix) }
if possibleTypes.count >= 2 {
self = .indeterminate(possibleTypes)
}
else if possibleTypes.count == 1, let card = possibleTypes.first {
self = .identified(card)
}
else {
self = .invalid
}
}

}

//MARK: - PrefixContainable
fileprivate protocol PrefixContainable {

func hasCommonPrefix(with text: String) -> Bool

}

extension ClosedRange: PrefixContainable {

func hasCommonPrefix(with text: String) -> Bool {
//cannot include Where clause in protocol conformance, so have to ensure Bound == String :(
guard let lower = lowerBound as? String, let upper = upperBound as? String else { return false }

let trimmedRange: ClosedRange<String> = {
let length = text.length
let trimmedStart = lower.prefix(length)
let trimmedEnd = upper.prefix(length)
return trimmedStart...trimmedEnd
}()

let trimmedText = text.prefix(trimmedRange.lowerBound.count)
return trimmedRange ~= trimmedText
}

}


extension String: PrefixContainable {

func hasCommonPrefix(with text: String) -> Bool {
return hasPrefix(text) || text.hasPrefix(self)
}

}

fileprivate extension String {

func prefix(_ maxLength: Int) -> String {
return String(maxLength)
}

var length: Int {
return count
}

}
65 changes: 65 additions & 0 deletions SwiftValidator/Rules/CardExpiryRule.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
//
// CardExpiryRule.swift
// SwiftValidator
//
// Created by Mark Boleigha on 13/03/2019.
// Copyright © 2019 jpotts18. All rights reserved.
//

import Foundation



/**
`CardExpiryRule` is a subclass of `Rule` that defines how a credit/debit's card number field is validated
*/
public class CardExpiryRule: Rule {
/// Error message to be displayed if validation fails.
private var message : String
/**
Initializes `CardExpiryRule` object with error message. Used to validate a card's expiry year.

- parameter message: String of error message.
- returns: An initialized `CardExpiryRule` object, or nil if an object could not be created for some reason that would not result in an exception.
*/
public init(message : String = "Card expiry date is invalid"){
self.message = message
}

/**
Validates a field.

- parameter value: String to check for validation. must be a card expiry date in MM/YY format
- returns: Boolean value. True on successful validation, otherwise False on failed Validation.
*/
public func validate(_ value: String) -> Bool {
guard value.count > 4 else{
return false
}
let date = value.replacingOccurrences(of: "/", with: "")
let monthIndex = date.index(date.startIndex, offsetBy: 2)
let Month = Int(date[..<monthIndex])

let yearIndex = date.index(date.endIndex, offsetBy: -2)
let Year = Int(date[yearIndex...])

///Holds the current year
let thisYear = String(NSCalendar.current.component(Calendar.Component.year, from: Date()))
let thisYearLast2 = thisYear.index(thisYear.startIndex, offsetBy: 2)
let thisYearTwoDigits = Int(thisYear[thisYearLast2...])!


return Month! <= 12 && Year! >= thisYearTwoDigits

}

/**
Used to display error message when validation fails.

- returns: String of error message.
*/
public func errorMessage() -> String {
return message
}

}
51 changes: 51 additions & 0 deletions SwiftValidator/Rules/CardNumberRule.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
//
// CardNumberRule.swift
// Validator
//
// Created by Boleigha Mark on 08/03/2018.
// Copyright © 2017 jpotts18. All rights reserved.
//
import Foundation

/**
`CardNumberRule` is a subclass of `Rule` that defines how a credit/debit's card number field is validated
*/
public class CardNumberRule: Rule {
/// Error message to be displayed if validation fails.
private var message : String
/**
Initializes `CardNumberRule` object with error message. Used to validate a card's expiry month.

- parameter message: String of error message.
- returns: An initialized `CardNumberRule` object, or nil if an object could not be created for some reason that would not result in an exception.
*/
public init(message : String = "Card is Invalid"){
self.message = message
}

/**
Validates a field.

- parameter value: String to check for validation.
- returns: Boolean value. True on successful validation, otherwise False on failed Validation.
*/
public func validate(_ value: String) -> Bool {
let cardNoFull = value.replacingOccurrences(of: " ", with: "")
guard CardState(fromNumber: cardNoFull) != .invalid else {
return false
}

return true

}

/**
Used to display error message when validation fails.

- returns: String of error message.
*/
public func errorMessage() -> String {
return message
}

}
Loading