Skip to content

Commit

Permalink
[FX-1873] Relax PAN validation for ME (#193)
Browse files Browse the repository at this point in the history
* Fix PAN validation for ME cards

* Add tests
  • Loading branch information
djoksimo authored Nov 14, 2024
1 parent bf692fb commit 8991649
Show file tree
Hide file tree
Showing 6 changed files with 137 additions and 68 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -12,75 +12,77 @@ class ForagePANValidations {
/// Check ebt card number length
static func checkPANLength(_ panNumber: String) -> StateIIN? {
if panNumber.count >= 6 {
let pan = panNumber.prefix(6)
return panNumbers.first { $0.panNumber == pan }
let iin = panNumber.prefix(6)
return panNumbers.first { $0.iin == iin }
}

return nil
}

/// List of allowed ebt card numbers and its respective length
static var panNumbers: [StateIIN] = [
StateIIN(state: USState.alabama, panNumber: "507680", panLength: 16),
StateIIN(state: USState.alaska, panNumber: "507695", panLength: 16),
StateIIN(state: USState.arizona, panNumber: "507706", panLength: 16),
StateIIN(state: USState.arkansas, panNumber: "610093", panLength: 16),
StateIIN(state: USState.california, panNumber: "507719", panLength: 16),
StateIIN(state: USState.colorado, panNumber: "507681", panLength: 16),
StateIIN(state: USState.connecticut, panNumber: "600890", panLength: 18),
StateIIN(state: USState.delaware, panNumber: "507713", panLength: 16),
StateIIN(state: USState.districtOfColumbia, panNumber: "507707", panLength: 16),
StateIIN(state: USState.florida, panNumber: "508139", panLength: 16),
StateIIN(state: USState.georgia, panNumber: "508148", panLength: 16),
StateIIN(state: USState.guam, panNumber: "578036", panLength: 16),
StateIIN(state: USState.hawaii, panNumber: "507698", panLength: 16),
StateIIN(state: USState.idaho, panNumber: "507692", panLength: 16),
StateIIN(state: USState.illinois, panNumber: "601453", panLength: 16),
StateIIN(state: USState.indiana, panNumber: "507704", panLength: 16),
StateIIN(state: USState.iowa, panNumber: "627485", panLength: 19),
StateIIN(state: USState.kansas, panNumber: "601413", panLength: 16),
StateIIN(state: USState.kentucky, panNumber: "507709", panLength: 16),
StateIIN(state: USState.louisiana, panNumber: "504476", panLength: 16),
StateIIN(state: USState.maine, panNumber: "507703", panLength: 19),
StateIIN(state: USState.maryland, panNumber: "600528", panLength: 16),
StateIIN(state: USState.massachusetts, panNumber: "600875", panLength: 18),
StateIIN(state: USState.michigan, panNumber: "507711", panLength: 16),
StateIIN(state: USState.minnesota, panNumber: "610423", panLength: 16),
StateIIN(state: USState.mississippi, panNumber: "507718", panLength: 16),
StateIIN(state: USState.missouri, panNumber: "507683", panLength: 16),
StateIIN(state: USState.montana, panNumber: "507714", panLength: 16),
StateIIN(state: USState.nebraska, panNumber: "507716", panLength: 16),
StateIIN(state: USState.nevada, panNumber: "507715", panLength: 16),
StateIIN(state: USState.newHampshire, panNumber: "507701", panLength: 16),
StateIIN(state: USState.newJersey, panNumber: "610434", panLength: 16),
StateIIN(state: USState.newMexico, panNumber: "586616", panLength: 16),
StateIIN(state: USState.newYork, panNumber: "600486", panLength: 19),
StateIIN(state: USState.northCarolina, panNumber: "508161", panLength: 16),
StateIIN(state: USState.ohio, panNumber: "507700", panLength: 16),
StateIIN(state: USState.oklahoma, panNumber: "508147", panLength: 16),
StateIIN(state: USState.oregon, panNumber: "507693", panLength: 16),
StateIIN(state: USState.pennsylvania, panNumber: "600760", panLength: 19),
StateIIN(state: USState.rhodeIsland, panNumber: "507682", panLength: 16),
StateIIN(state: USState.southCarolina, panNumber: "610470", panLength: 16),
StateIIN(state: USState.southDakota, panNumber: "508132", panLength: 16),
StateIIN(state: USState.tennessee, panNumber: "507702", panLength: 16),
StateIIN(state: USState.texas, panNumber: "610098", panLength: 19),
StateIIN(state: USState.usVirginIslands, panNumber: "507721", panLength: 16),
StateIIN(state: USState.utah, panNumber: "601036", panLength: 16),
StateIIN(state: USState.vermont, panNumber: "507705", panLength: 16),
StateIIN(state: USState.virginia, panNumber: "622044", panLength: 16),
StateIIN(state: USState.washington, panNumber: "507710", panLength: 16),
StateIIN(state: USState.westVirginia, panNumber: "507720", panLength: 16),
StateIIN(state: USState.wisconsin, panNumber: "507708", panLength: 16),
StateIIN(state: USState.wyoming, panNumber: "505349", panLength: 16),
StateIIN(state: USState.alabama, iin: "507680", panLengths: [16]),
StateIIN(state: USState.alaska, iin: "507695", panLengths: [16]),
StateIIN(state: USState.arizona, iin: "507706", panLengths: [16]),
StateIIN(state: USState.arkansas, iin: "610093", panLengths: [16]),
StateIIN(state: USState.california, iin: "507719", panLengths: [16]),
StateIIN(state: USState.colorado, iin: "507681", panLengths: [16]),
StateIIN(state: USState.connecticut, iin: "600890", panLengths: [18]),
StateIIN(state: USState.delaware, iin: "507713", panLengths: [16]),
StateIIN(state: USState.districtOfColumbia, iin: "507707", panLengths: [16]),
StateIIN(state: USState.florida, iin: "508139", panLengths: [16]),
StateIIN(state: USState.georgia, iin: "508148", panLengths: [16]),
StateIIN(state: USState.guam, iin: "578036", panLengths: [16]),
StateIIN(state: USState.hawaii, iin: "507698", panLengths: [16]),
StateIIN(state: USState.idaho, iin: "507692", panLengths: [16]),
StateIIN(state: USState.illinois, iin: "601453", panLengths: [16]),
StateIIN(state: USState.indiana, iin: "507704", panLengths: [16]),
StateIIN(state: USState.iowa, iin: "627485", panLengths: [19]),
StateIIN(state: USState.kansas, iin: "601413", panLengths: [16]),
StateIIN(state: USState.kentucky, iin: "507709", panLengths: [16]),
StateIIN(state: USState.louisiana, iin: "504476", panLengths: [16]),
// Maine cards can be 16 or 19 digits long!
StateIIN(state: USState.maine, iin: "507703", panLengths: [16, 19]),
StateIIN(state: USState.maryland, iin: "600528", panLengths: [16]),
StateIIN(state: USState.massachusetts, iin: "600875", panLengths: [18]),
StateIIN(state: USState.michigan, iin: "507711", panLengths: [16]),
StateIIN(state: USState.minnesota, iin: "610423", panLengths: [16]),
StateIIN(state: USState.mississippi, iin: "507718", panLengths: [16]),
StateIIN(state: USState.missouri, iin: "507683", panLengths: [16]),
StateIIN(state: USState.montana, iin: "507714", panLengths: [16]),
StateIIN(state: USState.nebraska, iin: "507716", panLengths: [16]),
StateIIN(state: USState.nevada, iin: "507715", panLengths: [16]),
StateIIN(state: USState.newHampshire, iin: "507701", panLengths: [16]),
StateIIN(state: USState.newJersey, iin: "610434", panLengths: [16]),
StateIIN(state: USState.newMexico, iin: "586616", panLengths: [16]),
StateIIN(state: USState.newYork, iin: "600486", panLengths: [19]),
StateIIN(state: USState.northCarolina, iin: "508161", panLengths: [16]),
StateIIN(state: USState.ohio, iin: "507700", panLengths: [16]),
StateIIN(state: USState.oklahoma, iin: "508147", panLengths: [16]),
StateIIN(state: USState.oregon, iin: "507693", panLengths: [16]),
StateIIN(state: USState.pennsylvania, iin: "600760", panLengths: [19]),
StateIIN(state: USState.rhodeIsland, iin: "507682", panLengths: [16]),
StateIIN(state: USState.southCarolina, iin: "610470", panLengths: [16]),
StateIIN(state: USState.southDakota, iin: "508132", panLengths: [16]),
StateIIN(state: USState.tennessee, iin: "507702", panLengths: [16]),
StateIIN(state: USState.texas, iin: "610098", panLengths: [19]),
StateIIN(state: USState.usVirginIslands, iin: "507721", panLengths: [16]),
StateIIN(state: USState.utah, iin: "601036", panLengths: [16]),
StateIIN(state: USState.vermont, iin: "507705", panLengths: [16]),
StateIIN(state: USState.virginia, iin: "622044", panLengths: [16]),
StateIIN(state: USState.washington, iin: "507710", panLengths: [16]),
StateIIN(state: USState.westVirginia, iin: "507720", panLengths: [16]),
StateIIN(state: USState.wisconsin, iin: "507708", panLengths: [16]),
StateIIN(state: USState.wyoming, iin: "505349", panLengths: [16]),
]
}

/// StateINN object to identify card object valid data
struct StateIIN {
let state: USState
let panNumber: String
let panLength: Int
// first 6 digits of EBT card
let iin: String
let panLengths: [Int]
}

extension StateIIN: Equatable {}
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,10 @@ class MaskedUITextField: FloatingTextField, ObservableState {
// MARK: - Text Handling

private func trimTrailingDigits(_ newUnmaskedText: String, stateIIN: StateIIN?) -> String {
let maxPanLength = min(newUnmaskedText.count, stateIIN?.panLength ?? 19)
let maxPanLength = min(
newUnmaskedText.count,
stateIIN?.panLengths.max() ?? 19
)

return String(newUnmaskedText.prefix(maxPanLength))
}
Expand Down Expand Up @@ -118,7 +121,7 @@ class MaskedUITextField: FloatingTextField, ObservableState {

if let stateIIN = stateIIN {
isValid = true
isComplete = newUnmaskedText.count == stateIIN.panLength
isComplete = stateIIN.panLengths.contains(newUnmaskedText.count)
derivedCardInfo.usState = stateIIN.state
return
}
Expand All @@ -134,12 +137,13 @@ class MaskedUITextField: FloatingTextField, ObservableState {
return MaskPattern.unset
}
if let stateIIN = stateIIN {
switch stateIIN.panLength {
case 16:
if stateIIN.panLengths.contains(16) && stateIIN.panLengths.contains(19) {
return newUnmaskedText.count <= 16 ? MaskPattern.sixteenDigits : MaskPattern.nineteenDigits
} else if stateIIN.panLengths.contains(16) {
return MaskPattern.sixteenDigits
case 18:
} else if stateIIN.panLengths.contains(18) {
return MaskPattern.eighteenDigits
default:
} else if stateIIN.panLengths.contains(19) {
return MaskPattern.nineteenDigits
}
}
Expand Down
25 changes: 25 additions & 0 deletions Tests/ForageSDKTests/ForagePANTextFieldTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,31 @@ final class ForagePANTextFieldTests: XCTestCase {
XCTAssertEqual(validTextField.derivedCardInfo.usState, .southDakota)
}

func test_maine_shouldBeValidCardFor16or19Digits() {
let validTextField = ForagePANTextField()

// 16 digits
validTextField.enhancedTextField.text = "5077031234123412"
validTextField.enhancedTextField.textFieldDidChange()
XCTAssertEqual(validTextField.enhancedTextField.actualPAN, "5077031234123412")

XCTAssertEqual(validTextField.enhancedTextField.text, "5077 0312 3412 3412")
XCTAssertTrue(validTextField.isValid)
XCTAssertTrue(validTextField.isComplete)
XCTAssertFalse(validTextField.isEmpty)
XCTAssertEqual(validTextField.derivedCardInfo.usState, .maine)

validTextField.enhancedTextField.text = "5077031234123412345"
validTextField.enhancedTextField.textFieldDidChange()
XCTAssertEqual(validTextField.enhancedTextField.actualPAN, "5077031234123412345")

XCTAssertEqual(validTextField.enhancedTextField.text, "507703 1234 1234 123 45")
XCTAssertTrue(validTextField.isValid)
XCTAssertTrue(validTextField.isComplete)
XCTAssertFalse(validTextField.isEmpty)
XCTAssertEqual(validTextField.derivedCardInfo.usState, .maine)
}

func test_textField_enterNumericString_shouldReturnTrue() {
let changesAllowed = foragePANTextField.textField(UITextField(), shouldChangeCharactersIn: NSRange(), replacementString: "1234")

Expand Down
17 changes: 12 additions & 5 deletions Tests/ForageSDKTests/ForagePANValidationsTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,20 +22,27 @@ final class ForagePANValidationsTests: XCTestCase {

func test_checkPANLength_withValid6digitsPan_shouldReturnState() {
let stateINN = ForagePANValidations.checkPANLength("507680")
let expectedResult = StateIIN(state: .alabama, panNumber: "507680", panLength: 16)
let expectedResult = StateIIN(state: .alabama, iin: "507680", panLengths: [16])
XCTAssertEqual(expectedResult, stateINN)
}

func test_checkPANLength_givenValidCard_shouldRespectMaxDigits() {
let stateINN = ForagePANValidations.checkPANLength("507680")
let expectedResult = 16
XCTAssertEqual(expectedResult, stateINN?.panLength)
let expectedResult = [16]
XCTAssertEqual(expectedResult, stateINN?.panLengths)
}

func test_checkPANLength_givenMainCard_shouldSupport16and19DigitCards() {
let stateINN = ForagePANValidations.checkPANLength("507703")
let expectedResult = [16, 19]
XCTAssertEqual(expectedResult, stateINN?.panLengths)
XCTAssertEqual(USState.maine, stateINN?.state)
}

func test_checkPANLength_givenValidCard_shouldRespectMaxDigits_forDifferentCards() {
let stateINN = ForagePANValidations.checkPANLength("600890")
let expectedResult = 18
XCTAssertEqual(expectedResult, stateINN?.panLength)
let expectedResult = [18]
XCTAssertEqual(expectedResult, stateINN?.panLengths)
}

func test_panNumbers_shouldHaveAllUsaStates() {
Expand Down
2 changes: 1 addition & 1 deletion Tests/ForageSDKTests/ForagePINTextFieldTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -207,7 +207,7 @@ final class ForagePINTextFieldTests: XCTestCase {

let textColor = foragePinTextField.textColor
XCTAssertEqual(color, textColor)

// Test RosettaPINTextField text color
let rosettaPINTextField = RosettaPINTextField()
rosettaPINTextField.textColor = .blue
Expand Down
31 changes: 31 additions & 0 deletions Tests/ForageSDKTests/MaskedUITextFieldTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -287,6 +287,37 @@ final class MaskedUITextFieldTests: XCTestCase {
XCTAssertEqual(maskedTextField.actualPAN, "9999123412341234123")
}

func testMasking_shouldMaskForMultiplePanLengths() {
// Maine cards can be 16 or 19 digits long

func assertMaskedTextField(input: String, expectedOutput: String) {
maskedTextField.text = input
maskedTextField.textFieldDidChange()
XCTAssertEqual(maskedTextField.text, expectedOutput)
}

assertMaskedTextField(
input: "507703111111123",
expectedOutput: "5077 0311 1111 123"
) // 15 digits
assertMaskedTextField(
input: "5077031111111234",
expectedOutput: "5077 0311 1111 1234"
) // 16 digits
assertMaskedTextField(
input: "50770311111112345",
expectedOutput: "507703 1111 1112 345"
) // 17 digits
assertMaskedTextField(
input: "507703111111123456",
expectedOutput: "507703 1111 1112 345 6"
) // 18 digits
assertMaskedTextField(
input: "5077031111111234567",
expectedOutput: "507703 1111 1112 345 67"
) // 19 digits
}

// MARK: Masking with backspace

func testMasking_backspaceAtEnd_shouldRemoveLastChar() {
Expand Down

0 comments on commit 8991649

Please sign in to comment.