aq_banking 0.1.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
+
[![Travis Status](https://travis-ci.org/rweng/aq_banking.svg)](https://travis-ci.org/rweng/aq_banking)
|
4
|
+
[![codecov.io](http://codecov.io/github/rweng/aq_banking/coverage.svg?branch=master)](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: []
|