syrup 0.0.12 → 0.0.13

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.
@@ -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