uk_account_validator 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,49 @@
1
+ Feature: Exceptions
2
+
3
+ Background:
4
+ Given I create a new checker
5
+
6
+ Scenario Outline: Valid Codes
7
+ Given I have a sort code <sortcode>
8
+ And I have an account number <accountnumber>
9
+
10
+ Then the combination is valid
11
+
12
+ Examples:
13
+ | test | description | sortcode | accountnumber |
14
+ | 4 | Exception 10 & 11 where first check passes and second check fails. | 871427 | 46238510 |
15
+ | 5 | Exception 10 & 11 where first check fails and second check passes. | 872427 | 46238510 |
16
+ | 6 | Exception 10 where in the account number ab=09 and the g=9. The first check passes and second check fails. | 871427 | 09123496 |
17
+ | 7 | Exception 10 where in the account number ab=99 and the g=9. The first check passes and the second check fails. | 871427 | 99123496 |
18
+ | 8 | Exception 3, and the sorting code is the start of a range. As c=6 the second check should be ignored. | 820000 | 73688637 |
19
+ | 9 | Exception 3, and the sorting code is the end of a range. As c=9 the second check should be ignored. | 827999 | 73988638 |
20
+ | 10 | Exception 3. As c!=6 or 9 perform both checks pass. | 827101 | 28748352 |
21
+ | 11 | Exception 4 where the remainder is equal to the checkdigit. | 134020 | 63849203 |
22
+ | 12 | Exception 1 - ensures that 27 has been added to the accumulated total and passes double alternate modulus check. | 118765 | 64371389 |
23
+ | 13 | Exception 6 where the account fails standard check but is a foreign currency account. | 200915 | 41011166 |
24
+ | 14 | Exception 5 where the check passes. | 938611 | 07806039 |
25
+ | 15 | Exception 5 where the check passes with substitution. | 938600 | 42368003 |
26
+ | 16 | Exception 5 where both checks produce a remainder of 0 and pass. | 938063 | 55065200 |
27
+ | 17 | Exception 7 where passes but would fail the standard check. | 772798 | 99345694 |
28
+ | 18 | Exception 8 where the check passes. | 086090 | 06774744 |
29
+ | 19 | Exception 2 & 9 where the first check passes. | 309070 | 02355688 |
30
+ | 20 | Exception 2 & 9 where the first check fails and second check passes with substitution. | 309070 | 12345668 |
31
+ | 21 | Exception 2 & 9 where a!=0 and g!=9 and passes. | 309070 | 12345677 |
32
+ | 22 | Exception 2 & 9 where a!=0 and g=9 and passes. | 309070 | 99345694 |
33
+ | 31 | Exception 12/13 where passes modulus 11 check | 074456 | 12345112 |
34
+ | 32 | Exception 12/13 where passes the modulus 11 check | 070116 | 34012583 |
35
+ | 33 | Exception 12/13 where fails the modulus 11 check, but passes the modulus 10 check. | 074456 | 11104102 |
36
+ | 34 | Exception 14 where the first check fails and the second check passes. | 180002 | 00000190 |
37
+
38
+ Scenario Outline: Invalid Codes
39
+ Given I have a sort code <sortcode>
40
+ And I have an account number <accountnumber>
41
+
42
+ Then the combination is invalid
43
+
44
+ Examples:
45
+ | test | description | sortcode | accountnumber |
46
+ | 23 | Exception 5 where the first checkdigit is correct and the second incorrect. | 938063 | 15764273 |
47
+ | 24 | Exception 5 where the first checkdigit is incorrect and the second correct. | 938063 | 15764264 |
48
+ | 25 | Exception 5 where the first checkdigit is incorrect with a remainder of 1. | 938063 | 15763217 |
49
+ | 26 | Exception 1 where it fails double alternate check. | 118765 | 64371388 |
@@ -0,0 +1,18 @@
1
+ Feature: Modulus 10 Checking
2
+
3
+ Background:
4
+ Given I create a new checker
5
+
6
+ Scenario: 1) Pass modulus 10 check
7
+ Given I have a sort code 089999
8
+ And I have an account number 66374958
9
+
10
+ Then the modulus is MOD10
11
+ And the combination is valid
12
+
13
+ Scenario: 29) Fail modulus 10 check
14
+ Given I have a sort code 089999
15
+ And I have an account number 66374959
16
+
17
+ Then the modulus is MOD10
18
+ And the combination is invalid
@@ -0,0 +1,15 @@
1
+ Feature: Modulus 11 Checking
2
+
3
+ Background:
4
+ Given I create a new checker
5
+
6
+ Scenario: 2) Pass modulus 11 check
7
+ Given I have a sort code 107999
8
+ And I have an account number 88837491
9
+
10
+ Then the modulus is MOD11
11
+ And the combination is valid
12
+
13
+ Scenario: 30) Fail modulus 11 check
14
+ Given I have a sort code 107999
15
+ And I have an account number 88837493
@@ -0,0 +1,26 @@
1
+ Feature: Modulus Weight
2
+
3
+ Scenario: Reading Weights
4
+ Given I have the following weight data:
5
+ """
6
+ 070116 070116 MOD11 0 0 7 6 5 8 9 4 5 6 7 8 9 -1 12
7
+ """
8
+
9
+ Then the weight's sort_code_start is 070116
10
+ And the weight's sort_code_end is 070116
11
+ And the weight's modulus is MOD11
12
+ And the weight's u is 0
13
+ And the weight's v is 0
14
+ And the weight's w is 7
15
+ And the weight's x is 6
16
+ And the weight's y is 5
17
+ And the weight's z is 8
18
+ And the weight's a is 9
19
+ And the weight's b is 4
20
+ And the weight's c is 5
21
+ And the weight's d is 6
22
+ And the weight's e is 7
23
+ And the weight's f is 8
24
+ And the weight's g is 9
25
+ And the weight's h is -1
26
+ And the weight's exception is 12
@@ -0,0 +1,14 @@
1
+ Feature: Modulus Weight Table
2
+
3
+ Scenario: Finding appropriate modulus weight
4
+ Given I have a modulus weight table loaded from data/valacdos.txt
5
+
6
+ Then the modulus for sort code 010004 is MOD11
7
+ And the modulus for sort code 010005 is MOD11
8
+ And the modulus for sort code 016714 is MOD11
9
+ And the modulus for sort code 016715 is MOD11
10
+
11
+ Scenario: Sort code doesn't exist
12
+ Given I have a modulus weight table loaded from data/valacdos.txt
13
+
14
+ Then the modulus for sort code 020000 cannot be found
@@ -0,0 +1,23 @@
1
+ Given(/^I create a new checker$/) do
2
+ @checker = UkAccountValidator::Validator.new
3
+ end
4
+
5
+ Given(/^I have a sort code (\d+)$/) do |sort_code|
6
+ @checker.sort_code = sort_code
7
+ end
8
+
9
+ Given(/^I have an account number (\d+)$/) do |acc_number|
10
+ @checker.account_number = acc_number
11
+ end
12
+
13
+ Then(/^the modulus is (\S+)$/) do |modulus|
14
+ expect(@checker.modulus_weights.first.modulus).to eq modulus
15
+ end
16
+
17
+ Then(/^the combination is valid$/) do
18
+ expect(@checker.valid?).to be true
19
+ end
20
+
21
+ Then(/^the combination is invalid$/) do
22
+ expect(@checker.valid?).to be false
23
+ end
@@ -0,0 +1,7 @@
1
+ Given(/^I have the following weight data:$/) do |weight_data|
2
+ @weight = UkAccountValidator::ModulusWeight.from_line(weight_data)
3
+ end
4
+
5
+ Then(/^the weight's (\S+) is (\S+)$/) do |arg, value|
6
+ expect(@weight.send(arg).to_s).to eq value
7
+ end
@@ -0,0 +1,13 @@
1
+ Given(/^I have a modulus weight table loaded from (.+?)$/) do |file_path|
2
+ @table = UkAccountValidator::ModulusWeightsTable.new(
3
+ File.join(File.dirname(__FILE__), '../..', file_path)
4
+ )
5
+ end
6
+
7
+ Then(/^the modulus for sort code (\d+) is (\S+)$/) do |sort_code, mod|
8
+ expect(@table.find(sort_code).first.modulus).to eq mod
9
+ end
10
+
11
+ Then(/^the modulus for sort code (\d+) cannot be found$/) do |sort_code|
12
+ expect(@table.find(sort_code)).to match_array([])
13
+ end
@@ -0,0 +1,2 @@
1
+ $LOAD_PATH << File.expand_path('../../lib', File.dirname(__FILE__))
2
+ require 'uk_account_validator'
@@ -0,0 +1,25 @@
1
+ Feature: Two Modulus Checks
2
+
3
+ Background:
4
+ Given I create a new checker
5
+
6
+ Scenario Outline: Valid Codes
7
+ Given I have a sort code <sortcode>
8
+ And I have an account number <accountnumber>
9
+
10
+ Then the combination is valid
11
+
12
+ Examples:
13
+ | test | description | sortcode | accountnumber |
14
+ | 3 | Pass modulus 11 and double alternate checks. | 202959 | 63748472 |
15
+
16
+ Scenario Outline: Invalid Codes
17
+ Given I have a sort code <sortcode>
18
+ And I have an account number <accountnumber>
19
+
20
+ Then the combination is invalid
21
+
22
+ Examples:
23
+ | test | description | sortcode | accountnumber |
24
+ | 27 | Pass modulus 11 check and fail double alternate check. | 203099 | 66831036 |
25
+ | 28 | Fail modulus 11 check and pass double alternate check. | 203099 | 58716970 |
@@ -0,0 +1,41 @@
1
+ require 'uk_account_validator/validator.rb'
2
+ require 'uk_account_validator/modulus_weight.rb'
3
+ require 'uk_account_validator/modulus_weights_table.rb'
4
+
5
+ require 'uk_account_validator/validators/base_validator.rb'
6
+ require 'uk_account_validator/validators/standard_modulus.rb'
7
+ require 'uk_account_validator/validators/modulus10.rb'
8
+ require 'uk_account_validator/validators/modulus11.rb'
9
+ require 'uk_account_validator/validators/double_alternate.rb'
10
+
11
+ module UkAccountValidator
12
+ def self.modulus_weights_table
13
+ @modulus_weights_table ||= read_modulus_weights_table
14
+ end
15
+
16
+ def self.sort_code_substitution
17
+ @sort_code_substitution ||= read_sort_code_substitution
18
+ end
19
+
20
+ private
21
+
22
+ def self.modulus_weights_table_file
23
+ File.join(File.dirname(__FILE__), '../data/valacdos.txt')
24
+ end
25
+
26
+ def self.read_modulus_weights_table
27
+ ModulusWeightsTable.new(modulus_weights_table_file)
28
+ end
29
+
30
+ def self.sort_code_substitution_file
31
+ File.join(File.dirname(__FILE__), '../data/scsubtab.txt')
32
+ end
33
+
34
+ def self.read_sort_code_substitution
35
+ @substitutions ||= Hash[
36
+ File.readlines(UkAccountValidator.sort_code_substitution_file).map do |line|
37
+ line.split(' ')
38
+ end
39
+ ]
40
+ end
41
+ end
@@ -0,0 +1,35 @@
1
+ module UkAccountValidator
2
+ class ModulusWeight
3
+ attr_accessor :sort_code_start, :sort_code_end, :modulus, :u, :v, :w, :x,
4
+ :y, :z, :a, :b, :c, :d, :e, :f, :g, :h, :exception
5
+
6
+ # the size of each column
7
+ COLUMN_SIZES = [6, 7, 9, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 4, 3]
8
+
9
+ # @param definition_line The line from valacdos.txt that defines this weight.
10
+ def self.from_line(definition_line)
11
+ # See https://www.ruby-forum.com/topic/184294#805359
12
+ data = definition_line.unpack("A#{COLUMN_SIZES.join('A')}")
13
+
14
+ data.map! { |d| d.strip }
15
+
16
+ @sort_code_start, @sort_code_end, @modulus, @u, @v, @w, @x,
17
+ @y, @z, @a, @b, @c, @d, @e, @f, @g, @h, @exception = data
18
+
19
+ ModulusWeight.new(*data)
20
+ end
21
+
22
+ def initialize(sort_code_start, sort_code_end, modulus, u, v, w, x, y, z,
23
+ a, b, c, d, e, f, g, h, exception)
24
+
25
+ @sort_code_start = sort_code_start
26
+ @sort_code_end = sort_code_end
27
+ @modulus = modulus
28
+ @exception = exception
29
+
30
+ @u, @v, @w, @x, @y, @z, @a, @b, @c, @d, @e, @f, @g, @h =
31
+ [u.to_i, v.to_i, w.to_i, x.to_i, y.to_i, z.to_i, a.to_i, b.to_i, c.to_i,
32
+ d.to_i, e.to_i, f.to_i, g.to_i, h.to_i]
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,31 @@
1
+ module UkAccountValidator
2
+ class ModulusWeightsTable
3
+
4
+ def initialize(path)
5
+ @weights = []
6
+
7
+ File.readlines(path).each do |line|
8
+ @weights << ModulusWeight.from_line(line)
9
+ end
10
+
11
+ @weights.sort! { |weight| weight.sort_code_start.to_i }
12
+ end
13
+
14
+ def find(sort_code, found_weights = [], exclude = [])
15
+ sort_code = sort_code.to_i
16
+
17
+ weight = @weights.bsearch do |w|
18
+ w.sort_code_start.to_i <= sort_code && !exclude.include?(w)
19
+ end
20
+
21
+ return found_weights if weight.nil?
22
+ return found_weights unless
23
+ weight.sort_code_start.to_i <= sort_code &&
24
+ sort_code <= weight.sort_code_end.to_i
25
+
26
+ found_weights << weight
27
+
28
+ find(sort_code, found_weights, exclude + [weight])
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,60 @@
1
+ module UkAccountValidator
2
+ class Validator
3
+ attr_accessor :account_number, :sort_code
4
+
5
+ def initialize(account_number = nil, sort_code = nil)
6
+ @account_number = account_number
7
+ @sort_code = sort_code
8
+ end
9
+
10
+ def modulus_weights
11
+ @modulus_weights ||= UkAccountValidator.modulus_weights_table.find(sort_code)
12
+ end
13
+
14
+ def validator(modulus)
15
+ case modulus
16
+ when 'MOD10'
17
+ Validators::Modulus10
18
+ when 'MOD11'
19
+ Validators::Modulus11
20
+ when 'DBLAL'
21
+ Validators::DoubleAlternate
22
+ else
23
+ fail NotImplementedError
24
+ end
25
+ end
26
+
27
+ def valid?
28
+ exceptions = modulus_weights.map(&:exception)
29
+
30
+ results = modulus_weights.each_with_index.map do |modulus_weight, i|
31
+ if i == 1 && exceptions.include?('2') && exceptions.include?('9')
32
+ next Validator.new(account_number, '309634').valid?
33
+ end
34
+
35
+ validator(modulus_weight.modulus).new(account_number, sort_code, modulus_weight, exceptions).valid?
36
+ end
37
+
38
+ return results.any? if exceptions.include?('2') && exceptions.include?('9')
39
+ return results.any? if exceptions.include?('10') && exceptions.include?('11')
40
+ return results.any? if exceptions.include?('12') && exceptions.include?('13')
41
+ return apply_exception_14 if !results.any? && exceptions.include?('14')
42
+
43
+ results.all?
44
+ end
45
+
46
+ # If the 8th digit of the account number (reading from left to right) is
47
+ # not 0, 1 or 9 then the account number fails the second check and is not a
48
+ # valid Coutts account number.
49
+ # If the 8th digit is 0, 1 or 9, then remove the digit from the account
50
+ # number and insert a 0 as the 1st digit for check purposes only
51
+ def apply_exception_14
52
+ return false unless %w(0 1 9).include?(account_number[7])
53
+
54
+ account_number.slice!(7)
55
+ exception_account_number = '0' + account_number
56
+
57
+ return Validator.new(exception_account_number, sort_code).valid?
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,154 @@
1
+ module UkAccountValidator
2
+ module Validators
3
+ class BaseValidator
4
+ NUMBER_INDEX = {
5
+ u: 0,
6
+ v: 1,
7
+ w: 2,
8
+ x: 3,
9
+ y: 4,
10
+ z: 5,
11
+ a: 6,
12
+ b: 7,
13
+ c: 8,
14
+ d: 9,
15
+ e: 10,
16
+ f: 11,
17
+ g: 12,
18
+ h: 13
19
+ }
20
+
21
+ def initialize(account_number, sort_code, modulus_weight, exceptions)
22
+ @account_number = account_number
23
+ @sort_code = sort_code
24
+ @modulus_weight = modulus_weight
25
+ @exceptions = exceptions
26
+
27
+ @sort_code = apply_exception_5_substitutions(sort_code) if @modulus_weight.exception == '5'
28
+ end
29
+
30
+ def applying_exceptions(test_digits)
31
+ apply_exception_2_9(test_digits) if @exceptions.include?('2') && @exceptions.include?('9')
32
+ apply_exception_3(test_digits) if @modulus_weight.exception == '3'
33
+ apply_exception_6(test_digits) if @modulus_weight.exception == '6'
34
+ apply_exception_7(test_digits) if @modulus_weight.exception == '7'
35
+ apply_exception_8(test_digits) if @modulus_weight.exception == '8'
36
+ apply_exception_10(test_digits) if @modulus_weight.exception == '10'
37
+
38
+ total = yield
39
+
40
+ total += 27 if @modulus_weight.exception == '1'
41
+
42
+ total
43
+ end
44
+
45
+ def zero_all
46
+ @modulus_weight = ModulusWeight.new(
47
+ @modulus_weight.sort_code_start,
48
+ @modulus_weight.sort_code_end,
49
+ @modulus_weight.modulus,
50
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
51
+ @modulus_weight.exception
52
+ )
53
+ end
54
+
55
+ def zero_u_b
56
+ @modulus_weight = ModulusWeight.new(
57
+ @modulus_weight.sort_code_start,
58
+ @modulus_weight.sort_code_end,
59
+ @modulus_weight.modulus,
60
+ 0, 0, 0, 0, 0, 0, 0, 0,
61
+ @modulus_weight.c, @modulus_weight.d, @modulus_weight.e,
62
+ @modulus_weight.f, @modulus_weight.g, @modulus_weight.h,
63
+ @modulus_weight.exception
64
+ )
65
+ end
66
+
67
+ def apply_exception_2_9(test_digits)
68
+ return if test_digits[NUMBER_INDEX[:a]] == 0
69
+
70
+ if test_digits[NUMBER_INDEX[:g]] != 9
71
+ @modulus_weight = ModulusWeight.new(
72
+ @modulus_weight.sort_code_start,
73
+ @modulus_weight.sort_code_end,
74
+ @modulus_weight.modulus,
75
+ 0, 0, 1, 2, 5, 3, 6, 4, 8, 7, 10, 9, 3, 1,
76
+ @modulus_weight.exception
77
+ )
78
+ else
79
+ @modulus_weight = ModulusWeight.new(
80
+ @modulus_weight.sort_code_start,
81
+ @modulus_weight.sort_code_end,
82
+ @modulus_weight.modulus,
83
+ 0, 0, 0, 0, 0, 0, 0, 0, 8, 7, 10, 9, 3, 1,
84
+ @modulus_weight.exception
85
+ )
86
+ end
87
+ end
88
+
89
+ def apply_exception_3(test_digits)
90
+ c = test_digits[NUMBER_INDEX[:c]]
91
+ zero_all if c == 6 || c == 9
92
+ end
93
+
94
+ def apply_exception_4(total, test_digits)
95
+ check_sum = [test_digits[NUMBER_INDEX[:g]], test_digits[NUMBER_INDEX[:h]]].join
96
+ check_sum = check_sum.to_i
97
+
98
+ total % modulus == check_sum
99
+ end
100
+
101
+ def apply_exception_5_substitutions(sort_code)
102
+ substitutions = UkAccountValidator.read_sort_code_substitution
103
+
104
+ if substitutions.keys.include?(sort_code)
105
+ sort_code = substitutions[sort_code]
106
+ end
107
+
108
+ return sort_code
109
+ end
110
+
111
+ def apply_exception_5(total, test_digits, check_digit)
112
+ check_sum = total % modulus
113
+ expected_sum = test_digits[NUMBER_INDEX[check_digit]].to_i
114
+
115
+ return false if check_sum == 1 && check_digit == :g
116
+ return true if check_sum == 0 && expected_sum == 0
117
+
118
+ return (modulus - check_sum) == expected_sum
119
+ end
120
+
121
+ # If a = 4, 5, 6, 7 or 8, and g and h are the same
122
+ def apply_exception_6(test_digits)
123
+ a = test_digits[NUMBER_INDEX[:a]]
124
+ g = test_digits[NUMBER_INDEX[:g]]
125
+ h = test_digits[NUMBER_INDEX[:h]]
126
+
127
+ zero_all if (a == 4 || a == 5 || a == 6 || a == 7 || a == 8) && g == h
128
+ end
129
+
130
+ def apply_exception_7(test_digits)
131
+ return unless test_digits[NUMBER_INDEX[:g]] == 9
132
+
133
+ zero_u_b
134
+ end
135
+
136
+ def apply_exception_8(test_digits)
137
+ test_string = '090126' + @account_number
138
+
139
+ test_digits.replace(test_string.split(//).map(&:to_i))
140
+ end
141
+
142
+ def apply_exception_10(test_digits)
143
+ # if ab = 09 or 99 and g=9, zeroise weighting positions u-b.
144
+ a = test_digits[NUMBER_INDEX[:a]]
145
+ b = test_digits[NUMBER_INDEX[:b]]
146
+
147
+ return unless (a == 0 && b == 9) ||
148
+ (a == 9 && b == 9 && test_digits[NUMBER_INDEX[:g]] == 9)
149
+
150
+ zero_u_b
151
+ end
152
+ end
153
+ end
154
+ end