syrup 0.0.13 → 0.0.14

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/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