aq_banking 0.1.1
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.
- checksums.yaml +7 -0
- data/.gitignore +9 -0
- data/.rspec +3 -0
- data/.ruby-version +1 -0
- data/.travis.yml +7 -0
- data/Dockerfile +15 -0
- data/Gemfile +4 -0
- data/README.md +31 -0
- data/Rakefile +6 -0
- data/aq_banking.gemspec +31 -0
- data/exe/aq_banking +12 -0
- data/lib/aq_banking/account_info_list.tt +41 -0
- data/lib/aq_banking/banks.rb +12 -0
- data/lib/aq_banking/cli.rb +55 -0
- data/lib/aq_banking/commander/result.rb +20 -0
- data/lib/aq_banking/commander/system_cmd_error.rb +7 -0
- data/lib/aq_banking/commander.rb +137 -0
- data/lib/aq_banking/errors.rb +14 -0
- data/lib/aq_banking/node_extensions.rb +153 -0
- data/lib/aq_banking/parsed/account.rb +65 -0
- data/lib/aq_banking/parsed/define_field.rb +15 -0
- data/lib/aq_banking/parsed/status.rb +15 -0
- data/lib/aq_banking/parsed/transaction.rb +26 -0
- data/lib/aq_banking/parser.rb +32 -0
- data/lib/aq_banking/version.rb +3 -0
- data/lib/aq_banking.rb +30 -0
- metadata +209 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 37e0c9634716c12e46c6d08dbc994d31b6937883
|
4
|
+
data.tar.gz: b971e91d93635c789d92dde7b83074e0a73dc4a7
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: b6528e939651d57a2aae7ca9bb2f7f4131085b4deab5dbec1ae9db4956ee907bfd07bed3534642b6c057722b436590b66b0d7a4c9e5fc6074ff9a50a3e619981
|
7
|
+
data.tar.gz: 3478fd205987fc55e7e5ca1b714f5b655ab111dd3dc66e0ac8fdc28087a0587dcbbc38db8f2d762d6079e14540bc6cd69f814008da5b42ffc66e81003118deb9
|
data/.gitignore
ADDED
data/.rspec
ADDED
data/.ruby-version
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
ruby-2.3
|
data/.travis.yml
ADDED
data/Dockerfile
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
FROM ruby:2.2.2
|
2
|
+
MAINTAINER Robin Wenglewski, robin@wenglewski.de
|
3
|
+
|
4
|
+
RUN apt-get update -qq && apt-get install -y \
|
5
|
+
vim-nox \
|
6
|
+
aqbanking-tools \
|
7
|
+
ktoblzcheck
|
8
|
+
|
9
|
+
RUN mkdir /gem
|
10
|
+
WORKDIR /gem
|
11
|
+
|
12
|
+
COPY . /gem
|
13
|
+
RUN bundle install
|
14
|
+
|
15
|
+
CMD bundle exec rspec
|
data/Gemfile
ADDED
data/README.md
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
# AqBanking
|
2
|
+
|
3
|
+
[](https://travis-ci.org/rweng/aq_banking)
|
4
|
+
[](http://codecov.io/github/rweng/aq_banking?branch=master)
|
5
|
+
|
6
|
+
## Installation
|
7
|
+
|
8
|
+
Add this line to your application's Gemfile:
|
9
|
+
|
10
|
+
```ruby
|
11
|
+
gem 'aq_banking', github: 'rweng/aq_banking'
|
12
|
+
```
|
13
|
+
|
14
|
+
And then execute:
|
15
|
+
|
16
|
+
$ bundle
|
17
|
+
|
18
|
+
## Tests
|
19
|
+
|
20
|
+
### Docker
|
21
|
+
|
22
|
+
docker build -t rweng/aq_banking .
|
23
|
+
docker run -t --rm -v $(pwd):/gem rweng/aq_banking
|
24
|
+
|
25
|
+
# or to login and run tests manually
|
26
|
+
docker run -it --rm -v $(pwd):/gem rweng/aq_banking bash
|
27
|
+
|
28
|
+
## Contributing
|
29
|
+
|
30
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/rweng/aq_banking.
|
31
|
+
|
data/Rakefile
ADDED
data/aq_banking.gemspec
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'aq_banking/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "aq_banking"
|
8
|
+
spec.version = AqBanking::VERSION
|
9
|
+
spec.authors = ["Robin Wenglewski"]
|
10
|
+
spec.email = ["robin@wenglewski.de"]
|
11
|
+
|
12
|
+
spec.summary = %q{wrapper around aqbanking cli tools}
|
13
|
+
spec.homepage = "https://github.com/rweng/aq_banking"
|
14
|
+
|
15
|
+
spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
16
|
+
spec.bindir = "exe"
|
17
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
18
|
+
spec.require_paths = ["lib"]
|
19
|
+
|
20
|
+
spec.add_dependency "thor"
|
21
|
+
spec.add_dependency "treetop"
|
22
|
+
spec.add_dependency "activesupport"
|
23
|
+
spec.add_dependency "highline", "~> 1.7.8"
|
24
|
+
|
25
|
+
spec.add_development_dependency "bundler", "~> 1.10"
|
26
|
+
spec.add_development_dependency "rake", "~> 10.0"
|
27
|
+
spec.add_development_dependency "rspec"
|
28
|
+
spec.add_development_dependency "pry"
|
29
|
+
spec.add_development_dependency "codecov"
|
30
|
+
spec.add_development_dependency "factory_girl"
|
31
|
+
end
|
data/exe/aq_banking
ADDED
@@ -0,0 +1,12 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
$LOAD_PATH.unshift File.expand_path('../lib', __dir__)
|
4
|
+
|
5
|
+
require 'aq_banking'
|
6
|
+
begin
|
7
|
+
AqBanking::Cli.start(ARGV)
|
8
|
+
rescue StandardError => e
|
9
|
+
require 'highline'
|
10
|
+
HighLine.color_scheme = HighLine::SampleColorScheme.new
|
11
|
+
HighLine.new.say HighLine.color(e.message, :error)
|
12
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
grammar AccountInfoList
|
2
|
+
rule block
|
3
|
+
sn word S '{' sN block_content '}' sn <Block>
|
4
|
+
end
|
5
|
+
|
6
|
+
rule block_content
|
7
|
+
(field / block)*
|
8
|
+
end
|
9
|
+
|
10
|
+
rule field
|
11
|
+
('char' / 'int') S word '="' field_value '"' sN <Field>
|
12
|
+
end
|
13
|
+
|
14
|
+
rule field_value
|
15
|
+
(!('"' sN) .)*
|
16
|
+
end
|
17
|
+
|
18
|
+
rule s
|
19
|
+
S?
|
20
|
+
end
|
21
|
+
|
22
|
+
rule S
|
23
|
+
[ \t]+
|
24
|
+
end
|
25
|
+
|
26
|
+
rule sn
|
27
|
+
sN?
|
28
|
+
end
|
29
|
+
|
30
|
+
rule sN
|
31
|
+
( ( S "\n" / s comment_to_eol / s "\n" ) s comment_to_eol? )+
|
32
|
+
end
|
33
|
+
|
34
|
+
rule comment_to_eol
|
35
|
+
'#' (!"\n" .)* "\n"
|
36
|
+
end
|
37
|
+
|
38
|
+
rule word
|
39
|
+
([a-zA-Z]+)
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
require 'thor'
|
2
|
+
|
3
|
+
module AqBanking
|
4
|
+
class Cli < Thor
|
5
|
+
class_option :dry, :type => :boolean, default: false, aliases: '-d'
|
6
|
+
class_option :verbose, :type => :boolean, default: false, aliases: '-v'
|
7
|
+
|
8
|
+
option :bank
|
9
|
+
option :server_url
|
10
|
+
option :hbci_version
|
11
|
+
desc 'request BANK_CODE ACCOUNT_NUMBER USER_ID PASSWORD', 'requests balance and transactions for an account. you must either specify --bank= or (--server-url and maybe --hbci-version)'
|
12
|
+
def request(bank_code, account_number, user_id, password)
|
13
|
+
response = commander.send_request!({
|
14
|
+
server_url: server_url,
|
15
|
+
hbci_version: hbci_version,
|
16
|
+
bank_code: bank_code,
|
17
|
+
account_number: account_number,
|
18
|
+
user_id: user_id,
|
19
|
+
password: password
|
20
|
+
})
|
21
|
+
|
22
|
+
account = parser.parse_account_list(response).first
|
23
|
+
|
24
|
+
puts account.to_json
|
25
|
+
end
|
26
|
+
|
27
|
+
desc 'list_banks', 'shows valid values for the --bank flag'
|
28
|
+
def list_banks
|
29
|
+
puts BANKS.keys.map(&:to_s).join('\n')
|
30
|
+
end
|
31
|
+
|
32
|
+
private
|
33
|
+
def commander
|
34
|
+
@commander ||= Commander.new
|
35
|
+
end
|
36
|
+
|
37
|
+
def parser
|
38
|
+
@parser ||= Parser.new
|
39
|
+
end
|
40
|
+
|
41
|
+
def server_url
|
42
|
+
options[:server_url] || bank.try(:[], :server_url) || raise(ArgumentError.new("server_url is required. specifiy --bank or --server-url"))
|
43
|
+
end
|
44
|
+
|
45
|
+
def hbci_version
|
46
|
+
options[:hbci_version] || bank.try(:[], :hbci_version) || raise(ArgumentError.new("hbci_version is required. specifiy --bank or --hbci-version"))
|
47
|
+
end
|
48
|
+
|
49
|
+
def bank
|
50
|
+
return unless options[:bank]
|
51
|
+
|
52
|
+
BANKS[options[:bank].to_sym] || raise(ArgumentError.new("bank \"#{options[:bank]}\" not found"))
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
class AqBanking::Commander::Result
|
2
|
+
attr_reader :stdout, :stderr, :status
|
3
|
+
delegate :success?, :exitstatus, to: :status
|
4
|
+
|
5
|
+
def initialize stdout, stderr, status
|
6
|
+
@stdout = stdout
|
7
|
+
@stderr = stderr
|
8
|
+
@status = status
|
9
|
+
end
|
10
|
+
|
11
|
+
def to_s
|
12
|
+
<<-END
|
13
|
+
status: #{status.exitstatus}
|
14
|
+
stdout:
|
15
|
+
#{stdout}
|
16
|
+
stderr:
|
17
|
+
#{stderr}
|
18
|
+
END
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,137 @@
|
|
1
|
+
require 'open3'
|
2
|
+
require 'timeout'
|
3
|
+
|
4
|
+
module AqBanking
|
5
|
+
class Commander
|
6
|
+
autoload :Result, File.expand_path('commander/result', __dir__)
|
7
|
+
autoload :SystemCmdError, File.expand_path('commander/system_cmd_error', __dir__)
|
8
|
+
|
9
|
+
DEFAULT_TIMEOUT = 120
|
10
|
+
|
11
|
+
delegate :logger, to: AqBanking
|
12
|
+
|
13
|
+
# @param [String] bank_code
|
14
|
+
# @param [String] account_number
|
15
|
+
# @param [String] pin_file path to pin
|
16
|
+
# @param [Date] from
|
17
|
+
# @param [Date] to
|
18
|
+
#
|
19
|
+
# @return [String]
|
20
|
+
# @raise [SystemCmdError]
|
21
|
+
def send_request(bank_code, account_number, pin_file, from: nil, to: nil)
|
22
|
+
cmd = "#{aq_cli} -P #{pin_file} request -b #{bank_code} -a #{account_number}"
|
23
|
+
cmd << " --fromdate=#{from.strftime("%Y%m%d")}" if from
|
24
|
+
cmd << " --todate=#{to.strftime("%Y%m%d")}" if to
|
25
|
+
cmd << " --transactions --balance"
|
26
|
+
|
27
|
+
result = run_or_raise cmd, 'request failed'
|
28
|
+
|
29
|
+
result.stdout
|
30
|
+
end
|
31
|
+
|
32
|
+
def with_pin bank_code, user_id, password, &block
|
33
|
+
pin_file = build_pin_file bank_code: bank_code, user_id: user_id, password: password
|
34
|
+
|
35
|
+
block.call(pin_file.path) if block
|
36
|
+
ensure
|
37
|
+
pin_file.close!
|
38
|
+
end
|
39
|
+
|
40
|
+
# sends a request, but creates the account and fetches the sysid if neccessary
|
41
|
+
#
|
42
|
+
# @return [String]
|
43
|
+
# @raise [SystemCmdError]
|
44
|
+
def send_request!(server_url:, hbci_version: '220', bank_code:, user_id:, password:, account_number:, user_name: '', from: nil, to: nil)
|
45
|
+
raise ArgumentError.new("unknown hbci_version: \"#{hbci_version}\"") unless HBCI_VERSIONS.include? hbci_version
|
46
|
+
raise ArgumentError.new("server url must be present") unless server_url.present?
|
47
|
+
|
48
|
+
with_pin bank_code, user_id, password do |pin_file_path|
|
49
|
+
unless account_exists? bank_code: bank_code, user_id: user_id
|
50
|
+
add_account(server_url: server_url, hbci_version: hbci_version,
|
51
|
+
bank_code: bank_code, user_id: user_id, user_name: user_name)
|
52
|
+
|
53
|
+
call_getsysid pin_file: pin_file_path, user_id: user_id, bank_code: bank_code
|
54
|
+
end
|
55
|
+
|
56
|
+
send_request(bank_code, account_number, pin_file_path, from: from, to: to)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
# @return [Boolean]
|
61
|
+
def account_valid?(bank_code:, account_number:)
|
62
|
+
result = run "ktoblzcheck #{bank_code} #{account_number}"
|
63
|
+
result.success?
|
64
|
+
end
|
65
|
+
|
66
|
+
# @param [String] bank_code
|
67
|
+
# @param [String] user_id
|
68
|
+
# @return [Boolean]
|
69
|
+
def account_exists?(bank_code:, user_id:)
|
70
|
+
result = run "aqhbci-tool4 listusers"
|
71
|
+
result.stdout.include?("Bank: de/#{bank_code} User Id: #{user_id}")
|
72
|
+
end
|
73
|
+
|
74
|
+
def add_account(server_url:, hbci_version:, bank_code:, user_id:, user_name: '')
|
75
|
+
cmd = ""
|
76
|
+
cmd << "#{aq_hbci} adduser -t pintan --context=1"
|
77
|
+
cmd << " -b #{bank_code} -u #{user_id}"
|
78
|
+
cmd << %Q( -N "#{user_name}")
|
79
|
+
cmd << %Q( --hbciversion=#{hbci_version})
|
80
|
+
cmd << %Q( -s "#{server_url}")
|
81
|
+
|
82
|
+
run_or_raise(cmd, 'could not add account').success?
|
83
|
+
end
|
84
|
+
|
85
|
+
def call_getsysid(pin_file:, user_id:, bank_code:)
|
86
|
+
cmd = "#{aq_hbci} -P #{pin_file} getsysid -u #{user_id} -b #{bank_code}"
|
87
|
+
|
88
|
+
run_or_raise(cmd, 'could not get sysid')
|
89
|
+
end
|
90
|
+
|
91
|
+
private
|
92
|
+
# @param [String] cmd
|
93
|
+
# @param [Float, Integer] timeout in seconds
|
94
|
+
# @return [AqBanking::Commander::Result]
|
95
|
+
# @raise [Timeout::Error]
|
96
|
+
def run(cmd, timeout: DEFAULT_TIMEOUT)
|
97
|
+
logger.debug "run: #{cmd}"
|
98
|
+
result = nil
|
99
|
+
|
100
|
+
Timeout::timeout(timeout) do
|
101
|
+
result = Result.new( *Open3.capture3(cmd) )
|
102
|
+
end
|
103
|
+
|
104
|
+
logger.debug result.to_s
|
105
|
+
|
106
|
+
result
|
107
|
+
end
|
108
|
+
|
109
|
+
def run_or_raise cmd, msg
|
110
|
+
result = run cmd
|
111
|
+
raise SystemCmdError.new(result), msg if not result.success? or special_error?(result)
|
112
|
+
result
|
113
|
+
end
|
114
|
+
|
115
|
+
# aqbanking-cli sometimes exits with 0 though it actually was unsuccessful.
|
116
|
+
# this method checks for these cases
|
117
|
+
def special_error? result
|
118
|
+
result.stderr.include?('Error executing outbox')
|
119
|
+
end
|
120
|
+
|
121
|
+
# @return [Tempfile]
|
122
|
+
def build_pin_file(bank_code:, user_id:, password:)
|
123
|
+
str = "PIN_#{bank_code}_#{user_id} = \"#{password}\"\n"
|
124
|
+
Tempfile.new('pin').tap do |tmp_file|
|
125
|
+
(tmp_file << str).rewind
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
def aq_hbci
|
130
|
+
'aqhbci-tool4 -A -n'
|
131
|
+
end
|
132
|
+
|
133
|
+
def aq_cli
|
134
|
+
'aqbanking-cli -A -n'
|
135
|
+
end
|
136
|
+
end
|
137
|
+
end
|
@@ -0,0 +1,153 @@
|
|
1
|
+
module AccountInfoList
|
2
|
+
class AmbiguousPathError < StandardError
|
3
|
+
attr_reader :path
|
4
|
+
|
5
|
+
def initialize path
|
6
|
+
@path = path
|
7
|
+
end
|
8
|
+
|
9
|
+
def message
|
10
|
+
"ambiguous path at: #{@path}"
|
11
|
+
end
|
12
|
+
|
13
|
+
def inspect
|
14
|
+
"#<#{self.class.name}: #{message}>"
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
class Block < Treetop::Runtime::SyntaxNode
|
19
|
+
DEFAULT_PATH_SEPARATOR = ' '
|
20
|
+
|
21
|
+
def inspect
|
22
|
+
"#{self.class.name}(name: #{name})"
|
23
|
+
end
|
24
|
+
|
25
|
+
# @return [String]
|
26
|
+
def name
|
27
|
+
elements[1].text_value
|
28
|
+
end
|
29
|
+
|
30
|
+
# @return [Array<AccountInfoList::Block>]
|
31
|
+
def fields
|
32
|
+
content_elements.select{|e| e.is_a? Field }
|
33
|
+
end
|
34
|
+
|
35
|
+
# @return [Array<AccountInfoList::Block>]
|
36
|
+
def blocks
|
37
|
+
content_elements.select{|e| e.is_a? Block}
|
38
|
+
end
|
39
|
+
|
40
|
+
# traverses the tree to return the block or fields at the given path
|
41
|
+
#
|
42
|
+
# e.g query "path to blocks"
|
43
|
+
# e.g query "path to blocks[1] otherblock"
|
44
|
+
# e.g query "path/to/blocks[1]/otherblock", separator: '/'
|
45
|
+
#
|
46
|
+
# @param [String, Symbol, Array<String>] path
|
47
|
+
# @param [String] separator split by this if given path is a String
|
48
|
+
# @return [Array<AccountInfoList::Block>]
|
49
|
+
# @raise AmbiguousPathError
|
50
|
+
# @raise ArgumentError if path is empty
|
51
|
+
def query(path, separator: DEFAULT_PATH_SEPARATOR)
|
52
|
+
[*(get_field(path, separator: separator))] + get_blocks(path, separator: separator)
|
53
|
+
end
|
54
|
+
|
55
|
+
# @see {#query}
|
56
|
+
def get_field(path, separator: DEFAULT_PATH_SEPARATOR)
|
57
|
+
path = parse_path(path, separator: separator)
|
58
|
+
field_name = path.pop
|
59
|
+
|
60
|
+
final_blocks = path.length == 0 ? [self] : get_blocks(path, separator: separator)
|
61
|
+
|
62
|
+
raise AmbiguousPathError.new(path.join(separator)) if final_blocks.length > 1
|
63
|
+
|
64
|
+
return [] if final_blocks.empty?
|
65
|
+
|
66
|
+
# return first matched field of final block
|
67
|
+
final_blocks.first.fields.select { |f| f.name.to_sym == field_name.to_sym }.first
|
68
|
+
end
|
69
|
+
|
70
|
+
# @see {#query}
|
71
|
+
def get_blocks(path, separator: DEFAULT_PATH_SEPARATOR)
|
72
|
+
path = parse_path(path, separator: separator)
|
73
|
+
|
74
|
+
# process first part of path
|
75
|
+
current_path_part = path.shift
|
76
|
+
match = current_path_part.match /^(?<name>\w+)(\[(?<index>\d+)\])?$/
|
77
|
+
name = match[:name] or raise ArgumentError('could not read name from ' + path.first)
|
78
|
+
index = match[:index].try(:to_i)
|
79
|
+
|
80
|
+
# find blocks with that name
|
81
|
+
found_blocks = blocks.select { |b| b.name.to_sym == name.to_sym }
|
82
|
+
|
83
|
+
# put only the block at index in found_blocks if index is set
|
84
|
+
found_blocks = [*(found_blocks[index])] if index.present?
|
85
|
+
|
86
|
+
# return found_blocks if path is at the end or we didn't find anything
|
87
|
+
return found_blocks if path.empty? or found_blocks.empty?
|
88
|
+
|
89
|
+
raise AmbiguousPathError.new(name) if found_blocks.length > 1
|
90
|
+
|
91
|
+
# recursion
|
92
|
+
begin
|
93
|
+
found_blocks.first.get_blocks path, separator: separator
|
94
|
+
rescue AmbiguousPathError => e
|
95
|
+
raise AmbiguousPathError.new([current_path_part, e.path].join(separator))
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
# @return [Time]
|
100
|
+
def as_datetime
|
101
|
+
raise 'date must be in utc' unless get_field('inUtc').value == '1'
|
102
|
+
|
103
|
+
Time.utc(
|
104
|
+
get_field(%i(date year)).value.to_i,
|
105
|
+
get_field(%i(date month)).value.to_i,
|
106
|
+
get_field(%i(date day)).value.to_i,
|
107
|
+
get_field(%i(time hour)).value.to_i,
|
108
|
+
get_field(%i(time min)).value.to_i,
|
109
|
+
get_field(%i(time sec)).value.to_i
|
110
|
+
)
|
111
|
+
end
|
112
|
+
|
113
|
+
private
|
114
|
+
def parse_path path, separator: DEFAULT_PATH_SEPARATOR
|
115
|
+
path = path.to_s if path.is_a? Symbol
|
116
|
+
path = path.split(separator) if path.is_a? String
|
117
|
+
path = path.map &:to_s
|
118
|
+
|
119
|
+
raise ArgumentError('path is empty') if path.empty?
|
120
|
+
|
121
|
+
path
|
122
|
+
end
|
123
|
+
|
124
|
+
def content_elements
|
125
|
+
elements[5].elements
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
class Field < Treetop::Runtime::SyntaxNode
|
130
|
+
def name
|
131
|
+
elements[2].text_value
|
132
|
+
end
|
133
|
+
|
134
|
+
def type
|
135
|
+
elements[0].text_value
|
136
|
+
end
|
137
|
+
|
138
|
+
def value
|
139
|
+
elements[4].text_value
|
140
|
+
end
|
141
|
+
|
142
|
+
def as_float
|
143
|
+
unless match = value.match(/^(-?\d+)(%2F(\d+))?$/)
|
144
|
+
AqBanking.logger.warn "as_float could not be matched: #{value}"
|
145
|
+
return nil
|
146
|
+
end
|
147
|
+
|
148
|
+
value = match.captures.first
|
149
|
+
divider = match.captures[2] || 1
|
150
|
+
value.to_f / divider.to_i
|
151
|
+
end
|
152
|
+
end
|
153
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
require 'json'
|
2
|
+
|
3
|
+
module AqBanking::Parsed
|
4
|
+
class Account
|
5
|
+
extend DefineField
|
6
|
+
|
7
|
+
attr_reader :tree
|
8
|
+
define_field :iban, :bic, :owner, :currency, :bank_code, :bank_name
|
9
|
+
define_field :number, path: 'accountNumber'
|
10
|
+
define_field :name, path: 'accountName'
|
11
|
+
define_field :type, path: 'accountType'
|
12
|
+
define_field :id, path: 'accountId'
|
13
|
+
|
14
|
+
def initialize tree
|
15
|
+
raise ArgumentError.new('tree must be an AccountInfoList::Block') unless tree.is_a? AccountInfoList::Block
|
16
|
+
raise ArgumentError.new('block_name must be accountInfo') unless tree.name == 'accountInfo'
|
17
|
+
|
18
|
+
@tree = tree
|
19
|
+
end
|
20
|
+
|
21
|
+
def statuses
|
22
|
+
raise 'tree must be set' if tree.nil?
|
23
|
+
|
24
|
+
tree.query('statusList status').map { |block| Status.new(block) }.sort_by(&:time).reverse
|
25
|
+
end
|
26
|
+
|
27
|
+
# some banks return multiple statuses containing multiple booked_balance and noted balance. When called on the acccount,
|
28
|
+
# it uses the last status (ordered by status.time) that contains a booked_balance / noted_balance
|
29
|
+
def booked_balance
|
30
|
+
first_status_with :booked_balance
|
31
|
+
end
|
32
|
+
|
33
|
+
# @see booked_balane
|
34
|
+
def noted_balance
|
35
|
+
first_status_with :noted_balance
|
36
|
+
end
|
37
|
+
|
38
|
+
def transactions
|
39
|
+
tree.get_blocks('transactionList transaction').map do |block|
|
40
|
+
Transaction.new(block)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def to_h
|
45
|
+
result = %i(iban bic owner currency bank_code bank_name booked_balance noted_balance number name type id).inject({}) do |result, field|
|
46
|
+
result[field] = send(field)
|
47
|
+
result
|
48
|
+
end
|
49
|
+
|
50
|
+
result[:transactions] = transactions.map &:to_h
|
51
|
+
result
|
52
|
+
end
|
53
|
+
|
54
|
+
def to_json
|
55
|
+
JSON.pretty_generate(to_h)
|
56
|
+
end
|
57
|
+
|
58
|
+
private
|
59
|
+
def first_status_with field
|
60
|
+
statuses.reduce(nil) do |result, status|
|
61
|
+
result.nil? ? status.send(field) : result
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
module AqBanking::Parsed::DefineField
|
2
|
+
def define_field *fields, path: nil, default: nil, &transform_block
|
3
|
+
fields.each do |field_name|
|
4
|
+
define_method field_name do
|
5
|
+
raise 'tree must be set' if tree.nil?
|
6
|
+
|
7
|
+
default_block = lambda {|field| field.value }
|
8
|
+
default_path = field_name.to_s.camelize(:lower).to_sym
|
9
|
+
|
10
|
+
field = tree.query(path || default_path).first
|
11
|
+
field.present? ? (transform_block || default_block).call(field) : default
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
module AqBanking::Parsed
|
2
|
+
class Status
|
3
|
+
extend DefineField
|
4
|
+
|
5
|
+
attr_reader :tree
|
6
|
+
|
7
|
+
define_field :booked_balance, path: 'bookedBalance value value', &:as_float
|
8
|
+
define_field :noted_balance, path: 'notedBalance value value', &:as_float
|
9
|
+
define_field(:time, path: 'time') { |v| v.value.to_i }
|
10
|
+
|
11
|
+
def initialize(transaction_block)
|
12
|
+
@tree = transaction_block
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
module AqBanking::Parsed
|
2
|
+
class Transaction
|
3
|
+
extend DefineField
|
4
|
+
|
5
|
+
attr_reader :tree
|
6
|
+
|
7
|
+
define_field :purpose, :local_bank_code, :local_account_number, :local_name, :remote_bank_code,
|
8
|
+
:remote_account_number, :remote_name
|
9
|
+
define_field :currency, path: 'value currency'
|
10
|
+
define_field :amount, path: 'value value', &:as_float
|
11
|
+
define_field :date, &:as_datetime
|
12
|
+
define_field :valuta_date, &:as_datetime
|
13
|
+
|
14
|
+
def initialize(transaction_block)
|
15
|
+
@tree = transaction_block
|
16
|
+
end
|
17
|
+
|
18
|
+
def to_h
|
19
|
+
%i(local_bank_code local_account_number local_name remote_bank_code remote_account_number remote_name
|
20
|
+
currency amount date valuta_date).reduce({}) do |result, field|
|
21
|
+
result[field] = send(field)
|
22
|
+
result
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
require 'treetop'
|
2
|
+
require 'aq_banking/node_extensions'
|
3
|
+
require 'aq_banking/account_info_list'
|
4
|
+
|
5
|
+
module AqBanking
|
6
|
+
class Parser
|
7
|
+
|
8
|
+
# @param [String] block_str
|
9
|
+
# @return [AqBanking::Parsed::Account]
|
10
|
+
def parse_account_list(block_str)
|
11
|
+
tree = parse_block block_str
|
12
|
+
tree.blocks.map do |account_block|
|
13
|
+
AqBanking::Parsed::Account.new account_block
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
# @param [String] block_str
|
18
|
+
# @return []
|
19
|
+
def parse_block(block_str)
|
20
|
+
parser = AccountInfoListParser.new
|
21
|
+
tree = parser.parse(block_str)
|
22
|
+
|
23
|
+
# If the AST is nil then there was an error during parsing
|
24
|
+
# we need to report a simple error message to help the user
|
25
|
+
if tree.nil?
|
26
|
+
raise Errors::ParsingError.new(block_str, parser), parser.failure_reason
|
27
|
+
end
|
28
|
+
|
29
|
+
tree
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
data/lib/aq_banking.rb
ADDED
@@ -0,0 +1,30 @@
|
|
1
|
+
require 'logger'
|
2
|
+
require 'active_support/core_ext/object/try'
|
3
|
+
require 'active_support/core_ext/string'
|
4
|
+
require 'active_support/core_ext/module/delegation'
|
5
|
+
|
6
|
+
module AqBanking
|
7
|
+
HBCI_VERSIONS = %w(220 300)
|
8
|
+
|
9
|
+
autoload :VERSION, 'aq_banking/version'
|
10
|
+
autoload :BANKS, 'aq_banking/banks'
|
11
|
+
autoload :Errors, 'aq_banking/errors'
|
12
|
+
autoload :Parser, 'aq_banking/parser'
|
13
|
+
autoload :Commander, 'aq_banking/commander'
|
14
|
+
autoload :Cli, 'aq_banking/cli'
|
15
|
+
|
16
|
+
module Parsed
|
17
|
+
autoload :Status, 'aq_banking/parsed/status'
|
18
|
+
autoload :Account, 'aq_banking/parsed/account'
|
19
|
+
autoload :Transaction, 'aq_banking/parsed/transaction'
|
20
|
+
autoload :DefineField, 'aq_banking/parsed/define_field'
|
21
|
+
end
|
22
|
+
|
23
|
+
class << self
|
24
|
+
attr_writer :logger
|
25
|
+
|
26
|
+
def logger
|
27
|
+
@logger ||= Logger.new(STDOUT)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
metadata
ADDED
@@ -0,0 +1,209 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: aq_banking
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Robin Wenglewski
|
8
|
+
autorequire:
|
9
|
+
bindir: exe
|
10
|
+
cert_chain: []
|
11
|
+
date: 2016-04-22 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: thor
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: treetop
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: activesupport
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: highline
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: 1.7.8
|
62
|
+
type: :runtime
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: 1.7.8
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: bundler
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - "~>"
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '1.10'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - "~>"
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '1.10'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: rake
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - "~>"
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '10.0'
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - "~>"
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '10.0'
|
97
|
+
- !ruby/object:Gem::Dependency
|
98
|
+
name: rspec
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - ">="
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: '0'
|
104
|
+
type: :development
|
105
|
+
prerelease: false
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - ">="
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: '0'
|
111
|
+
- !ruby/object:Gem::Dependency
|
112
|
+
name: pry
|
113
|
+
requirement: !ruby/object:Gem::Requirement
|
114
|
+
requirements:
|
115
|
+
- - ">="
|
116
|
+
- !ruby/object:Gem::Version
|
117
|
+
version: '0'
|
118
|
+
type: :development
|
119
|
+
prerelease: false
|
120
|
+
version_requirements: !ruby/object:Gem::Requirement
|
121
|
+
requirements:
|
122
|
+
- - ">="
|
123
|
+
- !ruby/object:Gem::Version
|
124
|
+
version: '0'
|
125
|
+
- !ruby/object:Gem::Dependency
|
126
|
+
name: codecov
|
127
|
+
requirement: !ruby/object:Gem::Requirement
|
128
|
+
requirements:
|
129
|
+
- - ">="
|
130
|
+
- !ruby/object:Gem::Version
|
131
|
+
version: '0'
|
132
|
+
type: :development
|
133
|
+
prerelease: false
|
134
|
+
version_requirements: !ruby/object:Gem::Requirement
|
135
|
+
requirements:
|
136
|
+
- - ">="
|
137
|
+
- !ruby/object:Gem::Version
|
138
|
+
version: '0'
|
139
|
+
- !ruby/object:Gem::Dependency
|
140
|
+
name: factory_girl
|
141
|
+
requirement: !ruby/object:Gem::Requirement
|
142
|
+
requirements:
|
143
|
+
- - ">="
|
144
|
+
- !ruby/object:Gem::Version
|
145
|
+
version: '0'
|
146
|
+
type: :development
|
147
|
+
prerelease: false
|
148
|
+
version_requirements: !ruby/object:Gem::Requirement
|
149
|
+
requirements:
|
150
|
+
- - ">="
|
151
|
+
- !ruby/object:Gem::Version
|
152
|
+
version: '0'
|
153
|
+
description:
|
154
|
+
email:
|
155
|
+
- robin@wenglewski.de
|
156
|
+
executables:
|
157
|
+
- aq_banking
|
158
|
+
extensions: []
|
159
|
+
extra_rdoc_files: []
|
160
|
+
files:
|
161
|
+
- ".gitignore"
|
162
|
+
- ".rspec"
|
163
|
+
- ".ruby-version"
|
164
|
+
- ".travis.yml"
|
165
|
+
- Dockerfile
|
166
|
+
- Gemfile
|
167
|
+
- README.md
|
168
|
+
- Rakefile
|
169
|
+
- aq_banking.gemspec
|
170
|
+
- exe/aq_banking
|
171
|
+
- lib/aq_banking.rb
|
172
|
+
- lib/aq_banking/account_info_list.tt
|
173
|
+
- lib/aq_banking/banks.rb
|
174
|
+
- lib/aq_banking/cli.rb
|
175
|
+
- lib/aq_banking/commander.rb
|
176
|
+
- lib/aq_banking/commander/result.rb
|
177
|
+
- lib/aq_banking/commander/system_cmd_error.rb
|
178
|
+
- lib/aq_banking/errors.rb
|
179
|
+
- lib/aq_banking/node_extensions.rb
|
180
|
+
- lib/aq_banking/parsed/account.rb
|
181
|
+
- lib/aq_banking/parsed/define_field.rb
|
182
|
+
- lib/aq_banking/parsed/status.rb
|
183
|
+
- lib/aq_banking/parsed/transaction.rb
|
184
|
+
- lib/aq_banking/parser.rb
|
185
|
+
- lib/aq_banking/version.rb
|
186
|
+
homepage: https://github.com/rweng/aq_banking
|
187
|
+
licenses: []
|
188
|
+
metadata: {}
|
189
|
+
post_install_message:
|
190
|
+
rdoc_options: []
|
191
|
+
require_paths:
|
192
|
+
- lib
|
193
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
194
|
+
requirements:
|
195
|
+
- - ">="
|
196
|
+
- !ruby/object:Gem::Version
|
197
|
+
version: '0'
|
198
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
199
|
+
requirements:
|
200
|
+
- - ">="
|
201
|
+
- !ruby/object:Gem::Version
|
202
|
+
version: '0'
|
203
|
+
requirements: []
|
204
|
+
rubyforge_project:
|
205
|
+
rubygems_version: 2.5.1
|
206
|
+
signing_key:
|
207
|
+
specification_version: 4
|
208
|
+
summary: wrapper around aqbanking cli tools
|
209
|
+
test_files: []
|