uk_account_validator 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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