syrup 0.0.8 → 0.0.9

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