commerce-bank-client 1.0.0

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/Manifest ADDED
@@ -0,0 +1,8 @@
1
+ Manifest
2
+ README.markdown
3
+ Rakefile
4
+ lib/commercebank.rb
5
+ lib/commercebank/monkey.rb
6
+ test/commerce_bank_client_test.rb
7
+ test/monkeypatch_test.rb
8
+ test/test_helper.rb
data/README.markdown ADDED
@@ -0,0 +1,15 @@
1
+ commerce-bank-client
2
+ ====================
3
+
4
+ A Ruby client library for the Commerce Bank website (https://banking.commercebank.com).
5
+
6
+ ## Installation
7
+
8
+ gem install commerce-bank-client
9
+
10
+ ## Usage
11
+
12
+ require "rubygems"
13
+ require "commercebank"
14
+
15
+ cb = CommerceBank.new
data/Rakefile ADDED
@@ -0,0 +1,16 @@
1
+ require 'rubygems'
2
+ require 'rake'
3
+ require 'echoe'
4
+
5
+ Echoe.new("commerce-bank-client", "1.0.0") do |p|
6
+
7
+ p.description = "An interface to the Commerce Bank website (https://banking.commercebank.com)."
8
+ p.url = "http://github.com/alexmchale/commerce-bank-client"
9
+ p.author = "Alex McHale"
10
+ p.email = "alexmchale@gmail.com"
11
+ p.ignore_pattern = %w( tmp/* script/* )
12
+ p.runtime_dependencies = []
13
+ p.development_dependencies = []
14
+ p.require_signed = true
15
+
16
+ end
@@ -0,0 +1,33 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ Gem::Specification.new do |s|
4
+ s.name = %q{commerce-bank-client}
5
+ s.version = "1.0.0"
6
+
7
+ s.required_rubygems_version = Gem::Requirement.new(">= 1.2") if s.respond_to? :required_rubygems_version=
8
+ s.authors = ["Alex McHale"]
9
+ s.cert_chain = ["/home/alexmchale/.ssh/gem-public_cert.pem"]
10
+ s.date = %q{2010-09-02}
11
+ s.description = %q{An interface to the Commerce Bank website (https://banking.commercebank.com).}
12
+ s.email = %q{alexmchale@gmail.com}
13
+ s.extra_rdoc_files = ["README.markdown", "lib/commercebank.rb", "lib/commercebank/monkey.rb"]
14
+ s.files = ["Manifest", "README.markdown", "Rakefile", "lib/commercebank.rb", "lib/commercebank/monkey.rb", "test/commerce_bank_client_test.rb", "test/monkeypatch_test.rb", "test/test_helper.rb", "commerce-bank-client.gemspec"]
15
+ s.homepage = %q{http://github.com/alexmchale/commerce-bank-client}
16
+ s.rdoc_options = ["--line-numbers", "--inline-source", "--title", "Commerce-bank-client", "--main", "README.markdown"]
17
+ s.require_paths = ["lib"]
18
+ s.rubyforge_project = %q{commerce-bank-client}
19
+ s.rubygems_version = %q{1.3.7}
20
+ s.signing_key = %q{/home/alexmchale/.ssh/gem-private_key.pem}
21
+ s.summary = %q{An interface to the Commerce Bank website (https://banking.commercebank.com).}
22
+ s.test_files = ["test/test_helper.rb", "test/monkeypatch_test.rb", "test/commerce_bank_client_test.rb"]
23
+
24
+ if s.respond_to? :specification_version then
25
+ current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
26
+ s.specification_version = 3
27
+
28
+ if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
29
+ else
30
+ end
31
+ else
32
+ end
33
+ end
@@ -0,0 +1,64 @@
1
+ require 'rubygems'
2
+ require 'pp'
3
+ require 'andand'
4
+ require 'cgi'
5
+ require 'yaml'
6
+
7
+ class Array
8
+ def binary
9
+ map {|e| yield(e) ? [e, nil] : [nil, e]}.transpose.map {|a| a.compact}
10
+ end
11
+
12
+ def paramify
13
+ hash = Hash.new
14
+
15
+ hash.merge! pop if last.kind_of? Hash
16
+ each {|e| hash[e] = true}
17
+
18
+ hash
19
+ end
20
+ end
21
+
22
+ class Object
23
+ def to_cents
24
+ (to_s.gsub(/[^-.0-9]/, '').to_f * 100).to_i
25
+ end
26
+
27
+ def to_dollars(*options)
28
+ options = options.paramify
29
+
30
+ plus = options[:show_plus] ? '+' : ''
31
+ minus = options[:hide_minus] ? '' : '-'
32
+ sign = to_i >= 0 ? plus : minus
33
+
34
+ ("%s%0.2f" % [ sign, to_i.abs / 100.0 ]).commify
35
+ end
36
+ end
37
+
38
+ class Date
39
+ def days_in_month
40
+ (Date.parse("12/31/#{strftime("%Y")}") << (12 - month)).day
41
+ end
42
+
43
+ def last_sunday
44
+ d = self
45
+ d -= 1 until d.wday == 0
46
+ d
47
+ end
48
+ end
49
+
50
+ class Hash
51
+ def to_url
52
+ map {|key, value| "#{CGI.escape key.to_s}=#{CGI.escape value.to_s}"}.join "&"
53
+ end
54
+
55
+ def to_cookie
56
+ map {|key, value| "#{key}=#{value}"}.join('; ')
57
+ end
58
+ end
59
+
60
+ class String
61
+ def commify
62
+ reverse.gsub(/(\d\d\d)(?=\d)/, '\1,').reverse
63
+ end
64
+ end
@@ -0,0 +1,232 @@
1
+ require 'rubygems'
2
+ require 'net/http'
3
+ require 'net/https'
4
+ require 'hpricot'
5
+ require 'andand'
6
+ require 'cgi'
7
+ require 'yaml'
8
+ require 'time'
9
+ require 'date'
10
+ require 'json'
11
+ require 'htmlentities'
12
+ require 'gmail'
13
+ require 'appconfig'
14
+ require 'commercebank/monkey.rb'
15
+
16
+ class Fixnum
17
+ def days; 24 * hours; end
18
+ def hours; 60 * minutes; end
19
+ def minutes; 60 * seconds; end
20
+ def seconds; self; end
21
+ end
22
+
23
+ class WebClient
24
+ attr_reader :fields, :cookies
25
+
26
+ def initialize
27
+ @cookies = Hash.new
28
+ @http = Net::HTTP.new('banking.commercebank.com', 443)
29
+ @http.use_ssl = true
30
+ @http.verify_mode = OpenSSL::SSL::VERIFY_NONE
31
+ end
32
+
33
+ def get(path, form = nil)
34
+ response = @http.get(path, header)
35
+ add_cookies(response)
36
+ @fields = (form && get_form(response.body, form)) || Hash.new
37
+ response
38
+ end
39
+
40
+ def post(path, form = nil)
41
+ response = @http.post(path, @fields.to_url, header)
42
+ add_cookies(response)
43
+ @fields = (form && get_form(response.body, form)) || Hash.new
44
+ response
45
+ end
46
+
47
+ private
48
+
49
+ def header
50
+ { 'Cookie' => @cookies.to_cookie }
51
+ end
52
+
53
+ def get_form(body, name)
54
+ Hpricot.buffer_size = 262144
55
+ doc = Hpricot.parse(body)
56
+ form = (doc/"##{name}").first
57
+ raise "could not find form #{name}" unless form
58
+ fields = Hash[*((form/"input").map {|e| [ e.attributes['name'], e.attributes['value'] ]}.flatten)]
59
+ fields['TestJavaScript'] = 'OK'
60
+ fields
61
+ end
62
+
63
+ def add_cookies(response)
64
+ CGI::Cookie.parse(response.header['set-cookie']).each do |key, value|
65
+ @cookies[key] = value.first
66
+ end
67
+
68
+ @cookies.delete 'path'
69
+ @cookies.delete 'expires'
70
+
71
+ self
72
+ end
73
+ end
74
+
75
+ class CommerceBank
76
+ attr_reader :pending, :register, :current, :available
77
+
78
+ def initialize
79
+ @config = AppConfig.new('~/.commerce.yaml')
80
+
81
+ client = WebClient.new
82
+
83
+ client.get('/')
84
+
85
+ client.get('/CBI/login.aspx', 'MAINFORM')
86
+
87
+ client.fields['txtUserID'] = @config.get('username')
88
+ response = client.post('/CBI/login.aspx', 'MAINFORM')
89
+
90
+ # If a question was asked, answer it then get the password page.
91
+ question = response.body.scan(/Your security question:&nbsp;&nbsp;(.*?)<\/td>/i).first.andand.first
92
+ if question
93
+ client.fields['txtChallengeAnswer'] = @config.get(question)
94
+ client.fields['saveComputer'] = 'rdoBindDeviceNo'
95
+ response = client.post('/CBI/login.aspx', 'MAINFORM')
96
+ end
97
+
98
+ raise "could not reach the password page" unless client.fields['__EVENTTARGET'] == 'btnLogin'
99
+
100
+ client.fields['txtPassword'] = @config.get('password')
101
+ response = client.post('/CBI/login.aspx')
102
+
103
+ response = client.get('/CBI/Accounts/CBI/Activity.aspx', 'MAINFORM')
104
+ (@current, @available) = parse_balance(response.body)
105
+ @pending = parse_pending(response.body)
106
+
107
+ client.fields['Anthem_UpdatePage'] = 'true'
108
+ client.fields['txtFilterFromDate:textBox'] = (Time.now - 30.days).strftime('%m/%d/%Y')
109
+ client.fields['txtFilterToDate:textBox'] = Time.now.strftime('%m/%d/%Y')
110
+ response = client.post('/CBI/Accounts/CBI/Activity.aspx?Anthem_CallBack=true')
111
+
112
+ raw_data = JSON.parse(response.body)
113
+ @register = parse_register(raw_data['controls']['pnlPosted'])
114
+
115
+ @register.each do |entry|
116
+ entry[:images].map! do |image_url|
117
+ url = "https://banking.commercebank.com/CBI/Accounts/CBI/#{image_url[:url]}"
118
+ client.get(url).andand.body
119
+ end
120
+ entry[:images].compact!
121
+ end
122
+ end
123
+
124
+ def daily_summary
125
+ today, yesterday, this_week, last_week = [], [], [], []
126
+
127
+ register.each do |entry|
128
+ if entry[:date] == Date.today then today << entry
129
+ elsif entry[:date] == (Date.today - 1) then yesterday << entry
130
+ elsif entry[:date] >= Date.today.last_sunday then this_week << entry
131
+ elsif entry[:date] >= (Date.today.last_sunday - 1).last_sunday then last_week << entry
132
+ end
133
+ end
134
+
135
+ yield 'Pending', @pending
136
+ yield 'Today', today
137
+ yield 'Yesterday', yesterday
138
+ yield 'This Week', this_week
139
+ yield 'Last Week', last_week
140
+ end
141
+
142
+ def monthly_summary(day_in_month = (Date.today - Date.today.day))
143
+ first_of_month = day_in_month - day_in_month.day + 1
144
+ last_of_month = first_of_month + day_in_month.days_in_month - 1
145
+
146
+ entries = register.find_all {|entry| entry[:date] >= first_of_month && entry[:date] <= last_of_month}
147
+
148
+ yield day_in_month.strftime('%B'), entries
149
+ end
150
+
151
+ private
152
+
153
+ def parse_balance(body)
154
+ Hpricot.buffer_size = 262144
155
+ doc = Hpricot.parse(body)
156
+ summaryRows = doc/"table.summaryTable"/"tr"
157
+ current = (summaryRows[3]/"td")[1].inner_html.to_cents
158
+ available = (summaryRows[4]/"td")[1].inner_html.to_cents
159
+ [current, available]
160
+ end
161
+
162
+ def parse_pending(body)
163
+ Hpricot.buffer_size = 262144
164
+ doc = Hpricot.parse(body)
165
+ coder = HTMLEntities.new
166
+
167
+ (doc/"#grdMemoPosted"/"tr").map do |e|
168
+ next nil unless (e['class'] == 'item' || e['class'] == 'alternatingItem')
169
+
170
+ values = (e/"td").map {|e1| coder.decode(e1.inner_html.strip)}
171
+
172
+ debit = values[2].to_cents
173
+ credit = values[3].to_cents
174
+ delta = credit - debit
175
+
176
+ { :date => parse_date(values[0]),
177
+ :destination => values[1],
178
+ :delta => delta,
179
+ :debit => debit,
180
+ :credit => credit }
181
+ end.compact
182
+ end
183
+
184
+ def parse_register(body)
185
+ Hpricot.buffer_size = 262144
186
+ doc = Hpricot.parse(body)
187
+ coder = HTMLEntities.new
188
+ (doc/"#grdHistory"/"tr").map do |e|
189
+ next nil unless [ 'item', 'alternatingitem' ].include? e['class'].to_s.downcase
190
+
191
+ anchor = e.at("a")
192
+ values = (e/"td").map {|e1| e1.inner_html}
193
+ date = parse_date(values[0])
194
+ check = values[1].strip
195
+ debit = values[3].to_cents
196
+ credit = values[4].to_cents
197
+ delta = credit - debit
198
+ total = values[5].scan(/\$[\d,]+\.\d\d/).first.to_cents
199
+
200
+ images = (e/"a").find_all do |e1|
201
+ e1['target'].to_s.downcase == 'checkimage'
202
+ end.map do |e1|
203
+ { :url => e1['href'], :title => e1.inner_html.strip }
204
+ end
205
+
206
+ {
207
+ :destination => coder.decode(anchor.inner_html.strip),
208
+ :url => anchor['href'],
209
+ :date => date,
210
+ :check => check,
211
+ :images => images,
212
+ :delta => delta,
213
+ :debit => debit,
214
+ :credit => credit,
215
+ :total => total
216
+ }
217
+ end.compact
218
+ end
219
+
220
+ def parse_date(date_string)
221
+ (month, day, year) = date_string.scan(/(\d+)\/(\d+)\/(\d+)/).first
222
+ return nil unless month && day && year
223
+
224
+ month = month.to_i
225
+ day = day.to_i
226
+ year = year.to_i
227
+ year += 2000 if year < 100
228
+
229
+ Date.parse("%04d-%02d-%02d" % [ year, month, day ])
230
+ end
231
+ end
232
+
@@ -0,0 +1,6 @@
1
+ require 'test_helper'
2
+
3
+ class CommerceBankClientTest < Test::Unit::TestCase
4
+ should "implement tests" do
5
+ end
6
+ end
@@ -0,0 +1,11 @@
1
+ require 'test_helper'
2
+
3
+ class MonkeyPatchTest < Test::Unit::TestCase
4
+ should "convert a hash to a valid url parameter string" do
5
+ h1 = { :foo => 1, :bar => 2, :baz => 3 }
6
+ assert_equal h1.to_url, 'foo=1&bar=2&baz=3'
7
+
8
+ h2 = { :foo => "What's up Doc", :bar => "1" }
9
+ assert_equal h2.to_url, "foo=What%27s+up+Doc&bar=1"
10
+ end
11
+ end
@@ -0,0 +1,10 @@
1
+ require 'rubygems'
2
+ require 'test/unit'
3
+ require 'shoulda'
4
+
5
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
6
+ $LOAD_PATH.unshift(File.dirname(__FILE__))
7
+ require 'lib/commercebank.rb'
8
+
9
+ class Test::Unit::TestCase
10
+ end
data.tar.gz.sig ADDED
@@ -0,0 +1,2 @@
1
+ �&y�#�����ɞ�q���l��˩"���F������J8�WB_A�3�?�Hi��z:S1�F �o@�6 ��]B�^�N�z/���W��N�kA���� �#���<t��3f��J��V�}U�������^s!��\��L���n��~w���4����6�i� ��Ǭ4o����0�VXYr]l!��K�,F޶�;K)�o�dY�v
2
+ 0E��(
metadata ADDED
@@ -0,0 +1,106 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: commerce-bank-client
3
+ version: !ruby/object:Gem::Version
4
+ hash: 23
5
+ prerelease: false
6
+ segments:
7
+ - 1
8
+ - 0
9
+ - 0
10
+ version: 1.0.0
11
+ platform: ruby
12
+ authors:
13
+ - Alex McHale
14
+ autorequire:
15
+ bindir: bin
16
+ cert_chain:
17
+ - |
18
+ -----BEGIN CERTIFICATE-----
19
+ MIIDNjCCAh6gAwIBAgIBADANBgkqhkiG9w0BAQUFADBBMRMwEQYDVQQDDAphbGV4
20
+ bWNoYWxlMRUwEwYKCZImiZPyLGQBGRYFZ21haWwxEzARBgoJkiaJk/IsZAEZFgNj
21
+ b20wHhcNMTAwODMxMTQ1NjExWhcNMTEwODMxMTQ1NjExWjBBMRMwEQYDVQQDDAph
22
+ bGV4bWNoYWxlMRUwEwYKCZImiZPyLGQBGRYFZ21haWwxEzARBgoJkiaJk/IsZAEZ
23
+ FgNjb20wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDIUoy5sx2lmVzR
24
+ higIWdA6zFNSt1eibOVF0nCptQ2BejzefTmfhpgwWKDX9FvGdF213WBwxIGJFP0c
25
+ yOJTOoTgKQwQd6zCSoeNgNNHrXwbd42OKfHBTN1+DvNVoK9mLB659fUJ5CoCqNLU
26
+ FI6RtILyZVH/Hja8nnHJrfGjJ0aWSRueVuO/06QmKvm19EJEVbTSd/DwAbJTYrQD
27
+ 6snqEY+64mcx8gvF2aCmGXKSpLqnTedsr3/16cd1rkHw7dZqbzyKc1uZaQL+giwp
28
+ Ijyn2SdMV+OKTf6xe/aL22aW/Z2b+tjQO8elB+/cz5kOlAkhtN3ENjf2e3H3UZSs
29
+ r+JQgRSdAgMBAAGjOTA3MAkGA1UdEwQCMAAwCwYDVR0PBAQDAgSwMB0GA1UdDgQW
30
+ BBS84ERhhfvntDfJzJe0/by+aeVeqDANBgkqhkiG9w0BAQUFAAOCAQEAFZRn7kGl
31
+ gvITFgcLlGAWNwYTfDiXm+8xjNXCyFLP2YgZvzNmP9HtzxnJHG9pk1tvAzKVgGQB
32
+ w+yPgHA9ASVnNP8/YifMVQtRkOYvmYiK+ynLTgqn3cCod0Q4mWcZ3mTsVfAqGxAZ
33
+ fyoyfn4C+7Ks6j3k7yXzcKoAi4w0RiHYUrNQAOFzkahHaovvophs88nIgq/HNSY6
34
+ tgs+JaGdLZUsgj0TZpzELi4d6iFDD44D/pAKN8VIpSlcbyKXkIyQa/NrKOTSie7u
35
+ AH5EvwIudku4RjdH363cTYX1xZ69LjcwrUpDrCJbG9jNK8icMoAOTEw0huZYvGRC
36
+ qyGdKMrvQEbACA==
37
+ -----END CERTIFICATE-----
38
+
39
+ date: 2010-09-02 00:00:00 +00:00
40
+ default_executable:
41
+ dependencies: []
42
+
43
+ description: An interface to the Commerce Bank website (https://banking.commercebank.com).
44
+ email: alexmchale@gmail.com
45
+ executables: []
46
+
47
+ extensions: []
48
+
49
+ extra_rdoc_files:
50
+ - README.markdown
51
+ - lib/commercebank.rb
52
+ - lib/commercebank/monkey.rb
53
+ files:
54
+ - Manifest
55
+ - README.markdown
56
+ - Rakefile
57
+ - lib/commercebank.rb
58
+ - lib/commercebank/monkey.rb
59
+ - test/commerce_bank_client_test.rb
60
+ - test/monkeypatch_test.rb
61
+ - test/test_helper.rb
62
+ - commerce-bank-client.gemspec
63
+ has_rdoc: true
64
+ homepage: http://github.com/alexmchale/commerce-bank-client
65
+ licenses: []
66
+
67
+ post_install_message:
68
+ rdoc_options:
69
+ - --line-numbers
70
+ - --inline-source
71
+ - --title
72
+ - Commerce-bank-client
73
+ - --main
74
+ - README.markdown
75
+ require_paths:
76
+ - lib
77
+ required_ruby_version: !ruby/object:Gem::Requirement
78
+ none: false
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ hash: 3
83
+ segments:
84
+ - 0
85
+ version: "0"
86
+ required_rubygems_version: !ruby/object:Gem::Requirement
87
+ none: false
88
+ requirements:
89
+ - - ">="
90
+ - !ruby/object:Gem::Version
91
+ hash: 11
92
+ segments:
93
+ - 1
94
+ - 2
95
+ version: "1.2"
96
+ requirements: []
97
+
98
+ rubyforge_project: commerce-bank-client
99
+ rubygems_version: 1.3.7
100
+ signing_key:
101
+ specification_version: 3
102
+ summary: An interface to the Commerce Bank website (https://banking.commercebank.com).
103
+ test_files:
104
+ - test/test_helper.rb
105
+ - test/monkeypatch_test.rb
106
+ - test/commerce_bank_client_test.rb
metadata.gz.sig ADDED
Binary file