syrup 0.0.1 → 0.0.2
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.
- data/.gitignore +6 -6
- data/.rspec +2 -1
- data/CHANGELOG.rdoc +2 -2
- data/Gemfile +4 -4
- data/README.rdoc +46 -28
- data/Rakefile +8 -10
- data/TODO.rdoc +15 -0
- data/lib/syrup/account.rb +95 -4
- data/lib/syrup/information_missing_error.rb +4 -0
- data/lib/syrup/institutions/institution_base.rb +111 -0
- data/lib/syrup/institutions/zions_bank.rb +173 -110
- data/lib/syrup/transaction.rb +13 -9
- data/lib/syrup/version.rb +3 -3
- data/lib/syrup.rb +27 -10
- data/spec/spec_helper.rb +15 -25
- data/spec/syrup/account_spec.rb +53 -0
- data/spec/syrup/institutions/institution_base_spec.rb +120 -0
- data/spec/syrup/institutions/zions_bank_spec.rb +25 -4
- data/spec/syrup/syrup_spec.rb +41 -0
- data/spec/syrup/transaction_spec.rb +20 -19
- data/syrup.gemspec +26 -26
- metadata +53 -63
- data/lib/syrup/extract.rb +0 -14
- data/lib/syrup/institutions/abstract_institution.rb +0 -26
- data/spec/syrup/extract_spec.rb +0 -12
data/.gitignore
CHANGED
@@ -1,6 +1,6 @@
|
|
1
|
-
*.swp
|
2
|
-
**/*.swp
|
3
|
-
*.gem
|
4
|
-
.bundle
|
5
|
-
Gemfile.lock
|
6
|
-
pkg/*
|
1
|
+
*.swp
|
2
|
+
**/*.swp
|
3
|
+
*.gem
|
4
|
+
.bundle
|
5
|
+
Gemfile.lock
|
6
|
+
pkg/*
|
data/.rspec
CHANGED
@@ -1 +1,2 @@
|
|
1
|
-
--color
|
1
|
+
--color
|
2
|
+
--format documentation
|
data/CHANGELOG.rdoc
CHANGED
@@ -1,3 +1,3 @@
|
|
1
|
-
0.0.1 (Date)
|
2
|
-
|
1
|
+
0.0.1 (Date)
|
2
|
+
|
3
3
|
* initial release
|
data/Gemfile
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
source "http://rubygems.org"
|
2
|
-
|
3
|
-
# Specify your gem's dependencies in syrup.gemspec
|
4
|
-
gemspec
|
1
|
+
source "http://rubygems.org"
|
2
|
+
|
3
|
+
# Specify your gem's dependencies in syrup.gemspec
|
4
|
+
gemspec
|
data/README.rdoc
CHANGED
@@ -1,28 +1,46 @@
|
|
1
|
-
= Syrup
|
2
|
-
|
3
|
-
Syrup
|
4
|
-
|
5
|
-
==
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
1
|
+
= Syrup
|
2
|
+
|
3
|
+
Syrup helps you to extract bank account information and transactions.
|
4
|
+
|
5
|
+
== Usage
|
6
|
+
|
7
|
+
# Setup an instance of the bank
|
8
|
+
zions_bank = Syrup.setup_institution('zions_bank') do |config|
|
9
|
+
config.username = 'user'
|
10
|
+
config.password = 'pass'
|
11
|
+
config.secret_questions = {'What is your secret question?' => "I don't know"}
|
12
|
+
end
|
13
|
+
|
14
|
+
# List accounts
|
15
|
+
zions_bank.accounts.each do |account|
|
16
|
+
puts "#{account.name} (#{account.current_balance}) # => "Checking (100.0)"
|
17
|
+
end
|
18
|
+
|
19
|
+
# Get transactions
|
20
|
+
account = zions_bank.find_account_by_id 123456
|
21
|
+
transactions = account.find_transactions(Date.today - 30) # => an array of Transactions from the last 30 days
|
22
|
+
transactions = account.find_transactions(Date.parse('2011-01-01'), Date.parse('2011-02-01') - 1) # => an array of Transactions from the month of January
|
23
|
+
|
24
|
+
== Installation
|
25
|
+
|
26
|
+
The latest version of Syrup can be installed with Rubygems:
|
27
|
+
|
28
|
+
[sudo] gem install "syrup"
|
29
|
+
|
30
|
+
In <b>Rails 3</b>, add this to your Gemfile and run the +bundle+ command.
|
31
|
+
|
32
|
+
gem "syrup"
|
33
|
+
|
34
|
+
In <b>Rails 2</b>, add this to your environment.rb file.
|
35
|
+
|
36
|
+
config.gem "syrup"
|
37
|
+
|
38
|
+
== Supported Institutions
|
39
|
+
|
40
|
+
Currently, only Zions Bank[http://zionsbank.com] is supported. I will be
|
41
|
+
implementing UCCU, USAA, and Wells Fargo (probably in that order). If you would
|
42
|
+
like support for a different bank, you have two options:
|
43
|
+
|
44
|
+
1. Get me the credentials to log into an account with that bank (you'd have to
|
45
|
+
trust me).
|
46
|
+
2. Implement it yourself and submit a pull request.
|
data/Rakefile
CHANGED
@@ -1,11 +1,9 @@
|
|
1
|
-
require 'bundler'
|
2
|
-
require 'rspec/core/rake_task'
|
3
|
-
|
4
|
-
Bundler::GemHelper.install_tasks
|
5
|
-
|
6
|
-
desc "Run
|
7
|
-
RSpec::Core::RakeTask.new
|
8
|
-
|
9
|
-
end
|
10
|
-
|
1
|
+
require 'bundler'
|
2
|
+
require 'rspec/core/rake_task'
|
3
|
+
|
4
|
+
Bundler::GemHelper.install_tasks
|
5
|
+
|
6
|
+
desc "Run tests"
|
7
|
+
RSpec::Core::RakeTask.new
|
8
|
+
|
11
9
|
task :default => :spec
|
data/TODO.rdoc
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
|
2
|
+
make sure that mechanize validates SSL certificates
|
3
|
+
|
4
|
+
pass in username, password, secret questions
|
5
|
+
|
6
|
+
zions.fetch_accounts
|
7
|
+
should I store things?
|
8
|
+
List accounts
|
9
|
+
* create an array of Account objects
|
10
|
+
|
11
|
+
account.transactions OR account.fetch_transactions
|
12
|
+
zions.
|
13
|
+
When getting transactions
|
14
|
+
* create an array of Transaction objects
|
15
|
+
* populate as many variables on Account as you can (eg. current_balance, etc.)
|
data/lib/syrup/account.rb
CHANGED
@@ -1,5 +1,96 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
4
|
-
|
1
|
+
require 'date'
|
2
|
+
|
3
|
+
module Syrup
|
4
|
+
class Account
|
5
|
+
# known types are :deposit and :credit
|
6
|
+
attr_accessor :id
|
7
|
+
attr_writer :name, :type, :account_number, :current_balance, :available_balance, :prior_day_balance
|
8
|
+
|
9
|
+
def name
|
10
|
+
populate
|
11
|
+
@name
|
12
|
+
end
|
13
|
+
|
14
|
+
def type
|
15
|
+
populate
|
16
|
+
@type
|
17
|
+
end
|
18
|
+
|
19
|
+
def account_number
|
20
|
+
populate
|
21
|
+
@account_number
|
22
|
+
end
|
23
|
+
|
24
|
+
def current_balance
|
25
|
+
populate
|
26
|
+
@current_balance
|
27
|
+
end
|
28
|
+
|
29
|
+
def available_balance
|
30
|
+
populate
|
31
|
+
@available_balance
|
32
|
+
end
|
33
|
+
|
34
|
+
def prior_day_balance
|
35
|
+
populate
|
36
|
+
@prior_day_balance
|
37
|
+
end
|
38
|
+
|
39
|
+
def initialize(attr_hash = nil)
|
40
|
+
if attr_hash
|
41
|
+
attr_hash.each do |k, v|
|
42
|
+
instance_variable_set "@#{k}", v
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
@cached_transactions = []
|
47
|
+
end
|
48
|
+
|
49
|
+
def populated?
|
50
|
+
@populated
|
51
|
+
end
|
52
|
+
|
53
|
+
def populated=(value)
|
54
|
+
@populated = value
|
55
|
+
end
|
56
|
+
|
57
|
+
def populate
|
58
|
+
unless populated? || @institution.nil?
|
59
|
+
raise "The account id must not be nil when populating an account" if id.nil?
|
60
|
+
@institution.populate_account(id)
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
def ==(other_account)
|
65
|
+
other_account.id == self.id && other_account.is_a?(Account)
|
66
|
+
end
|
67
|
+
|
68
|
+
def find_transactions(starting_at, ending_at = Date.today)
|
69
|
+
return [] if starting_at > ending_at
|
70
|
+
|
71
|
+
@institution.fetch_transactions(self.id, starting_at, ending_at)
|
72
|
+
end
|
73
|
+
|
74
|
+
def merge!(account_with_info)
|
75
|
+
if account_with_info
|
76
|
+
account_with_info.instance_variables.each do |filled_var|
|
77
|
+
self.instance_variable_set(filled_var, account_with_info.instance_variable_get(filled_var))
|
78
|
+
end
|
79
|
+
end
|
80
|
+
self
|
81
|
+
end
|
82
|
+
|
83
|
+
def valid?
|
84
|
+
if @valid.nil?
|
85
|
+
populate
|
86
|
+
@valid = populated?
|
87
|
+
end
|
88
|
+
@valid
|
89
|
+
end
|
90
|
+
|
91
|
+
def valid=(validity)
|
92
|
+
@valid = validity
|
93
|
+
end
|
94
|
+
|
95
|
+
end
|
5
96
|
end
|
@@ -0,0 +1,111 @@
|
|
1
|
+
module Syrup
|
2
|
+
module Institutions
|
3
|
+
class InstitutionBase
|
4
|
+
|
5
|
+
class << self
|
6
|
+
def inherited(subclass)
|
7
|
+
@subclasses ||= []
|
8
|
+
@subclasses << subclass
|
9
|
+
end
|
10
|
+
|
11
|
+
def subclasses
|
12
|
+
@subclasses
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
attr_accessor :username, :password, :secret_questions
|
17
|
+
|
18
|
+
def initialize
|
19
|
+
@accounts = []
|
20
|
+
end
|
21
|
+
|
22
|
+
def setup
|
23
|
+
yield self
|
24
|
+
self
|
25
|
+
end
|
26
|
+
|
27
|
+
def populated?
|
28
|
+
@populated
|
29
|
+
end
|
30
|
+
|
31
|
+
def populated=(value)
|
32
|
+
@populated = value
|
33
|
+
end
|
34
|
+
|
35
|
+
def accounts
|
36
|
+
populate_accounts
|
37
|
+
@accounts
|
38
|
+
end
|
39
|
+
|
40
|
+
def find_account_by_id(account_id)
|
41
|
+
account = @accounts.find { |a| a.id == account_id }
|
42
|
+
unless account || populated?
|
43
|
+
account = Account.new(:id => account_id)
|
44
|
+
@accounts << account
|
45
|
+
end
|
46
|
+
account
|
47
|
+
end
|
48
|
+
|
49
|
+
def populate_account(account_id)
|
50
|
+
unless populated?
|
51
|
+
result = fetch_account(account_id)
|
52
|
+
return nil if result.nil?
|
53
|
+
|
54
|
+
if result.respond_to?(:each)
|
55
|
+
populate_accounts(result)
|
56
|
+
find_account_by_id(account_id)
|
57
|
+
else
|
58
|
+
result.populated = true
|
59
|
+
account = find_account_by_id(account_id)
|
60
|
+
account.merge! result if account
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
def populate_accounts(populated_accounts = nil)
|
66
|
+
unless populated?
|
67
|
+
all_accounts = populated_accounts || fetch_accounts
|
68
|
+
|
69
|
+
# Remove any accounts that were added, that don't actually exist
|
70
|
+
@accounts.keep_if do |a|
|
71
|
+
if all_accounts.include?(a)
|
72
|
+
true
|
73
|
+
else
|
74
|
+
a.valid = false
|
75
|
+
false
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
# Add any additional account information
|
80
|
+
new_accounts = []
|
81
|
+
all_accounts.each do |filled_account|
|
82
|
+
account = @accounts.find { |a| a.id == filled_account.id }
|
83
|
+
|
84
|
+
filled_account.populated = true
|
85
|
+
|
86
|
+
# If we already had an account with this id, fill it with data
|
87
|
+
if account
|
88
|
+
account.merge! filled_account
|
89
|
+
else
|
90
|
+
new_accounts << filled_account
|
91
|
+
end
|
92
|
+
end
|
93
|
+
@accounts |= new_accounts # Uses set union
|
94
|
+
|
95
|
+
self.populated = true
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
protected
|
100
|
+
|
101
|
+
def agent
|
102
|
+
@agent ||= Mechanize.new
|
103
|
+
end
|
104
|
+
|
105
|
+
def parse_currency(currency)
|
106
|
+
currency.scan(/[0-9.]/).join.to_f
|
107
|
+
end
|
108
|
+
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
@@ -1,111 +1,174 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
json['accountBalance']['
|
30
|
-
new_account = Account.new
|
31
|
-
new_account.name = account['name']
|
32
|
-
new_account.
|
33
|
-
new_account.
|
34
|
-
new_account.
|
35
|
-
new_account.type = :
|
36
|
-
|
37
|
-
accounts << new_account
|
38
|
-
end
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
1
|
+
require 'date'
|
2
|
+
|
3
|
+
module Syrup
|
4
|
+
module Institutions
|
5
|
+
class ZionsBank < InstitutionBase
|
6
|
+
|
7
|
+
class << self
|
8
|
+
def name
|
9
|
+
"Zions Bank"
|
10
|
+
end
|
11
|
+
|
12
|
+
def id
|
13
|
+
"zions_bank"
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def fetch_account(account_id)
|
18
|
+
fetch_accounts
|
19
|
+
end
|
20
|
+
|
21
|
+
def fetch_accounts
|
22
|
+
ensure_authenticated
|
23
|
+
|
24
|
+
# List accounts
|
25
|
+
page = agent.get('https://banking.zionsbank.com/ibuir/displayAccountBalance.htm')
|
26
|
+
json = ActiveSupport::JSON.decode(page.body)
|
27
|
+
|
28
|
+
accounts = []
|
29
|
+
json['accountBalance']['depositAccountList'].each do |account|
|
30
|
+
new_account = Account.new(:id => account['accountId'])
|
31
|
+
new_account.name = account['name']
|
32
|
+
new_account.account_number = account['number']
|
33
|
+
new_account.current_balance = parse_currency(account['currentAmt'])
|
34
|
+
new_account.available_balance = parse_currency(account['availableAmt'])
|
35
|
+
new_account.type = :deposit
|
36
|
+
|
37
|
+
accounts << new_account
|
38
|
+
end
|
39
|
+
json['accountBalance']['creditAccountList'].each do |account|
|
40
|
+
new_account = Account.new(:id => account['accountId'])
|
41
|
+
new_account.name = account['name']
|
42
|
+
new_account.account_number = account['number']
|
43
|
+
new_account.current_balance = parse_currency(account['balanceDueAmt'])
|
44
|
+
new_account.type = :credit
|
45
|
+
|
46
|
+
accounts << new_account
|
47
|
+
end
|
48
|
+
|
49
|
+
accounts
|
50
|
+
end
|
51
|
+
|
52
|
+
def fetch_transactions(account_id, starting_at, ending_at)
|
53
|
+
ensure_authenticated
|
54
|
+
|
55
|
+
transactions = []
|
56
|
+
|
57
|
+
post_vars = { "actAcct" => account_id, "dayRange.searchType" => "dates", "dayRange.startDate" => starting_at.strftime('%m/%d/%Y'), "dayRange.endDate" => ending_at.strftime('%m/%d/%Y'), "submit_view.x" => 11, "submit_view.y" => 11, "submit_view" => "view" }
|
58
|
+
|
59
|
+
page = agent.post("https://banking.zionsbank.com/zfnb/userServlet/app/bank/user/register_view_main?reSort=false&actAcct=#{account_id}", post_vars)
|
60
|
+
|
61
|
+
page.search('tr').each do |row_element|
|
62
|
+
data = []
|
63
|
+
datapart = row_element.css('.data')
|
64
|
+
if datapart
|
65
|
+
data += datapart
|
66
|
+
datapart = row_element.css('.curr')
|
67
|
+
data += datapart if datapart
|
68
|
+
end
|
69
|
+
|
70
|
+
datapart = row_element.css('.datagrey')
|
71
|
+
if datapart
|
72
|
+
data += datapart
|
73
|
+
datapart = row_element.css('.currgrey')
|
74
|
+
data += datapart if datapart
|
75
|
+
end
|
76
|
+
|
77
|
+
if data.size == 7
|
78
|
+
data.map! {|cell| cell.inner_html.strip.gsub(/[^ -~]/, '') }
|
79
|
+
|
80
|
+
transaction = Transaction.new
|
81
|
+
|
82
|
+
transaction.posted_at = Date.strptime(data[0], '%m/%d/%Y')
|
83
|
+
transaction.payee = data[2]
|
84
|
+
transaction.status = data[3].include?("Posted") ? :posted : :pending
|
85
|
+
unless data[4].empty?
|
86
|
+
transaction.amount = -parse_currency(data[4])
|
87
|
+
end
|
88
|
+
unless data[5].empty?
|
89
|
+
transaction.amount = parse_currency(data[5])
|
90
|
+
end
|
91
|
+
|
92
|
+
transactions << transaction
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
# https://banking.zionsbank.com/zfnb/userServlet/app/bank/user/register_view_main?actAcct=498282&sortBy=Default&sortOrder=Default
|
97
|
+
# actAcct is the accountId (498282)
|
98
|
+
# dayRange.startDate, dayRange.endDate
|
99
|
+
# dayRange.searchType (dates or days, dates uses dayRange.startDate and dayRange.endDate, days uses dayRange.numberOfDays)
|
100
|
+
|
101
|
+
# The transactions table is messy. Cells we want either have data, curr, datagrey, or currgrey css class
|
102
|
+
# 1. The date (initiated or cleared? they're generally the same)
|
103
|
+
# 2. The type of transaction (Debit, Transfer Debit, ATM Debit, Deposit 3785596). This may be irrelevant because of the position of the transaction amount.)
|
104
|
+
# 3. The payee
|
105
|
+
# 4. The transaction status (Posted or ... Pending?)
|
106
|
+
# 5. The debit amount
|
107
|
+
# 6. The deposit amount
|
108
|
+
# 7. The then-current account balance
|
109
|
+
|
110
|
+
transactions
|
111
|
+
end
|
112
|
+
|
113
|
+
private
|
114
|
+
|
115
|
+
def ensure_authenticated
|
116
|
+
|
117
|
+
# Check to see if already authenticated
|
118
|
+
page = agent.get('https://banking.zionsbank.com/ibuir')
|
119
|
+
if page.body.include?("SessionTimeOutException")
|
120
|
+
|
121
|
+
raise InformationMissingError, "Please supply a username" unless self.username
|
122
|
+
raise InformationMissingError, "Please supply a password" unless self.password
|
123
|
+
|
124
|
+
@agent = Mechanize.new
|
125
|
+
|
126
|
+
# Enter the username
|
127
|
+
page = agent.get('https://zionsbank.com')
|
128
|
+
form = page.form('logonForm')
|
129
|
+
form.publicCred1 = username
|
130
|
+
page = form.submit
|
131
|
+
|
132
|
+
# If the supplied username is incorrect, raise an exception
|
133
|
+
raise "Invalid username" if page.title == "Error Page"
|
134
|
+
|
135
|
+
# Go on to the next page
|
136
|
+
page = page.links.first.click
|
137
|
+
|
138
|
+
# Find the secret question
|
139
|
+
question = page.search('div.form_field')[2].css('div').inner_text
|
140
|
+
|
141
|
+
# If the answer to this question was not supplied, raise an exception
|
142
|
+
raise InformationMissingError, "Please answer the question, \"#{question}\"" unless secret_questions[question]
|
143
|
+
|
144
|
+
# Enter the answer to the secret question
|
145
|
+
form = page.forms.first
|
146
|
+
form["challengeEntry.answerText"] = secret_questions[question]
|
147
|
+
form.radiobutton_with(:value => 'false').check
|
148
|
+
submit_button = form.button_with(:name => '_eventId_submit')
|
149
|
+
page = form.submit(submit_button)
|
150
|
+
|
151
|
+
# If the supplied answer is incorrect, raise an exception
|
152
|
+
raise InformationMissingError, "\"#{secret_questions[question]}\" is not the correct answer to, \"#{question}\"" unless page.search('#errorComponent').empty?
|
153
|
+
|
154
|
+
# Enter the password
|
155
|
+
form = page.forms.first
|
156
|
+
form.privateCred1 = password
|
157
|
+
submit_button = form.button_with(:name => '_eventId_submit')
|
158
|
+
page = form.submit(submit_button)
|
159
|
+
|
160
|
+
# If the supplied password is incorrect, raise an exception
|
161
|
+
raise InformationMissingError, "An invalid password was supplied" unless page.search('#errorComponent').empty?
|
162
|
+
|
163
|
+
# Clicking this link logs us into the banking.zionsbank.com domain
|
164
|
+
page = page.links.first.click
|
165
|
+
|
166
|
+
raise "Unknown URL reached. Try logging in manually through a browser." if page.uri.to_s != "https://banking.zionsbank.com/ibuir/displayUserInterface.htm"
|
167
|
+
end
|
168
|
+
|
169
|
+
true
|
170
|
+
end
|
171
|
+
|
172
|
+
end
|
173
|
+
end
|
111
174
|
end
|