syrup 0.0.12 → 0.0.13

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,196 +1,196 @@
1
- require 'bigdecimal'
2
-
3
- module Syrup
4
- module Institutions
5
- class InstitutionBase
6
-
7
- class << self
8
- # This method is called whenever a class inherits from this class. We keep track of
9
- # all of them because they should all be institutions. This way we can provide a
10
- # list of supported institutions via code.
11
- def inherited(subclass)
12
- @subclasses ||= []
13
- @subclasses << subclass
14
- end
15
-
16
- # Returns an array of all classes that inherit from this class. Or, in other words,
17
- # an array of all supported institutions
18
- def subclasses
19
- @subclasses
20
- end
21
- end
22
-
23
- ##
24
- # :attr_writer: populated
25
-
26
- ##
27
- # :attr_reader: populated?
28
-
29
- ##
30
- # :attr_reader: agent
31
- # Gets an instance of Mechanize for use by any subclasses.
32
-
33
- ##
34
- # :attr_reader: accounts
35
- # Returns an array of all of the user's accounts at this institution.
36
- # If accounts hasn't been populated, it populates accounts and then returns them.
37
-
38
- #
39
- attr_accessor :username, :password, :secret_questions
40
-
41
- def initialize
42
- @accounts = []
43
- end
44
-
45
- # This method allows you to setup an institution with block syntax
46
- #
47
- # InstitutionBase.setup do |config|
48
- # config.username = 'my_user"
49
- # ...
50
- # end
51
- def setup
52
- yield self
53
- self
54
- end
55
-
56
- def populated?
57
- @populated
58
- end
59
-
60
- def populated=(value)
61
- @populated = value
62
- end
63
-
64
- def accounts
65
- populate_accounts
66
- @accounts
67
- end
68
-
69
- # Returns an account with the specified +account_id+. Always use this method to
70
- # create a new `Account` object. If you do, it will get populated correctly whenever
71
- # the population occurs.
72
- def find_account_by_id(account_id)
73
- account = @accounts.find { |a| a.id == account_id }
74
- unless account || populated?
75
- account = Account.new(:id => account_id, :institution => self)
76
- @accounts << account
77
- end
78
- account
79
- end
80
-
81
- # Populates an account given an `account_id`. The implementing institution may populate
82
- # all accounts when this is called if there isn't a way to only request one account's
83
- # information.
84
- def populate_account(account_id)
85
- unless populated?
86
- result = fetch_account(account_id)
87
- return nil if result.nil?
88
-
89
- if result.respond_to?(:each)
90
- populate_accounts(result)
91
- find_account_by_id(account_id)
92
- else
93
- result.populated = true
94
- account = find_account_by_id(account_id)
95
- account.merge! result if account
96
- end
97
- end
98
- end
99
-
100
- # Populates all of the user's accounts at this institution.
101
- def populate_accounts(populated_accounts = nil)
102
- unless populated?
103
- all_accounts = populated_accounts || fetch_accounts
104
-
105
- # Remove any accounts that were added, that don't actually exist
106
- @accounts.delete_if do |a|
107
- if all_accounts.include?(a)
108
- false
109
- else
110
- a.valid = false
111
- true
112
- end
113
- end
114
-
115
- # Add any additional account information
116
- new_accounts = []
117
- all_accounts.each do |filled_account|
118
- account = @accounts.find { |a| a.id == filled_account.id }
119
-
120
- filled_account.populated = true
121
-
122
- # If we already had an account with this id, fill it with data
123
- if account
124
- account.merge! filled_account
125
- else
126
- new_accounts << filled_account
127
- end
128
- end
129
- @accounts |= new_accounts # Uses set union
130
-
131
- self.populated = true
132
- end
133
- end
134
-
135
- protected
136
-
137
- def agent
138
- unless @agent
139
- @agent = Mechanize.new
140
-
141
- # Provide path to cert bundle for Windows
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
144
- end
145
-
146
- @agent
147
- end
148
-
149
- # This is just a helper method that simplifies the common process of extracting a number
150
- # from a string representing a currency.
151
- #
152
- # parse_currency('$ 1,234.56') #=> 1234.56
153
- def parse_currency(currency)
154
- BigDecimal.new(currency.gsub(/[^0-9.]/, ''))
155
- end
156
-
157
- # A helper method that replaces a few HTML entities with their actual characters
158
- #
159
- # unescape_html("You &amp; I") #=> "You & I"
160
- def unescape_html(str)
161
- str.gsub(/&(.*?);/n) do
162
- match = $1.dup
163
- case match
164
- when /\Aamp\z/ni then '&'
165
- when /\Aquot\z/ni then '"'
166
- when /\Agt\z/ni then '>'
167
- when /\Alt\z/ni then '<'
168
- when /\A#0*(\d+)\z/n then
169
- if Integer($1) < 256
170
- Integer($1).chr
171
- else
172
- if Integer($1) < 65536 and ($KCODE[0] == ?u or $KCODE[0] == ?U)
173
- [Integer($1)].pack("U")
174
- else
175
- "&##{$1};"
176
- end
177
- end
178
- when /\A#x([0-9a-f]+)\z/ni then
179
- if $1.hex < 256
180
- $1.hex.chr
181
- else
182
- if $1.hex < 65536 and ($KCODE[0] == ?u or $KCODE[0] == ?U)
183
- [$1.hex].pack("U")
184
- else
185
- "&#x#{$1};"
186
- end
187
- end
188
- else
189
- "&#{match};"
190
- end
191
- end
192
- end
193
-
194
- end
195
- end
196
- end
1
+ require 'bigdecimal'
2
+
3
+ module Syrup
4
+ module Institutions
5
+ class InstitutionBase
6
+
7
+ class << self
8
+ # This method is called whenever a class inherits from this class. We keep track of
9
+ # all of them because they should all be institutions. This way we can provide a
10
+ # list of supported institutions via code.
11
+ def inherited(subclass)
12
+ @subclasses ||= []
13
+ @subclasses << subclass
14
+ end
15
+
16
+ # Returns an array of all classes that inherit from this class. Or, in other words,
17
+ # an array of all supported institutions
18
+ def subclasses
19
+ @subclasses
20
+ end
21
+ end
22
+
23
+ ##
24
+ # :attr_writer: populated
25
+
26
+ ##
27
+ # :attr_reader: populated?
28
+
29
+ ##
30
+ # :attr_reader: agent
31
+ # Gets an instance of Mechanize for use by any subclasses.
32
+
33
+ ##
34
+ # :attr_reader: accounts
35
+ # Returns an array of all of the user's accounts at this institution.
36
+ # If accounts hasn't been populated, it populates accounts and then returns them.
37
+
38
+ #
39
+ attr_accessor :username, :password, :secret_questions
40
+
41
+ def initialize
42
+ @accounts = []
43
+ end
44
+
45
+ # This method allows you to setup an institution with block syntax
46
+ #
47
+ # InstitutionBase.setup do |config|
48
+ # config.username = 'my_user"
49
+ # ...
50
+ # end
51
+ def setup
52
+ yield self
53
+ self
54
+ end
55
+
56
+ def populated?
57
+ @populated
58
+ end
59
+
60
+ def populated=(value)
61
+ @populated = value
62
+ end
63
+
64
+ def accounts
65
+ populate_accounts
66
+ @accounts
67
+ end
68
+
69
+ # Returns an account with the specified +account_id+. Always use this method to
70
+ # create a new `Account` object. If you do, it will get populated correctly whenever
71
+ # the population occurs.
72
+ def find_account_by_id(account_id)
73
+ account = @accounts.find { |a| a.id == account_id }
74
+ unless account || populated?
75
+ account = Account.new(:id => account_id, :institution => self)
76
+ @accounts << account
77
+ end
78
+ account
79
+ end
80
+
81
+ # Populates an account given an `account_id`. The implementing institution may populate
82
+ # all accounts when this is called if there isn't a way to only request one account's
83
+ # information.
84
+ def populate_account(account_id)
85
+ unless populated?
86
+ result = fetch_account(account_id)
87
+ return nil if result.nil?
88
+
89
+ if result.respond_to?(:each)
90
+ populate_accounts(result)
91
+ find_account_by_id(account_id)
92
+ else
93
+ result.populated = true
94
+ account = find_account_by_id(account_id)
95
+ account.merge! result if account
96
+ end
97
+ end
98
+ end
99
+
100
+ # Populates all of the user's accounts at this institution.
101
+ def populate_accounts(populated_accounts = nil)
102
+ unless populated?
103
+ all_accounts = populated_accounts || fetch_accounts
104
+
105
+ # Remove any accounts that were added, that don't actually exist
106
+ @accounts.delete_if do |a|
107
+ if all_accounts.include?(a)
108
+ false
109
+ else
110
+ a.valid = false
111
+ true
112
+ end
113
+ end
114
+
115
+ # Add any additional account information
116
+ new_accounts = []
117
+ all_accounts.each do |filled_account|
118
+ account = @accounts.find { |a| a.id == filled_account.id }
119
+
120
+ filled_account.populated = true
121
+
122
+ # If we already had an account with this id, fill it with data
123
+ if account
124
+ account.merge! filled_account
125
+ else
126
+ new_accounts << filled_account
127
+ end
128
+ end
129
+ @accounts |= new_accounts # Uses set union
130
+
131
+ self.populated = true
132
+ end
133
+ end
134
+
135
+ protected
136
+
137
+ def agent
138
+ unless @agent
139
+ @agent = Mechanize.new
140
+
141
+ # Provide path to cert bundle for Windows
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
144
+ end
145
+
146
+ @agent
147
+ end
148
+
149
+ # This is just a helper method that simplifies the common process of extracting a number
150
+ # from a string representing a currency.
151
+ #
152
+ # parse_currency('$ 1,234.56') #=> 1234.56
153
+ def parse_currency(currency)
154
+ BigDecimal.new(currency.gsub(/[^0-9.]/, ''))
155
+ end
156
+
157
+ # A helper method that replaces a few HTML entities with their actual characters
158
+ #
159
+ # unescape_html("You &amp; I") #=> "You & I"
160
+ def unescape_html(str)
161
+ str.gsub(/&(.*?);/n) do
162
+ match = $1.dup
163
+ case match
164
+ when /\Aamp\z/ni then '&'
165
+ when /\Aquot\z/ni then '"'
166
+ when /\Agt\z/ni then '>'
167
+ when /\Alt\z/ni then '<'
168
+ when /\A#0*(\d+)\z/n then
169
+ if Integer($1) < 256
170
+ Integer($1).chr
171
+ else
172
+ if Integer($1) < 65536 and ($KCODE[0] == ?u or $KCODE[0] == ?U)
173
+ [Integer($1)].pack("U")
174
+ else
175
+ "&##{$1};"
176
+ end
177
+ end
178
+ when /\A#x([0-9a-f]+)\z/ni then
179
+ if $1.hex < 256
180
+ $1.hex.chr
181
+ else
182
+ if $1.hex < 65536 and ($KCODE[0] == ?u or $KCODE[0] == ?U)
183
+ [$1.hex].pack("U")
184
+ else
185
+ "&#x#{$1};"
186
+ end
187
+ end
188
+ else
189
+ "&#{match};"
190
+ end
191
+ end
192
+ end
193
+
194
+ end
195
+ end
196
+ end
@@ -1,137 +1,142 @@
1
- require 'date'
2
- require 'bigdecimal'
3
-
4
- module Syrup
5
- module Institutions
6
- class Uccu < InstitutionBase
7
-
8
- class << self
9
- def name
10
- "UCCU"
11
- end
12
-
13
- def id
14
- "uccu"
15
- end
16
- end
17
-
18
- def fetch_account(account_id)
19
- fetch_accounts
20
- end
21
-
22
- def fetch_accounts
23
- ensure_authenticated
24
-
25
- # List accounts
26
- page = agent.post('https://pb.uccu.com/UCCU/Ajax/RpcHandler.ashx',
27
- '{"id":0,"method":"accounts.getBalances","params":[false]}',
28
- 'X-JSON-RPC' => 'accounts.getBalances')
29
-
30
- json = MultiJson.load(page.body)
31
-
32
- accounts = []
33
- json['result'].each do |account|
34
- next if account['accountIndex'] == -1
35
-
36
- new_account = Account.new(:id => account['accountIndex'], :institution => self)
37
- new_account.name = unescape_html(account['displayName'][/^[^(]*/, 0].strip)
38
- new_account.account_number = account['displayName'][/\(([*0-9-]+)\)/, 1]
39
- new_account.current_balance = BigDecimal.new(account['current'])
40
- new_account.available_balance = BigDecimal.new(account['available'])
41
- # new_account.type = :deposit # :credit
42
-
43
- accounts << new_account
44
- end
45
-
46
- accounts
47
- end
48
-
49
- def fetch_transactions(account_id, starting_at, ending_at)
50
- ensure_authenticated
51
-
52
- transactions = []
53
-
54
- page = agent.get("https://pb.uccu.com/UCCU/Accounts/Activity.aspx?index=#{account_id}")
55
- form = page.form("MAINFORM")
56
- form.ddlAccounts = account_id
57
- form.ddlType = 0 # 0 = All types of transactions
58
- form.field_with(:id => 'txtFromDate_textBox').value = starting_at.month.to_s + '/' + starting_at.strftime('%e/%Y').strip
59
- form.field_with(:id => 'txtToDate_textBox').value = ending_at.month.to_s + '/' + ending_at.strftime('%e/%Y').strip
60
- submit_button = form.button_with(:name => 'btnSubmitHistoryRequest')
61
- page = form.submit(submit_button)
62
-
63
- # Look for the account balance
64
- account = find_account_by_id(account_id)
65
- page.search('.summaryTable tr').each do |row_element|
66
- first_cell_text = ''
67
- row_element.children.each do |cell_element|
68
- if first_cell_text.empty?
69
- first_cell_text = cell_element.content.strip if cell_element.respond_to? :name
70
- else
71
- content = cell_element.content.strip
72
- case first_cell_text
73
- when "Available Balance:"
74
- account.available_balance = parse_currency(content) if content.match(/\d+/)
75
- when "Current Balance:"
76
- account.current_balance = parse_currency(content) if content.match(/\d+/)
77
- end
78
- end
79
- end
80
- end
81
-
82
- # Get all the transactions
83
- page.search('#ctlAccountActivityChecking tr').each do |row_element|
84
- next if row_element['class'] == 'header'
85
-
86
- data = row_element.css('td').map {|element| element.content.strip }
87
-
88
- transaction = Transaction.new
89
- transaction.posted_at = Date.strptime(data[0], '%m/%d/%Y')
90
- transaction.payee = unescape_html(data[3])
91
- transaction.status = :posted # :pending
92
- transaction.amount = -parse_currency(data[4]) if data[4].match(/\d+/)
93
- transaction.amount = parse_currency(data[5]) if data[5].match(/\d+/)
94
-
95
- transactions << transaction
96
- end
97
-
98
- transactions
99
- end
100
-
101
- private
102
-
103
- def ensure_authenticated
104
-
105
- # Check to see if already authenticated
106
- page = agent.get('https://pb.uccu.com/UCCU/Accounts/Activity.aspx')
107
- if page.body.include?("Please enter your User ID and Password below.") || page.body.include?("Your Online Banking session has expired.")
108
-
109
- raise InformationMissingError, "Please supply a username" unless self.username
110
- raise InformationMissingError, "Please supply a password" unless self.password
111
-
112
- @agent = Mechanize.new
113
-
114
- # Enter the username
115
- page = agent.get('https://pb.uccu.com/UCCU/Login.aspx')
116
- form = page.form('MAINFORM')
117
- form.field_with(:id => 'ctlSignon_txtUserID').value = username
118
- form.field_with(:id => 'ctlSignon_txtPassword').value = password
119
- form.field_with(:id => 'ctlSignon_ddlSignonDestination').value = 'Accounts.Overview'
120
- form.TestJavaScript = 'OK'
121
- login_button = form.button_with(:name => 'ctlSignon:btnLogin')
122
- page = form.submit(login_button)
123
-
124
- # If the supplied username/password is incorrect, raise an exception
125
- raise InformationMissingError, "Invalid username or password" if page.body.include?("Login not accepted.") || page.body.include?("Please enter a valid signon ID.") || page.body.include?("Please enter a valid Password.")
126
-
127
- # Secret questions???
128
-
129
- raise "Unknown URL reached. Try logging in manually through a browser." if page.uri.to_s != "https://pb.uccu.com/UCCU/Accounts/Overview.aspx"
130
- end
131
-
132
- true
133
- end
134
-
135
- end
136
- end
137
- end
1
+ require 'date'
2
+ require 'bigdecimal'
3
+
4
+ module Syrup
5
+ module Institutions
6
+ class Uccu < InstitutionBase
7
+
8
+ class << self
9
+ def name
10
+ "UCCU"
11
+ end
12
+
13
+ def id
14
+ "uccu"
15
+ end
16
+ end
17
+
18
+ def fetch_account(account_id)
19
+ fetch_accounts
20
+ end
21
+
22
+ def fetch_accounts
23
+ ensure_authenticated
24
+
25
+ # List accounts
26
+ page = agent.post('https://pb.uccu.com/UCCU/Ajax/RpcHandler.ashx',
27
+ '{"id":0,"method":"accounts.getBalances","params":[false]}',
28
+ 'X-JSON-RPC' => 'accounts.getBalances')
29
+
30
+ json = MultiJson.load(page.body)
31
+
32
+ accounts = []
33
+ json['result'].each do |account|
34
+ next if account['accountIndex'] == -1
35
+
36
+ new_account = Account.new(:id => account['accountIndex'], :institution => self)
37
+ new_account.name = unescape_html(account['displayName'][/^[^(]*/, 0].strip)
38
+ new_account.account_number = account['displayName'][/\(([*0-9-]+)\)/, 1]
39
+ new_account.current_balance = BigDecimal.new(account['current'].to_s)
40
+ new_account.available_balance = BigDecimal.new(account['available'].to_s)
41
+ # new_account.type = :deposit # :credit
42
+
43
+ accounts << new_account
44
+ end
45
+
46
+ accounts
47
+ end
48
+
49
+ def fetch_transactions(account_id, starting_at, ending_at)
50
+ ensure_authenticated
51
+
52
+ transactions = []
53
+
54
+ page = agent.get("https://pb.uccu.com/UCCU/Accounts/Activity.aspx?index=#{account_id}")
55
+ form = page.form("MAINFORM")
56
+ form.ddlAccounts = account_id
57
+ form.ddlType = 0 # 0 = All types of transactions
58
+ form.field_with(:id => 'txtFromDate_textBox').value = starting_at.month.to_s + '/' + starting_at.strftime('%e/%Y').strip
59
+ form.field_with(:id => 'txtToDate_textBox').value = ending_at.month.to_s + '/' + ending_at.strftime('%e/%Y').strip
60
+ submit_button = form.button_with(:name => 'btnSubmitHistoryRequest')
61
+ page = form.submit(submit_button)
62
+
63
+ # Look for the account balance
64
+ account = find_account_by_id(account_id)
65
+ page.search('.summaryTable tr').each do |row_element|
66
+ first_cell_text = ''
67
+ row_element.children.each do |cell_element|
68
+ if first_cell_text.empty?
69
+ first_cell_text = cell_element.content.strip if cell_element.respond_to? :name
70
+ else
71
+ content = cell_element.content.strip
72
+ case first_cell_text
73
+ when "Available Balance:"
74
+ account.available_balance = parse_currency(content) if content.match(/\d+/)
75
+ when "Current Balance:"
76
+ account.current_balance = parse_currency(content) if content.match(/\d+/)
77
+ end
78
+ end
79
+ end
80
+ end
81
+
82
+ # Get all the transactions
83
+ page.search('#ctlAccountActivityChecking tr').each do |row_element|
84
+ next if row_element['class'] == 'header'
85
+
86
+ data = row_element.css('td').map {|element| element.content.strip }
87
+
88
+ transaction = Transaction.new
89
+ transaction.posted_at = Date.strptime(data[0], '%m/%d/%Y')
90
+ transaction.payee = unescape_html(data[3])
91
+ transaction.status = :posted # :pending
92
+ transaction.amount = -parse_currency(data[4]) if data[4].match(/\d+/)
93
+ transaction.amount = parse_currency(data[5]) if data[5].match(/\d+/)
94
+
95
+ transactions << transaction
96
+ end
97
+
98
+ transactions
99
+ end
100
+
101
+ private
102
+
103
+ def ensure_authenticated
104
+
105
+ # Check to see if already authenticated
106
+ page = agent.get('https://pb.uccu.com/UCCU/Accounts/Activity.aspx')
107
+ if page.body.include?("Please enter your User ID and Password below.") || page.body.include?("Your Online Banking session has expired.")
108
+
109
+ raise InformationMissingError, "Please supply a username" unless self.username
110
+ raise InformationMissingError, "Please supply a password" unless self.password
111
+
112
+ @agent = Mechanize.new
113
+
114
+ # Enter the username
115
+ page = agent.get('https://pb.uccu.com/UCCU/Login.aspx')
116
+ form = page.form('MAINFORM')
117
+ form.field_with(:id => 'ctlSignon_txtUserID').value = username
118
+ form.field_with(:id => 'ctlSignon_txtPassword').value = password
119
+ form.field_with(:id => 'ctlSignon_ddlSignonDestination').value = 'Accounts.Overview'
120
+ form.TestJavaScript = 'OK'
121
+ login_button = form.button_with(:name => 'ctlSignon:btnLogin')
122
+ page = form.submit(login_button)
123
+
124
+ # If the supplied username/password is incorrect, raise an exception
125
+ raise InformationMissingError, "Invalid username or password" if page.body.include?("Login not accepted.") || page.body.include?("Please enter a valid signon ID.") || page.body.include?("Please enter a valid Password.")
126
+
127
+ # Secret questions???
128
+
129
+ if page.uri.to_s != "https://pb.uccu.com/UCCU/Accounts/Overview.aspx"
130
+ page = agent.get('https://pb.uccu.com/UCCU/Accounts/Activity.aspx')
131
+ if page.body.include?("Please enter your User ID and Password below.") || page.body.include?("Your Online Banking session has expired.")
132
+ raise "Unknown URL reached. Try logging in manually through a browser."
133
+ end
134
+ end
135
+ end
136
+
137
+ true
138
+ end
139
+
140
+ end
141
+ end
142
+ end