syrup 0.0.13 → 0.0.14

Sign up to get free protection for your applications and to get access to all the features.
data/CHANGELOG.rdoc CHANGED
@@ -1,3 +1,7 @@
1
+ 0.0.14 (Jun 6, 2013)
2
+
3
+ * support new Zions Bank website
4
+
1
5
  0.0.3 (Jun 29, 2011)
2
6
 
3
7
  * removed activesupport dependency in favor of multi_json
@@ -6,4 +10,4 @@
6
10
 
7
11
  0.0.1
8
12
 
9
- * initial release
13
+ * initial release
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- syrup (0.0.12)
4
+ syrup (0.0.13)
5
5
  mechanize (>= 1.0.0)
6
6
  multi_json (>= 1.0.3)
7
7
 
@@ -9,50 +9,51 @@ GEM
9
9
  remote: http://rubygems.org/
10
10
  specs:
11
11
  columnize (0.3.6)
12
- diff-lcs (1.1.3)
13
- domain_name (0.5.3)
14
- unf (~> 0.0.3)
15
- linecache (0.46)
16
- rbx-require-relative (> 0.0.4)
17
- mechanize (2.4)
12
+ debugger (1.6.0)
13
+ columnize (>= 0.3.1)
14
+ debugger-linecache (~> 1.2.0)
15
+ debugger-ruby_core_source (~> 1.2.1)
16
+ debugger-linecache (1.2.0)
17
+ debugger-ruby_core_source (1.2.2)
18
+ diff-lcs (1.2.4)
19
+ domain_name (0.5.11)
20
+ unf (>= 0.0.5, < 1.0.0)
21
+ http-cookie (1.0.1)
22
+ domain_name (~> 0.5)
23
+ mechanize (2.7.1)
18
24
  domain_name (~> 0.5, >= 0.5.1)
25
+ http-cookie (~> 1.0.0)
19
26
  mime-types (~> 1.17, >= 1.17.2)
20
27
  net-http-digest_auth (~> 1.1, >= 1.1.1)
21
28
  net-http-persistent (~> 2.5, >= 2.5.2)
22
29
  nokogiri (~> 1.4)
23
30
  ntlm-http (~> 0.1, >= 0.1.1)
24
- webrobots (~> 0.0, >= 0.0.9)
25
- mime-types (1.18)
26
- multi_json (1.3.2)
27
- net-http-digest_auth (1.2)
28
- net-http-persistent (2.6)
29
- nokogiri (1.5.2)
31
+ webrobots (>= 0.0.9, < 0.2)
32
+ mime-types (1.23)
33
+ multi_json (1.7.4)
34
+ net-http-digest_auth (1.3)
35
+ net-http-persistent (2.8)
36
+ nokogiri (1.5.9)
30
37
  ntlm-http (0.1.1)
31
- rake (0.9.2.2)
32
- rbx-require-relative (0.0.9)
33
- rspec (2.9.0)
34
- rspec-core (~> 2.9.0)
35
- rspec-expectations (~> 2.9.0)
36
- rspec-mocks (~> 2.9.0)
37
- rspec-core (2.9.0)
38
- rspec-expectations (2.9.1)
39
- diff-lcs (~> 1.1.3)
40
- rspec-mocks (2.9.0)
41
- ruby-debug (0.10.4)
42
- columnize (>= 0.1)
43
- ruby-debug-base (~> 0.10.4.0)
44
- ruby-debug-base (0.10.4)
45
- linecache (>= 0.3)
46
- unf (0.0.5)
38
+ rake (10.0.4)
39
+ rspec (2.13.0)
40
+ rspec-core (~> 2.13.0)
41
+ rspec-expectations (~> 2.13.0)
42
+ rspec-mocks (~> 2.13.0)
43
+ rspec-core (2.13.1)
44
+ rspec-expectations (2.13.0)
45
+ diff-lcs (>= 1.1.3, < 2.0)
46
+ rspec-mocks (2.13.1)
47
+ unf (0.1.1)
47
48
  unf_ext
48
- unf_ext (0.0.4)
49
- webrobots (0.0.13)
49
+ unf_ext (0.0.6)
50
+ webrobots (0.1.1)
50
51
 
51
52
  PLATFORMS
52
53
  ruby
53
54
 
54
55
  DEPENDENCIES
56
+ debugger
55
57
  rake
56
58
  rspec (>= 2.6.0)
57
- ruby-debug
58
59
  syrup!
data/README.rdoc CHANGED
@@ -37,10 +37,11 @@ In <b>Rails 2</b>, add this to your environment.rb file.
37
37
 
38
38
  == Supported Institutions
39
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:
40
+ Currently, only Zions Bank, UCCU, and Bank of American Fork are supported. I
41
+ will be implementing UCCU, USAA, and Wells Fargo (probably in that order). If
42
+ you would like support for a different bank, you have two options:
43
43
 
44
44
  1. Get me the credentials to log into an account with that bank (you'd have to
45
45
  trust me).
46
- 2. Implement it yourself and submit a pull request. See {Adding Support For Another Institution}[https://github.com/dontangg/syrup/wiki/Adding-Support-For-Another-Institution]
46
+ 2. Implement it yourself and submit a pull request. See
47
+ {Adding Support For Another Institution}[https://github.com/dontangg/syrup/wiki/Adding-Support-For-Another-Institution]
data/TODO.rdoc CHANGED
@@ -1,6 +1,4 @@
1
1
 
2
- Make sure that mechanize validates SSL certificates
3
-
4
2
  When getting transactions
5
3
  * populate as many variables on Account as you can (eg. current_balance, etc.)
6
4
 
@@ -0,0 +1,180 @@
1
+ require 'date'
2
+ require 'bigdecimal'
3
+ require 'bigdecimal/util'
4
+ require 'csv'
5
+
6
+ module Syrup
7
+ module Institutions
8
+
9
+ class BankAf < InstitutionBase
10
+
11
+ class << self
12
+ def name
13
+ "Bank of American Fork"
14
+ end
15
+
16
+ def id
17
+ "bank_af"
18
+ end
19
+ end
20
+
21
+ def fetch_account(account_id)
22
+ fetch_accounts
23
+ end
24
+
25
+ def fetch_accounts
26
+ ensure_authenticated
27
+
28
+ # List accounts
29
+ page = agent.get('https://cm.netteller.com/login2008/Views/Retail/AccountListing.aspx')
30
+
31
+ accounts = []
32
+ page.search('#ctl00_PageContent_ctl00__acctsBase__depositsTab__depositsGrid tr').each do |row_element|
33
+ next if row_element["class"] == "th"
34
+
35
+ cells = row_element.css('td')
36
+
37
+ new_account = Account.new(:institution => self)
38
+ new_account.id = cells[1].inner_text.strip
39
+ new_account.name = new_account.id
40
+ new_account.available_balance = BigDecimal.new(parse_currency(cells[2].inner_text))
41
+ # new_account.current_balance =
42
+ # new_account.account_number =
43
+ # new_account.type = :deposit # :credit
44
+
45
+ accounts << new_account
46
+ end
47
+
48
+ accounts
49
+ end
50
+
51
+ def write_page(page, unique)
52
+ File.open("test#{unique}.html", 'w') do |f|
53
+ f.write page.uri.to_s
54
+ f.write page.body
55
+ end
56
+ end
57
+
58
+ def get_event_target(html)
59
+ match = /doPostBack\('([^'"\\]+)'/.match(html)
60
+ match[1].gsub(/%24/, '$')
61
+ end
62
+
63
+ def fetch_transactions(account_id, starting_at, ending_at)
64
+ ensure_authenticated
65
+
66
+ # Get the accounts page and click on the desired account link
67
+ page = agent.get('https://cm.netteller.com/login2008/Views/Retail/AccountListing.aspx')
68
+
69
+ form = page.form('aspnetForm')
70
+ event_target = nil
71
+ page.search('#ctl00_PageContent_ctl00__acctsBase__depositsTab__depositsGrid tr').each do |row_element|
72
+ next if row_element["class"] == "th"
73
+
74
+ cells = row_element.css('td')
75
+
76
+ if cells[1].inner_text.strip == account_id
77
+ event_target = cells[4].css('select')[0]["name"]
78
+ end
79
+ end
80
+ raise InformationMissingError, "Invalid account ID: #{account_id}" unless event_target
81
+ form["__EVENTTARGET"] = event_target
82
+ form.field_with(:name => 'ctl00$PageContent$ctl00$_acctsBase$_depositsTab$_depositsGrid$ctl02$_activity').option_with(:value => "TransactionDownloadViewAction").select
83
+ page = form.submit
84
+
85
+ # Tranferring to Coldfusion
86
+ form = page.forms[0]
87
+ form.action = "https://www.netteller.com/bankaf/hbProcessRequest.cfm?activity=D"
88
+ page = form.submit
89
+
90
+ # Submit the download request form
91
+ form = page.forms[0]
92
+ form.field_with(:name => 'AccountIndex').option_with(:text => account_id).select
93
+ form.field_with(:name => 'trans').option_with(:value => 'BetweenTwoDates').select
94
+ form.field_with(:name => 'format').option_with(:value => 'QFX').select
95
+ form["from"] = starting_at.strftime('%m/%d/%Y')
96
+ form["to"] = ending_at.strftime('%m/%d/%Y')
97
+ submit_button = form.button_with(:id => 'submitButton')
98
+ page = form.submit(submit_button)
99
+
100
+ page = page.link_with(:href => /DeliverContent/).click
101
+
102
+ # Get the transactions!
103
+ transactions = []
104
+ account = find_account_by_id(account_id)
105
+ page.body.each_line do |line|
106
+ line.strip!
107
+
108
+ if line.start_with?("<STMTTRN>")
109
+ match = /DTPOSTED>(\d+)<TRNAMT>\s?([0-9.-]+).*NAME>([^<]+)/.match(line)
110
+ txn = Transaction.new
111
+
112
+ txn.posted_at = Date.strptime(match[1][0..7], '%Y%m%d')
113
+ txn.amount = parse_currency(match[2])
114
+ txn.payee = match[3].strip
115
+ txn.status = :posted
116
+
117
+ transactions << txn
118
+ elsif line.start_with?("</BANKTRANLIST>")
119
+ match = /LEDGERBAL><BALAMT>([0-9.-]+).*AVAILBAL><BALAMT>([0-9.-]+)/.match(line)
120
+ account.name = account.id
121
+ account.current_balance = match[1].to_d
122
+ account.available_balance = match[2].to_d
123
+ end
124
+ end
125
+
126
+ transactions
127
+ end
128
+
129
+ private
130
+
131
+ def ensure_authenticated
132
+
133
+ # Check to see if already authenticated
134
+ page = agent.get('https://cm.netteller.com/login2008/Views/Retail/AccountListing.aspx')
135
+ if page.body.include?("An Error Occurred While Processing Your Request")
136
+
137
+ raise InformationMissingError, "Please supply a username" unless self.username
138
+ raise InformationMissingError, "Please supply a password" unless self.password
139
+
140
+ # Enter the username and password
141
+ login_vars = { 'ID' => username, 'PIN' => password }
142
+ page = agent.post('https://cm.netteller.com/login2008/Authentication/Views/Login.aspx?fi=bankaf&bn=9de8ca724dd43418&burlid=dc1ba449ca4ad5c0', login_vars)
143
+
144
+ # If the supplied username/password is incorrect, raise an exception
145
+ raise InformationMissingError, "Invalid username or password" if page.body.include?("Invalid Online Banking ID or Password")
146
+
147
+ form = page.forms[0]
148
+ form["ctl00$PageContent$DevicePrintHiddenField"] = "version=1&pm_fpua=mozilla/5.0 (macintosh; intel mac os x 10.7; rv:11.0) gecko/20100101 firefox/11.0|5.0 (Macintosh)|MacIntel&pm_fpsc=24|1280|800|774&pm_fpsw=&pm_fptz=-6&pm_fpln=lang=en-US|syslang=|userlang=&pm_fpjv=1&pm_fpco=1"
149
+ page = form.submit
150
+
151
+ if page.uri.to_s == "https://cm.netteller.com/login2008/Authentication/Views/ChallengeQuestions.aspx"
152
+ form = page.forms[0]
153
+ question1 = page.search('#ctl00_PageContent_Question1Label').inner_text
154
+ form["ctl00$PageContent$Answer1TextBox"] = secret_questions[question1]
155
+
156
+ question2 = page.search('#ctl00_PageContent_Question2Label').inner_text
157
+ form["ctl00$PageContent$Answer2TextBox"] = secret_questions[question2]
158
+
159
+ submit_button = form.button_with(:name => 'ctl00$PageContent$SubmitButton')
160
+
161
+ page = form.submit(submit_button)
162
+
163
+ # TODO: What if the secret questions' answers were incorrect
164
+ #write_page(page, 'sq')
165
+ end
166
+
167
+ form = page.forms[0]
168
+ form.action = "https://cm.netteller.com/login2008/Default.aspx"
169
+ page = form.submit
170
+
171
+ # TODO: find a better way to test success
172
+ raise "Unknown URL reached. Try logging in manually through a browser." if page.uri.to_s != "https://cm.netteller.com/login2008/Views/Retail/AccountListing.aspx"
173
+ end
174
+
175
+ true
176
+ end
177
+
178
+ end
179
+ end
180
+ end
@@ -1,9 +1,7 @@
1
-
2
- <!-- saved from url=(0033)http://curl.haxx.se/ca/cacert.pem -->
3
- <html><head><meta http-equiv="Content-Type" content="text/html; charset=ISO-8859-1"></head><body><pre style="word-wrap: break-word; white-space: pre-wrap;">##
1
+ ##
4
2
  ## ca-bundle.crt -- Bundle of CA Root Certificates
5
3
  ##
6
- ## Certificate data from Mozilla as of: Sun Feb 19 04:03:37 2012
4
+ ## Certificate data from Mozilla as of: Wed Apr 25 15:02:13 2012
7
5
  ##
8
6
  ## This is a bundle of X.509 certificates of public Certificate Authorities
9
7
  ## (CA). These were automatically extracted from Mozilla's root certificates
@@ -16,42 +14,7 @@
16
14
  ## Just configure this file as the SSLCACertificateFile.
17
15
  ##
18
16
 
19
- # ***** BEGIN LICENSE BLOCK *****
20
- # Version: MPL 1.1/GPL 2.0/LGPL 2.1
21
- #
22
- # The contents of this file are subject to the Mozilla Public License Version
23
- # 1.1 (the "License"); you may not use this file except in compliance with
24
- # the License. You may obtain a copy of the License at
25
- # http://www.mozilla.org/MPL/
26
- #
27
- # Software distributed under the License is distributed on an "AS IS" basis,
28
- # WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
29
- # for the specific language governing rights and limitations under the
30
- # License.
31
- #
32
- # The Original Code is the Netscape security libraries.
33
- #
34
- # The Initial Developer of the Original Code is
35
- # Netscape Communications Corporation.
36
- # Portions created by the Initial Developer are Copyright (C) 1994-2000
37
- # the Initial Developer. All Rights Reserved.
38
- #
39
- # Contributor(s):
40
- #
41
- # Alternatively, the contents of this file may be used under the terms of
42
- # either the GNU General Public License Version 2 or later (the "GPL"), or
43
- # the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
44
- # in which case the provisions of the GPL or the LGPL are applicable instead
45
- # of those above. If you wish to allow use of your version of this file only
46
- # under the terms of either the GPL or the LGPL, and not to allow others to
47
- # use your version of this file under the terms of the MPL, indicate your
48
- # decision by deleting the provisions above and replace them with the notice
49
- # and other provisions required by the GPL or the LGPL. If you do not delete
50
- # the provisions above, a recipient may use your version of this file under
51
- # the terms of any one of the MPL, the GPL or the LGPL.
52
- #
53
- # ***** END LICENSE BLOCK *****
54
- # @(#) $RCSfile: certdata.txt,v $ $Revision: 1.82 $ $Date: 2012/02/18 21:41:46 $
17
+ # @(#) $RCSfile: certdata.txt,v $ $Revision: 1.83 $ $Date: 2012/04/25 14:49:29 $
55
18
 
56
19
  GTE CyberTrust Global Root
57
20
  ==========================
@@ -3366,4 +3329,3 @@ l+axofjk70YllJyJ22k4vuxcDlbHZVHlUIiIv0LVKz3l+bqeLrPK9HOSAgu+TGbrIP65y7WZf+a2
3366
3329
  E/rKS03Z7lNGBjvGTq2TWoF+bCpLagVFjPIhpDGQh2xlnJ2lYJU6Un/10asIbvPuW/mIPX64b24D
3367
3330
  5EI=
3368
3331
  -----END CERTIFICATE-----
3369
- </pre></body></html>
@@ -140,7 +140,7 @@ module Syrup
140
140
 
141
141
  # Provide path to cert bundle for Windows
142
142
  # Downloaded from http://curl.haxx.se/ca/
143
- @agent.agent.http.ca_file = File.expand_path(File.dirname(__FILE__) + "/cacert.pem") if RUBY_PLATFORM =~ /mingw|mswin/i
143
+ @agent.agent.http.ca_file = File.expand_path(File.dirname(__FILE__) + "/cacert.pem")
144
144
  end
145
145
 
146
146
  @agent
@@ -151,7 +151,7 @@ module Syrup
151
151
  #
152
152
  # parse_currency('$ 1,234.56') #=> 1234.56
153
153
  def parse_currency(currency)
154
- BigDecimal.new(currency.gsub(/[^0-9.]/, ''))
154
+ BigDecimal.new(currency.gsub(/[^0-9.-]/, ''))
155
155
  end
156
156
 
157
157
  # A helper method that replaces a few HTML entities with their actual characters
@@ -3,186 +3,213 @@ require 'date'
3
3
  module Syrup
4
4
  module Institutions
5
5
  class ZionsBank < InstitutionBase
6
-
6
+
7
7
  class << self
8
8
  def name
9
9
  "Zions Bank"
10
10
  end
11
-
11
+
12
12
  def id
13
13
  "zions_bank"
14
14
  end
15
15
  end
16
-
16
+
17
17
  def fetch_account(account_id)
18
18
  fetch_accounts
19
19
  end
20
-
21
- def fetch_accounts
22
- ensure_authenticated
23
20
 
21
+ def fetch_accounts
22
+ # TODO: If I ever care about this, I'll add it in later. This is the url to grab from:
23
+ # https://banking.zionsbank.com/olb/retail/protected/myBank/balances?OWASP_CSRFTOKEN=
24
+ #
25
+ #ensure_authenticated
26
+ #
24
27
  # List accounts
25
- page = agent.get('https://banking.zionsbank.com/ibuir/displayAccountBalance.htm')
26
- json = MultiJson.load(page.body)
27
-
28
- accounts = []
29
- json['accountBalance']['depositAccountList'].each do |account|
30
- new_account = Account.new(:id => account['accountId'], :institution => self)
31
- new_account.name = unescape_html(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'], :institution => self)
41
- new_account.name = unescape_html(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
28
+ #page = agent.get('https://banking.zionsbank.com/ibuir/displayAccountBalance.htm')
29
+ #json = MultiJson.load(page.body)
48
30
 
49
- accounts
31
+ #accounts = []
32
+ #json['accountBalance']['depositAccountList'].each do |account|
33
+ # new_account = Account.new(:id => account['accountId'], :institution => self)
34
+ # new_account.name = unescape_html(account['name'])
35
+ # new_account.account_number = account['number']
36
+ # new_account.current_balance = parse_currency(account['currentAmt'])
37
+ # new_account.available_balance = parse_currency(account['availableAmt'])
38
+ # new_account.type = :deposit
39
+
40
+ # accounts << new_account
41
+ #end
42
+ #json['accountBalance']['creditAccountList'].each do |account|
43
+ # new_account = Account.new(:id => account['accountId'], :institution => self)
44
+ # new_account.name = unescape_html(account['name'])
45
+ # new_account.account_number = account['number']
46
+ # new_account.current_balance = parse_currency(account['balanceDueAmt'])
47
+ # new_account.type = :credit
48
+
49
+ # accounts << new_account
50
+ #end
51
+
52
+ #accounts
53
+
54
+ []
50
55
  end
51
-
56
+
52
57
  def fetch_transactions(account_id, starting_at, ending_at)
53
58
  ensure_authenticated
54
-
59
+
55
60
  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
- # Get all the transactions
62
- page.search('tr').each do |row_element|
63
- # Look for the account information first
64
- account = find_account_by_id(account_id)
65
- datapart = row_element.css('.acct')
66
- if datapart && datapart.inner_html.size > 0
67
- if match = /Prior Day Balance:\s*([^<]+)/.match(datapart.inner_html)
61
+
62
+ act_oid, act_attr = account_id.split('|')
63
+
64
+ url = "https://banking.zionsbank.com/olb/retail/protected/account/register/account?attr=#{act_attr}&#{@csrf}"
65
+ page = agent.get(url)
66
+
67
+ form = page.forms.first
68
+ form.action += "?#{@csrf}" unless form.action.include?(@csrf)
69
+ form["accountOid"] = act_oid
70
+ form["searchBy"] = "DR"
71
+ form['fromDate'] = starting_at.strftime('%m/%d/%Y')
72
+ form['toDate'] = ending_at.strftime('%m/%d/%Y')
73
+ submit_button = form.button_with(:id => 'formbutton')
74
+ page = form.submit(submit_button)
75
+
76
+ # Look for the account information first
77
+ account = find_account_by_id(account_id)
78
+ page.search('#subCell').first.element_children.each do |element|
79
+ if element.name == "div"
80
+ if match = /Prior Day Balance:\s*\$([0-9.,]+)/.match(element.inner_text)
68
81
  account.prior_day_balance = parse_currency(match[1])
69
- end
70
- if match = /Current Balance:\s*([^<]+)/.match(datapart.inner_html)
82
+ elsif match = /Current Balance:\s*\$([0-9.,]+)/.match(element.inner_text)
71
83
  account.current_balance = parse_currency(match[1])
72
- end
73
- if match = /Available Balance:\s*([^<]+)/.match(datapart.inner_html)
84
+ elsif match = /Available Balance:\s*\$([0-9.,]+)/.match(element.inner_text)
74
85
  account.available_balance = parse_currency(match[1])
75
86
  end
76
87
  end
77
-
78
- data = []
79
- datapart = row_element.css('.data')
80
- if datapart
81
- data += datapart
82
- datapart = row_element.css('.curr')
83
- data += datapart if datapart
84
- end
85
-
86
- datapart = row_element.css('.datagrey')
87
- if datapart
88
- data += datapart
89
- datapart = row_element.css('.currgrey')
90
- data += datapart if datapart
88
+ end
89
+
90
+ # Get all the transactions
91
+ include_pending = true
92
+ begin
93
+ transactions += get_transactions_from_page(page, include_pending)
94
+
95
+ has_next_page = false
96
+ pagination_div = page.search('#pagination').first
97
+ if pagination_div # if #pagination isn't there, then there aren't any more pages
98
+ next_page_link = pagination_div.search('.prevnext').last
99
+ if next_page_link.inner_text.strip.downcase == 'next'
100
+ url = "#{next_page_link['href']}&#{@csrf}"
101
+ page = agent.get(url)
102
+ has_next_page = true
103
+ include_pending = false # The pending txns are included on every page, so don't get them again when we switch pages
104
+ end
91
105
  end
92
-
93
- if data.size == 7
94
- data.map! {|cell| cell.inner_html.strip.gsub(/[^ -~]/, '') }
95
-
106
+ end while has_next_page
107
+
108
+ transactions
109
+ end
110
+
111
+ def get_transactions_from_page(page, include_pending)
112
+ transactions = []
113
+
114
+ page.search('#RegisterCntBox .list_table tr').each do |row_element|
115
+
116
+ date_cell = row_element.search('.table_column_0').first
117
+ if date_cell
118
+ status_image = row_element.search('.table_column_3 img').first
119
+ status = status_image['alt'] == 'Cleared' ? :posted : :pending
120
+ next unless status == :posted || include_pending
121
+
96
122
  transaction = Transaction.new
97
123
 
98
- transaction.posted_at = Date.strptime(data[0], '%m/%d/%Y')
99
- transaction.payee = unescape_html(data[2])
100
- transaction.status = data[3].include?("Posted") ? :posted : :pending
101
- unless data[4].empty?
102
- transaction.amount = -parse_currency(data[4])
124
+ transaction.posted_at = Date.strptime(date_cell.inner_text.strip, '%m/%d/%Y')
125
+
126
+ payee_cell = row_element.search('.table_column_2 .printdisplay .changeText').first || row_element.search('.table_column_2').first
127
+ transaction.payee = payee_cell.inner_text.strip
128
+
129
+ transaction.status = status
130
+
131
+ debit_amount_cell = row_element.search('.table_column_4').first
132
+ debit_amount = debit_amount_cell.inner_text.strip
133
+ unless debit_amount.empty?
134
+ transaction.amount = -parse_currency(debit_amount)
103
135
  end
104
- unless data[5].empty?
105
- transaction.amount = parse_currency(data[5])
136
+
137
+ credit_amount_cell = row_element.search('.table_column_5').first
138
+ credit_amount = credit_amount_cell.inner_text.strip
139
+ unless credit_amount.empty?
140
+ transaction.amount = parse_currency(credit_amount)
106
141
  end
107
-
142
+
108
143
  transactions << transaction
109
144
  end
145
+
110
146
  end
111
-
147
+
112
148
  transactions
113
149
  end
114
-
150
+
115
151
  private
116
-
152
+
117
153
  def ensure_authenticated
118
-
119
- # Check to see if already authenticated
120
- page = agent.get('https://banking.zionsbank.com/ibuir/')
121
- if page.body.include?("SessionTimeOutException")
122
-
123
- raise InformationMissingError, "Please supply a username" unless self.username
124
- raise InformationMissingError, "Please supply a password" unless self.password
125
-
126
- # Enter the username
127
- page = agent.get('https://www.zionsbank.com')
128
- form = page.form('logonForm')
129
- form.pm_fp = "version%3D1%26pm%5Ffpua%3Dmozilla%2F5%2E0%20%28windows%20nt%206%2E1%3B%20wow64%29%20applewebkit%2F535%2E19%20%28khtml%2C%20like%20gecko%29%20chrome%2F18%2E0%2E1025%2E162%20safari%2F535%2E19%7C5%2E0%20%28Windows%20NT%206%2E1%3B%20WOW64%29%20AppleWebKit%2F535%2E19%20%28KHTML%2C%20like%20Gecko%29%20Chrome%2F18%2E0%2E1025%2E162%20Safari%2F535%2E19%7CWin32%26pm%5Ffpsc%3D32%7C1920%7C1200%7C1200%26pm%5Ffpsw%3D%7Cqt1%7Cqt2%7Cqt3%7Cqt4%7Cqt5%7Cqt6%26pm%5Ffptz%3D%2D6%26pm%5Ffpln%3Dlang%3Den%2DUS%7Csyslang%3D%7Cuserlang%3D%26pm%5Ffpjv%3D1%26pm%5Ffpco%3D1"
130
- form.publicCred1 = username
131
- page = form.submit
132
-
133
- # If the supplied username is incorrect, raise an exception
134
- raise InformationMissingError, "Invalid username" if page.title == "Error Page"
135
-
136
- # Go on to the next page
137
- page = page.links.first.click
138
-
139
- refresh = page.body.match /meta http-equiv="Refresh" content="0; url=([^"]+)/
140
- if refresh
141
- url = refresh[1]
142
- page = agent.get("https://securentry.zionsbank.com#{url}")
143
- end
144
-
145
- # Skip the secret question if we are prompted for the password
146
- unless page.body.include?("Site Validation and Password")
147
- # Find the secret question
148
- question = page.search('div.form_field')[2].css('div').inner_text
149
-
150
- # If the answer to this question was not supplied, raise an exception
151
- raise InformationMissingError, "Please answer the question, \"#{question}\"" unless secret_questions && secret_questions[question]
152
-
153
- # Enter the answer to the secret question
154
- form = page.forms.first
155
- form["challengeEntry.answerText"] = secret_questions[question]
156
- form.radiobutton_with(:value => 'false').check
157
- submit_button = form.button_with(:name => '_eventId_submit')
158
- page = form.submit(submit_button)
159
-
160
- # If the supplied answer is incorrect, raise an exception
161
- raise InformationMissingError, "\"#{secret_questions[question]}\" is not the correct answer to, \"#{question}\"" unless page.search('#errorComponent').empty?
162
- end
163
154
 
164
- # Enter the password
165
- form = page.forms.first
166
- form.privateCred1 = password
167
- submit_button = form.button_with(:name => '_eventId_submit')
168
- page = form.submit(submit_button)
169
-
170
- # If the supplied password is incorrect, raise an exception
171
- raise InformationMissingError, "An invalid password was supplied" unless page.search('#errorComponent').empty?
172
-
173
- # Clicking this link logs us into the banking.zionsbank.com domain
174
- page = page.links.first.click
175
-
176
- if page.uri.to_s != "https://banking.zionsbank.com/ibuir/displayUserInterface.htm"
177
- page = agent.get('https://banking.zionsbank.com/zfnb/userServlet/app/bank/user/viewaccountsbysubtype/viewAccount')
178
-
179
- raise "Unknown URL reached. Try logging in manually through a browser." if page.body.include?("SessionTimeOutException")
180
- end
155
+ # We no longer have a way to check to see if we're logged in or not... assume we're not.
156
+
157
+ raise InformationMissingError, "Please supply a username" unless self.username
158
+ raise InformationMissingError, "Please supply a password" unless self.password
159
+
160
+ # Enter the username
161
+ page = agent.get('https://www.zionsbank.com')
162
+ form = page.form('logonForm')
163
+ form.publicCred1 = self.username
164
+ form.privateCred1 = self.password
165
+ page = form.submit
166
+
167
+ # If the supplied username is incorrect, raise an exception
168
+ # In my tests, invalid username takes you to the password page and an invalid password takes you to the error page
169
+ raise InformationMissingError, "Invalid username/password" if page.title == "Password Page" || page.title == "Error Page"
170
+
171
+ # Go on to the next page
172
+ # It is something like this: https://banking.zionsbank.com/olb/retail/logon/mfa/sso?SAMLart=<bigLongKey>
173
+ page = page.links.first.click
174
+
175
+ #refresh = page.body.match /meta http-equiv="Refresh" content="0; url=([^"]+)/
176
+ #if refresh
177
+ # url = refresh[1]
178
+ # page = agent.get("https://securentry.zionsbank.com#{url}")
179
+ #end
180
+
181
+ # TODO: figure out how this is supposed to work now
182
+ # Skip the secret question if we are prompted for the password
183
+ #unless page.body.include?("Site Validation and Password")
184
+ # # Find the secret question
185
+ # question = page.search('div.form_field')[2].css('div').inner_text
186
+ #
187
+ # # If the answer to this question was not supplied, raise an exception
188
+ # raise InformationMissingError, "Please answer the question, \"#{question}\"" unless secret_questions && secret_questions[question]
189
+ #
190
+ # # Enter the answer to the secret question
191
+ # form = page.forms.first
192
+ # form["challengeEntry.answerText"] = secret_questions[question]
193
+ # form.radiobutton_with(:value => 'false').check
194
+ # submit_button = form.button_with(:name => '_eventId_submit')
195
+ # page = form.submit(submit_button)
196
+ #
197
+ # # If the supplied answer is incorrect, raise an exception
198
+ # raise InformationMissingError, "\"#{secret_questions[question]}\" is not the correct answer to, \"#{question}\"" unless page.search('#errorComponent').empty?
199
+ #end
200
+
201
+ if !page.uri.to_s.start_with?("https://banking.zionsbank.com/olb/retail/protected/home")
202
+ raise "Unknown URL reached. Try logging in manually through a browser."
181
203
  end
182
-
204
+
205
+ token_name = page.search('#csrfTokenName').text
206
+ token_value = page.search('#csrfTokenValue').text
207
+
208
+ @csrf = "#{token_name}=#{token_value}"
209
+
183
210
  true
184
211
  end
185
-
212
+
186
213
  end
187
214
  end
188
215
  end
data/lib/syrup/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Syrup
2
- VERSION = "0.0.13"
2
+ VERSION = "0.0.14"
3
3
  end
@@ -0,0 +1,41 @@
1
+ require "spec_helper"
2
+
3
+ include Institutions
4
+
5
+ describe BankAf, :bank_integration => true do
6
+ before(:each) do
7
+ @bank = BankAf.new
8
+ @bank.setup do |config|
9
+ config.username = ""
10
+ config.password = ""
11
+ config.secret_questions = {}
12
+ end
13
+ end
14
+
15
+ it "fetches one account"
16
+ it "fetches all accounts" do
17
+ accounts = @bank.fetch_accounts
18
+ accounts.each do |account|
19
+ puts "#{account.id} #{account.name}"
20
+ end
21
+ end
22
+
23
+ it "fetches transactions given a date range" do
24
+ account_id = 'Checking'
25
+
26
+ account = @bank.find_account_by_id(account_id)
27
+ account.instance_variable_get(:@prior_day_balance).should be_nil
28
+ account.instance_variable_get(:@current_balance).should be_nil
29
+ account.instance_variable_get(:@available_balance).should be_nil
30
+
31
+ @bank.fetch_transactions(account_id, Date.today - 30, Date.today)
32
+
33
+ puts account.prior_day_balance
34
+ puts account.current_balance
35
+ puts account.available_balance
36
+
37
+ #account.prior_day_balance.should_not be_nil
38
+ account.current_balance.should_not be_nil
39
+ account.available_balance.should_not be_nil
40
+ end
41
+ end
@@ -1,41 +1,41 @@
1
- require "spec_helper"
2
-
3
- include Institutions
4
-
5
- describe ZionsBank, :bank_integration => true do
6
- before(:each) do
7
- @bank = ZionsBank.new
8
- @bank.setup do |config|
9
- config.username = ""
10
- config.password = ""
11
- config.secret_questions = {}
12
- end
13
- end
14
-
15
- it "fetches one account"
16
- it "fetches all accounts" do
17
- accounts = @bank.fetch_accounts
18
- accounts.each do |account|
19
- puts "#{account.id} #{account.name}"
20
- end
21
- end
22
-
23
- it "fetches transactions given a date range" do
24
- account_id = 1
25
-
26
- account = @bank.find_account_by_id(account_id)
27
- account.instance_variable_get(:@prior_day_balance).should be_nil
28
- account.instance_variable_get(:@current_balance).should be_nil
29
- account.instance_variable_get(:@available_balance).should be_nil
30
-
31
- @bank.fetch_transactions(account_id, Date.today - 30, Date.today)
32
-
33
- puts account.prior_day_balance
34
- puts account.current_balance
35
- puts account.available_balance
36
-
37
- account.prior_day_balance.should_not be_nil
38
- account.current_balance.should_not be_nil
39
- account.available_balance.should_not be_nil
40
- end
41
- end
1
+ require "spec_helper"
2
+
3
+ include Institutions
4
+
5
+ describe ZionsBank, :bank_integration => true do
6
+ before(:each) do
7
+ @bank = ZionsBank.new
8
+ @bank.setup do |config|
9
+ config.username = ""
10
+ config.password = ""
11
+ config.secret_questions = {}
12
+ end
13
+ end
14
+
15
+ #it "fetches one account"
16
+ it "fetches all accounts" do
17
+ accounts = @bank.fetch_accounts
18
+ accounts.each do |account|
19
+ puts "#{account.id} #{account.name}"
20
+ end
21
+ end
22
+
23
+ it "fetches transactions given a date range" do
24
+ account_id = "1|a"
25
+
26
+ account = @bank.find_account_by_id(account_id)
27
+ account.instance_variable_get(:@prior_day_balance).should be_nil
28
+ account.instance_variable_get(:@current_balance).should be_nil
29
+ account.instance_variable_get(:@available_balance).should be_nil
30
+
31
+ @bank.fetch_transactions(account_id, Date.today - 30, Date.today)
32
+
33
+ puts "Prior day balance: $#{account.prior_day_balance.to_f}"
34
+ puts "Current balance: $#{account.current_balance.to_f}"
35
+ puts "Available balance: $#{account.available_balance.to_f}"
36
+
37
+ account.prior_day_balance.should_not be_nil
38
+ account.current_balance.should_not be_nil
39
+ account.available_balance.should_not be_nil
40
+ end
41
+ end
data/syrup.gemspec CHANGED
@@ -16,7 +16,7 @@ Gem::Specification.new do |s|
16
16
  s.add_dependency "multi_json", ">= 1.0.3"
17
17
 
18
18
  s.add_development_dependency "rspec", ">= 2.6.0"
19
- s.add_development_dependency "ruby-debug"
19
+ s.add_development_dependency "debugger"
20
20
  s.add_development_dependency "rake"
21
21
 
22
22
  s.rubyforge_project = s.name
metadata CHANGED
@@ -1,109 +1,103 @@
1
- --- !ruby/object:Gem::Specification
1
+ --- !ruby/object:Gem::Specification
2
2
  name: syrup
3
- version: !ruby/object:Gem::Version
4
- hash: 5
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.14
5
5
  prerelease:
6
- segments:
7
- - 0
8
- - 0
9
- - 13
10
- version: 0.0.13
11
6
  platform: ruby
12
- authors:
7
+ authors:
13
8
  - Don Wilson
14
9
  autorequire:
15
10
  bindir: bin
16
11
  cert_chain: []
17
-
18
- date: 2012-05-10 00:00:00 -06:00
19
- default_executable:
20
- dependencies:
21
- - !ruby/object:Gem::Dependency
12
+ date: 2013-06-07 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
22
15
  name: mechanize
23
- prerelease: false
24
- requirement: &id001 !ruby/object:Gem::Requirement
16
+ requirement: !ruby/object:Gem::Requirement
25
17
  none: false
26
- requirements:
27
- - - ">="
28
- - !ruby/object:Gem::Version
29
- hash: 23
30
- segments:
31
- - 1
32
- - 0
33
- - 0
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
34
21
  version: 1.0.0
35
22
  type: :runtime
36
- version_requirements: *id001
37
- - !ruby/object:Gem::Dependency
38
- name: multi_json
39
23
  prerelease: false
40
- requirement: &id002 !ruby/object:Gem::Requirement
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ! '>='
28
+ - !ruby/object:Gem::Version
29
+ version: 1.0.0
30
+ - !ruby/object:Gem::Dependency
31
+ name: multi_json
32
+ requirement: !ruby/object:Gem::Requirement
41
33
  none: false
42
- requirements:
43
- - - ">="
44
- - !ruby/object:Gem::Version
45
- hash: 17
46
- segments:
47
- - 1
48
- - 0
49
- - 3
34
+ requirements:
35
+ - - ! '>='
36
+ - !ruby/object:Gem::Version
50
37
  version: 1.0.3
51
38
  type: :runtime
52
- version_requirements: *id002
53
- - !ruby/object:Gem::Dependency
54
- name: rspec
55
39
  prerelease: false
56
- requirement: &id003 !ruby/object:Gem::Requirement
40
+ version_requirements: !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ! '>='
44
+ - !ruby/object:Gem::Version
45
+ version: 1.0.3
46
+ - !ruby/object:Gem::Dependency
47
+ name: rspec
48
+ requirement: !ruby/object:Gem::Requirement
57
49
  none: false
58
- requirements:
59
- - - ">="
60
- - !ruby/object:Gem::Version
61
- hash: 23
62
- segments:
63
- - 2
64
- - 6
65
- - 0
50
+ requirements:
51
+ - - ! '>='
52
+ - !ruby/object:Gem::Version
66
53
  version: 2.6.0
67
54
  type: :development
68
- version_requirements: *id003
69
- - !ruby/object:Gem::Dependency
70
- name: ruby-debug
71
55
  prerelease: false
72
- requirement: &id004 !ruby/object:Gem::Requirement
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ none: false
58
+ requirements:
59
+ - - ! '>='
60
+ - !ruby/object:Gem::Version
61
+ version: 2.6.0
62
+ - !ruby/object:Gem::Dependency
63
+ name: debugger
64
+ requirement: !ruby/object:Gem::Requirement
73
65
  none: false
74
- requirements:
75
- - - ">="
76
- - !ruby/object:Gem::Version
77
- hash: 3
78
- segments:
79
- - 0
80
- version: "0"
66
+ requirements:
67
+ - - ! '>='
68
+ - !ruby/object:Gem::Version
69
+ version: '0'
81
70
  type: :development
82
- version_requirements: *id004
83
- - !ruby/object:Gem::Dependency
84
- name: rake
85
71
  prerelease: false
86
- requirement: &id005 !ruby/object:Gem::Requirement
72
+ version_requirements: !ruby/object:Gem::Requirement
87
73
  none: false
88
- requirements:
89
- - - ">="
90
- - !ruby/object:Gem::Version
91
- hash: 3
92
- segments:
93
- - 0
94
- version: "0"
74
+ requirements:
75
+ - - ! '>='
76
+ - !ruby/object:Gem::Version
77
+ version: '0'
78
+ - !ruby/object:Gem::Dependency
79
+ name: rake
80
+ requirement: !ruby/object:Gem::Requirement
81
+ none: false
82
+ requirements:
83
+ - - ! '>='
84
+ - !ruby/object:Gem::Version
85
+ version: '0'
95
86
  type: :development
96
- version_requirements: *id005
87
+ prerelease: false
88
+ version_requirements: !ruby/object:Gem::Requirement
89
+ none: false
90
+ requirements:
91
+ - - ! '>='
92
+ - !ruby/object:Gem::Version
93
+ version: '0'
97
94
  description: Simple account balance and transactions extractor by scraping bank websites.
98
- email:
95
+ email:
99
96
  - dontangg@gmail.com
100
97
  executables: []
101
-
102
98
  extensions: []
103
-
104
99
  extra_rdoc_files: []
105
-
106
- files:
100
+ files:
107
101
  - .gitignore
108
102
  - .rspec
109
103
  - CHANGELOG.rdoc
@@ -115,6 +109,7 @@ files:
115
109
  - lib/syrup.rb
116
110
  - lib/syrup/account.rb
117
111
  - lib/syrup/information_missing_error.rb
112
+ - lib/syrup/institutions/bank_af.rb
118
113
  - lib/syrup/institutions/cacert.pem
119
114
  - lib/syrup/institutions/institution_base.rb
120
115
  - lib/syrup/institutions/uccu.rb
@@ -123,49 +118,41 @@ files:
123
118
  - lib/syrup/version.rb
124
119
  - spec/spec_helper.rb
125
120
  - spec/syrup/account_spec.rb
121
+ - spec/syrup/institutions/bank_af_spec.rb
126
122
  - spec/syrup/institutions/institution_base_spec.rb
127
123
  - spec/syrup/institutions/uccu_spec.rb
128
124
  - spec/syrup/institutions/zions_bank_spec.rb
129
125
  - spec/syrup/syrup_spec.rb
130
126
  - spec/syrup/transaction_spec.rb
131
127
  - syrup.gemspec
132
- has_rdoc: true
133
128
  homepage: http://github.com/dontangg/syrup
134
129
  licenses: []
135
-
136
130
  post_install_message:
137
131
  rdoc_options: []
138
-
139
- require_paths:
132
+ require_paths:
140
133
  - lib
141
- required_ruby_version: !ruby/object:Gem::Requirement
134
+ required_ruby_version: !ruby/object:Gem::Requirement
142
135
  none: false
143
- requirements:
144
- - - ">="
145
- - !ruby/object:Gem::Version
146
- hash: 3
147
- segments:
148
- - 0
149
- version: "0"
150
- required_rubygems_version: !ruby/object:Gem::Requirement
136
+ requirements:
137
+ - - ! '>='
138
+ - !ruby/object:Gem::Version
139
+ version: '0'
140
+ required_rubygems_version: !ruby/object:Gem::Requirement
151
141
  none: false
152
- requirements:
153
- - - ">="
154
- - !ruby/object:Gem::Version
155
- hash: 3
156
- segments:
157
- - 0
158
- version: "0"
142
+ requirements:
143
+ - - ! '>='
144
+ - !ruby/object:Gem::Version
145
+ version: '0'
159
146
  requirements: []
160
-
161
147
  rubyforge_project: syrup
162
- rubygems_version: 1.6.2
148
+ rubygems_version: 1.8.23
163
149
  signing_key:
164
150
  specification_version: 3
165
151
  summary: Simple account balance and transactions extractor.
166
- test_files:
152
+ test_files:
167
153
  - spec/spec_helper.rb
168
154
  - spec/syrup/account_spec.rb
155
+ - spec/syrup/institutions/bank_af_spec.rb
169
156
  - spec/syrup/institutions/institution_base_spec.rb
170
157
  - spec/syrup/institutions/uccu_spec.rb
171
158
  - spec/syrup/institutions/zions_bank_spec.rb