nordea-rb 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
data/LICENSE ADDED
@@ -0,0 +1,19 @@
1
+ Copyright (c) 2009 Martin Str�m
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ of this software and associated documentation files (the "Software"), to deal
5
+ in the Software without restriction, including without limitation the rights
6
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ copies of the Software, and to permit persons to whom the Software is
8
+ furnished to do so, subject to the following conditions:
9
+
10
+ The above copyright notice and this permission notice shall be included in
11
+ all copies or substantial portions of the Software.
12
+
13
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19
+ THE SOFTWARE.
data/README.markdown ADDED
@@ -0,0 +1,163 @@
1
+ # Nordea-rb
2
+
3
+
4
+ # First, Please note
5
+ I don't take no responsable at all if somethings messes up with your bank account or if you break
6
+ any terms of service (which this lib probably does). It has worked fine for me for a while though.
7
+
8
+
9
+ ## What?
10
+
11
+ nordea-rb is a small Ruby library to access your Nordea bank account and list balances etc.
12
+
13
+ It uses the WAP-version (https://mobil.nordea.se) to access the data. This version has many variables names etc
14
+ in Swedish so I suppose this only works for the Swedish version right now but it might be easy to adjust it if
15
+ other languages use the same system.
16
+
17
+ I've only tested it with my own login details so there might be issues if you have a different setup.
18
+
19
+
20
+ ## Storing the account details in Mac OS X's Keychain
21
+
22
+ On Mac OS X, the recommended way of storing your accounts details is in the built-in Keychain.
23
+ Here's how to set it up:
24
+
25
+ 1. Run the application "Keychain Access" (located in /Applications/Utilities)
26
+ 2. Add a new Password Item (File → New Password Item)
27
+ 3. You can use which name you'd like for "Keychain Item Name" but by default, nordea-rb will look for an item named "Nordea" (with capital "N")
28
+ 4. Enter your "personnummer" (Swedish SSN) as "Account Name"
29
+ 5. Enter your PIN code as "Password". (yes it indicates that its password strength is "weak")
30
+ 6. Save
31
+
32
+
33
+ ## Examples
34
+
35
+ Creating a new instance
36
+
37
+ # use the following account details
38
+ n = Nordea.new("1234567890", "1337")
39
+
40
+ # or using a hash
41
+ n = Nordea.new(:pnr => "1234567890", :pin => "1337")
42
+
43
+
44
+ Getting account details from the Keychain:
45
+
46
+ # use the default keychain and look for an account called "Nordea"
47
+ n = Nordea.new(:keychain)
48
+
49
+ # default keychain with an account named "My Nordea"
50
+ n = Nordea.new(:keychain, :label => 'My Nordea')
51
+
52
+ # Use another keychain file
53
+ n = Nordea.new(:keychain, :label => 'nordea-rb', :keychain_file => '/Users/me/Library/Keychains/login.keychain')
54
+
55
+
56
+ Logging in and out
57
+
58
+ # manually login and logout
59
+ n = Nordea.new(:keychain)
60
+ n.login
61
+ # ...
62
+ n.logout
63
+
64
+
65
+ # If you pass a block, then it will login and logout automatically
66
+ Nordea.new("1234567890", "1337") do |n|
67
+ # no need to login or out
68
+ end
69
+
70
+ Listing your accounts
71
+
72
+ Nordea.new(...) do |n|
73
+ puts "you have #{n.accounts.size} account(s)"
74
+ end
75
+
76
+ Accessing your account details
77
+
78
+ Nordea.new(...) do |n|
79
+ account = n.accounts.first
80
+ another_account = n.accounts['Savings']
81
+ puts another_account.name # => "Savings"
82
+ puts account.balance, account.currency, account.to_s
83
+
84
+ # You can also access accounts using a regexp:
85
+ puts n.accounts[/saving/i]
86
+ end
87
+
88
+ Getting a list of transactions for an account
89
+
90
+ Nordea.new(...) do |n|
91
+ savings = n.accounts['Savings']
92
+ puts savings.transactions.size # => displays 20 per page, paging is not yet supported
93
+
94
+ salary_account = n.accounts['Salary']
95
+ latest = salary_account.transactions.first
96
+
97
+ puts latest.text # => the text on the transaction
98
+ puts latest.amount # => -2000.0
99
+ puts latest.date # => 2009-01-02
100
+ puts latest.withdrawal? # => true
101
+ puts latest.deposit? # => false
102
+ end
103
+
104
+ Transfer money between your own accounts:
105
+
106
+ Nordea.new(...) do |n|
107
+ savings = n.accounts['Savings']
108
+ salary_account = n.accounts['Salary']
109
+
110
+ # transfer 10 SEK from the savings account to the salary account
111
+ savings.withdraw(10.0, 'SEK', :deposit_to => salary_account)
112
+
113
+ # basically the same transaction:
114
+ salary_account.deposit(10.0, 'SEK', :withdraw_from => savings)
115
+ end
116
+
117
+
118
+ ## Command Line Tool
119
+
120
+ The command line tool is pretty basic right now and it only prints the given account(s) balance(s).
121
+ Improving it to do more things (like list transactions etc) would just be a matter of polishing the
122
+ option parser which I throw together quickly.
123
+
124
+ Show the balances for the savings account
125
+
126
+ ./nordea --pnr=1234567890 --pin=0987 Savings
127
+
128
+ Show the balances for the accounts named "Savings" and "Salary". Login from keychain
129
+
130
+ ./nordea --keychain Savings Salary
131
+
132
+
133
+ ## TODO
134
+
135
+ - Support for transfering money between your own accounts
136
+ - Clean up the API and class architecture
137
+ - Add support for paging in transactions list
138
+ - Build an iPhone optimized web app which uses nordea-rb as backend so we finally can have a better mobile bank
139
+
140
+
141
+ ## Credits and license
142
+
143
+ By [Martin Ström](http://twitter.com/haraldmartin) under the MIT license:
144
+
145
+ > Copyright (c) 2009 Martin Ström
146
+ >
147
+ > Permission is hereby granted, free of charge, to any person obtaining a copy
148
+ > of this software and associated documentation files (the "Software"), to deal
149
+ > in the Software without restriction, including without limitation the rights
150
+ > to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
151
+ > copies of the Software, and to permit persons to whom the Software is
152
+ > furnished to do so, subject to the following conditions:
153
+ >
154
+ > The above copyright notice and this permission notice shall be included in
155
+ > all copies or substantial portions of the Software.
156
+ >
157
+ > THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
158
+ > IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
159
+ > FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
160
+ > AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
161
+ > LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
162
+ > OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
163
+ > THE SOFTWARE.
data/Rakefile ADDED
@@ -0,0 +1,25 @@
1
+ require "rubygems"
2
+ require "rake/testtask"
3
+
4
+ task :default => :test
5
+
6
+ Rake::TestTask.new do |t|
7
+ t.libs << "test"
8
+ t.test_files = FileList["test/test_*.rb"]
9
+ t.verbose = true
10
+ end
11
+
12
+ begin
13
+ require 'jeweler'
14
+ Jeweler::Tasks.new do |gemspec|
15
+ gemspec.name = "nordea-rb"
16
+ gemspec.summary = "Ruby library for accessing your Nordea Bank account and transferring money between your own accounts."
17
+ gemspec.description = "Ruby library for accessing your Nordea Bank account and transferring money between your own accounts."
18
+ gemspec.email = "name@my-domain.se"
19
+ gemspec.homepage = "http://github.com/haraldmartin/nordea-rb"
20
+ gemspec.authors = ["Martin Ström"]
21
+ end
22
+ Jeweler::GemcutterTasks.new
23
+ rescue LoadError
24
+ puts "Jeweler not available. Install it with: gem install jeweler"
25
+ end
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.2.0
data/bin/nordea ADDED
@@ -0,0 +1,66 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'optparse'
4
+ require File.join(File.dirname(__FILE__), *%w".. lib nordea")
5
+
6
+ options, accounts, pnr, command = {}, []
7
+
8
+ opts = OptionParser.new do |opts|
9
+ opts.banner = "Usage: nordea [options]"
10
+
11
+ def opts.show_usage
12
+ puts self
13
+ exit
14
+ end
15
+
16
+ opts.on('-p', '--pnr=PERSONNUMMER', "Login using the PERSONNUMMER") do |p|
17
+ pnr = p
18
+ end
19
+
20
+ opts.on('-i', '--pin=PIN', "Login using the PIN code") do |pin|
21
+ options = pin
22
+ end
23
+
24
+ opts.on('-k', '--keychain', "Get account details from the default keychain. See README how to set this up") do
25
+ pnr = :keychain
26
+ end
27
+
28
+ opts.on('-f', '--keychain-file=FILE', "Use the keychain FILE") do |file|
29
+ options[:keychain_file] = file
30
+ end
31
+
32
+ opts.on('-l', '--keychain-label=LABEL', "Look for a keychain item named LABEL") do |label|
33
+ options[:label] = label
34
+ end
35
+
36
+ opts.on('-b', '--balance', "Print the account's balance") do
37
+ # right now balance is the only supported command
38
+ end
39
+
40
+ opts.on_tail("-h", "--help", "Shows this help message") { opts.show_usage }
41
+ opts.on_tail("-v", "--version", "Shows version") do
42
+ puts Nordea::Version::STRING
43
+ exit
44
+ end
45
+
46
+ begin
47
+ opts.order(ARGV) { |a| accounts << a }
48
+ rescue OptionParser::ParseError => e
49
+ opts.warn e.message
50
+ opts.show_usage
51
+ end
52
+
53
+ end
54
+
55
+ opts.parse!
56
+ opts.show_usage unless pnr && accounts.length > 0
57
+
58
+ begin
59
+ Nordea.new(pnr, options) do |n|
60
+ accounts.each do |account|
61
+ puts n.accounts[/#{account}/i].to_s
62
+ end
63
+ end
64
+ rescue Nordea::InvalidLogin => e
65
+ puts "Invalid login: #{e.message}"
66
+ end
@@ -0,0 +1,42 @@
1
+ module Nordea
2
+ class Request
3
+ HOST_NAME = 'gfs.nb.se'
4
+ HOST_PORT = 443
5
+ SCRIPT_NAME = '/bin2/gfskod'
6
+ USER_AGENT = 'Nokia9110/1.0'
7
+
8
+ def initialize(command, extra_params = {})
9
+ @command, @extra_params = command, extra_params
10
+ request
11
+ end
12
+
13
+ def connection
14
+ http = Net::HTTP.new(HOST_NAME, HOST_PORT)
15
+ http.use_ssl = true
16
+ http
17
+ end
18
+
19
+ def parse_xml
20
+ Hpricot.XML(response)
21
+ end
22
+
23
+ def request
24
+ res = connection.post(SCRIPT_NAME, query, headers)
25
+ @result = res.body
26
+ end
27
+
28
+ def headers
29
+ { "User-Agent" => USER_AGENT }
30
+ end
31
+
32
+ def response
33
+ @result
34
+ end
35
+
36
+ def query
37
+ @extra_params.merge({ "OBJECT" => @command }).
38
+ inject([]) { |all, (key, value)| all << "#{key}=#{value}" }.
39
+ join("&")
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,102 @@
1
+ module Nordea
2
+ class Account < Resource
3
+ def initialize(fields = {}, command_params = {}, session = nil)
4
+ @name, @balance, @currency, @index = fields[:name], fields[:balance], fields[:currency], fields[:index]
5
+ @command_params = command_params
6
+ @session = session
7
+ end
8
+
9
+ attr_accessor :name, :balance, :currency, :index, :session
10
+
11
+ def account_type_name; 'konton' end
12
+
13
+ def to_command_params
14
+ @command_params
15
+ end
16
+
17
+ # TODO move shared transaction fetching to Nordea::Resource and call super
18
+ def transactions(reload = false)
19
+ @transactions = nil if reload
20
+ @transactions ||= begin
21
+ doc = session.request(Commands::TRANSACTIONS, to_command_params).parse_xml
22
+ doc.search('go[@href="#trans"]').inject([]) do |all, node|
23
+ all << Transaction.new_from_xml(node)
24
+ end
25
+ end
26
+ end
27
+
28
+ def to_s
29
+ "#{name}: #{balance_to_s} #{currency}"
30
+ end
31
+
32
+ def balance_to_s(decimals = 2)
33
+ %Q{%.#{decimals}f} % balance
34
+ end
35
+
36
+ def self.new_from_xml(xml_node, session)
37
+ xml_node = Hpricot(xml_node) unless xml_node.class.to_s =~ /^Hpricot::/
38
+ xml_node = xml_node.at("anchor") unless xml_node.name == 'anchor'
39
+ new _setup_fields(xml_node), _setup_command_params(xml_node), session
40
+ end
41
+
42
+ def withdraw(amount, currency = 'SEK', options = {})
43
+ options.symbolize_keys!
44
+ to = options[:deposit_to]
45
+
46
+ raise ArgumentError, "options[:deposit_to] must be a Nordea::Account" unless to.is_a?(Nordea::Account)
47
+
48
+ currency, amount = currency.to_s, amount.to_s.sub(".", ",")
49
+ from_account_info = [index, currency, name].join(":")
50
+ to_account_info = [to.index, to.currency, to.name].join(":")
51
+
52
+ params = { :from_account_info => from_account_info,
53
+ :to_account_info => to_account_info,
54
+ :amount => amount,
55
+ :currency_code => currency }
56
+
57
+ session.request(Nordea::Commands::TRANSFER_TO_OWN_ACCOUNT_PHASE_1, params)
58
+ session.request(Nordea::Commands::TRANSFER_TO_OWN_ACCOUNT_PHASE_2, params)
59
+ session.request(Nordea::Commands::TRANSFER_TO_OWN_ACCOUNT_PHASE_3, {
60
+ :currency_code => currency,
61
+ :from_account_number => index,
62
+ :from_account_name => name,
63
+ :to_account_number => to.index,
64
+ :to_account_name => to.name,
65
+ :amount => amount,
66
+ :exchange_rate => "0",
67
+ :from_currency_code => currency,
68
+ :to_currency_code => currency
69
+ })
70
+
71
+ session.accounts(true)
72
+ end
73
+
74
+ def deposit(amount, currency = 'SEK', options = {})
75
+ options.symbolize_keys!
76
+ from = options.delete(:withdraw_from)
77
+
78
+ raise ArgumentError unless from.is_a?(Nordea::Account)
79
+
80
+ from.withdraw(amount, currency, options.merge(:deposit_to => self))
81
+ end
82
+
83
+ private
84
+
85
+ def self._setup_fields(xml_node)
86
+ name = xml_node.at("postfield[@name='account_name']")['value']
87
+ currency = xml_node.at("postfield[@name='account_currency_code']")['value']
88
+ index = xml_node.at("postfield[@name='account_index']")['value']
89
+ balance = xml_node.next_sibling.next
90
+ { :name => name, :currency => currency,
91
+ :balance => Support.dirty_currency_string_to_f(balance), :index => index }
92
+ end
93
+
94
+ def self._setup_command_params(xml_node)
95
+ command_params = (xml_node/'postfield').to_a.inject({}) do |all, postfield|
96
+ name = postfield['name']
97
+ all[name] = postfield['value'] unless name =~ /sid|OBJECT/
98
+ all
99
+ end
100
+ end
101
+ end
102
+ end
@@ -0,0 +1,5 @@
1
+ module Nordea
2
+ class Card < Resource
3
+ def account_type_name; 'kort' end
4
+ end
5
+ end
@@ -0,0 +1,5 @@
1
+ module Nordea
2
+ class Fund < Resource
3
+ def account_type_name; 'fonder' end
4
+ end
5
+ end
@@ -0,0 +1,5 @@
1
+ module Nordea
2
+ class Loan < Resource
3
+ def account_type_name; "lan" end
4
+ end
5
+ end
@@ -0,0 +1,7 @@
1
+ module Nordea
2
+ class Resource
3
+ def request_xml
4
+ # Request.new("KF00TW", "account_type_name" => account_type_name)
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,17 @@
1
+ require 'nordea/resources/resource'
2
+ require 'nordea/resources/account'
3
+ require 'nordea/resources/card'
4
+ require 'nordea/resources/fund'
5
+ require 'nordea/resources/loan'
6
+
7
+ class ResourceCollection < Array
8
+ def [](account_name)
9
+ if account_name.is_a?(String)
10
+ detect { |e| e.name == account_name }
11
+ elsif account_name.is_a?(Regexp)
12
+ detect { |e| e.name =~ account_name }
13
+ else
14
+ super
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,63 @@
1
+ module Nordea
2
+ class Session
3
+ attr_accessor :pnr, :pin, :token
4
+ attr_reader :num_requests
5
+
6
+ def initialize(pnr, pin, &block)
7
+ @pnr, @pin = pnr, pin
8
+ @num_requests = 0
9
+ if block_given?
10
+ login
11
+ yield self
12
+ logout
13
+ end
14
+ end
15
+
16
+ def login
17
+ doc = request(Commands::LOGIN_PHASE_1, { "kundnr" => pnr, "pinkod" => pin, "sid" => "0" }).parse_xml
18
+ begin
19
+ @token = (doc/'*[@name=sid]').attr("value")
20
+ contract = (doc/'postfield[@name=contract]').attr("value")
21
+ rescue Exception => e
22
+ if node = doc.at("card[@title='Fel']")
23
+ error_message = node.at("p").inner_text.strip
24
+ else
25
+ error_message = ""
26
+ end
27
+ raise Nordea::InvalidLogin, error_message
28
+ end
29
+ request Commands::LOGIN_PHASE_2, "bank" => "mobile_light", "no_prev" => "1", "contract" => contract
30
+ end
31
+
32
+ alias_method :login!, :login
33
+
34
+ def logout
35
+ request Commands::LOGOUT
36
+ end
37
+
38
+ alias_method :logout!, :logout
39
+
40
+ def request(command, extra_params = {})
41
+ extra_params.symbolize_keys!
42
+ extra_params.merge!(:sid => token) unless extra_params.has_key?(:sid)
43
+ @num_requests += 1
44
+ Nordea::Request.new(command, extra_params)
45
+ end
46
+
47
+ def accounts(reload = false)
48
+ @accounts = nil if reload
49
+ @accounts ||= setup_resources(Account)
50
+ end
51
+
52
+ private
53
+
54
+ def setup_resources(klass)
55
+ type = klass.new.account_type_name
56
+ doc = request(Commands::RESOURCE, "account_type_name" => type).parse_xml
57
+ (doc/"go postfield[@name='OBJECT'][@value='#{Commands::TRANSACTIONS}']").
58
+ inject(ResourceCollection.new) do |all, field|
59
+ all << klass.new_from_xml(field.parent.parent, self)
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,63 @@
1
+ module Nordea
2
+ class Support
3
+ def self.dirty_currency_string_to_f(string)
4
+ string.to_s.strip.gsub(/(\302|\240)/, "").gsub('.', '').gsub(',', '.').delete('SEK').to_f
5
+ end
6
+ end
7
+ end
8
+
9
+ def quietly
10
+ v = $VERBOSE
11
+ $VERBOSE = nil
12
+ yield
13
+ ensure
14
+ $VERBOSE = v
15
+ end
16
+
17
+ # quietly { OpenSSL::SSL::VERIFY_PEER = OpenSSL::SSL::VERIFY_NONE }
18
+
19
+ class Net::HTTP
20
+ alias_method :old_initialize, :initialize
21
+ def initialize(*args)
22
+ old_initialize(*args)
23
+ @ssl_context = OpenSSL::SSL::SSLContext.new
24
+ @ssl_context.verify_mode = OpenSSL::SSL::VERIFY_NONE
25
+ end
26
+ end
27
+
28
+ class Symbol
29
+ def to_proc
30
+ Proc.new { |obj, *args| obj.send(self, *args) }
31
+ end
32
+ end
33
+
34
+ # from ActiveSupport
35
+ class Hash
36
+ def stringify_keys
37
+ inject({}) do |options, (key, value)|
38
+ options[key.to_s] = value
39
+ options
40
+ end
41
+ end
42
+
43
+ def stringify_keys!
44
+ keys.each do |key|
45
+ self[key.to_s] = delete(key)
46
+ end
47
+ self
48
+ end
49
+
50
+ def symbolize_keys
51
+ inject({}) do |options, (key, value)|
52
+ options[(key.to_sym rescue key) || key] = value
53
+ options
54
+ end
55
+ end
56
+
57
+ def symbolize_keys!
58
+ self.replace(self.symbolize_keys)
59
+ end
60
+
61
+ alias_method :to_options, :symbolize_keys
62
+ alias_method :to_options!, :symbolize_keys!
63
+ end
@@ -0,0 +1,27 @@
1
+ require 'date'
2
+
3
+ module Nordea
4
+ class Transaction
5
+ def initialize(date, amount, text)
6
+ @date, @amount, @text = date, amount, text
7
+ end
8
+
9
+ attr_accessor :date, :amount, :text
10
+
11
+ def withdrawal?
12
+ amount < 0
13
+ end
14
+
15
+ def deposit?
16
+ amount > 0
17
+ end
18
+
19
+ def self.new_from_xml(xml)
20
+ node = xml.is_a?(String) ? Hpricot.XML(xml) : xml
21
+ date = Date.parse(node.at("setvar[@name='date']")['value'])
22
+ amount = Support.dirty_currency_string_to_f(node.at("setvar[@name='amount']")['value'])
23
+ text = node.at("setvar[@name='text']")['value']
24
+ new date, amount, text
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,9 @@
1
+ module Nordea
2
+ module Version
3
+ MAJOR = 0
4
+ MINOR = 2
5
+ TINY = 0
6
+
7
+ STRING = [MAJOR, MINOR, TINY].join(".")
8
+ end
9
+ end