nordea-rb 0.2.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/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