aq_banking 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
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
@@ -0,0 +1,9 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --format documentation
2
+ --color
3
+ -f d
data/.ruby-version ADDED
@@ -0,0 +1 @@
1
+ ruby-2.3
data/.travis.yml ADDED
@@ -0,0 +1,7 @@
1
+ language: ruby
2
+ rvm:
3
+ - 2.3.0
4
+ - 2.2.2
5
+ - 2.1.5
6
+ before_install: gem install bundler -v 1.10.3
7
+ script: bundle exec rspec
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
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in aq_banking.gemspec
4
+ gemspec
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
@@ -0,0 +1,6 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task :default => :spec
@@ -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,12 @@
1
+ module AqBanking
2
+ BANKS = {
3
+ diba: {
4
+ server_url: 'https://fints.ing-diba.de/fints/',
5
+ hbci_version: '220'
6
+ },
7
+ comdirect: {
8
+ server_url: 'https://fints.comdirect.de/fints',
9
+ hbci_version: '300'
10
+ }
11
+ }
12
+ 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,7 @@
1
+ class AqBanking::Commander::SystemCmdError < StandardError
2
+ attr_reader :result
3
+
4
+ def initialize result
5
+ @result = result
6
+ end
7
+ 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,14 @@
1
+ module AqBanking
2
+ module Errors
3
+
4
+
5
+ class ParsingError < StandardError
6
+ attr_reader :response, :parser
7
+
8
+ def initialize(response, parser)
9
+ @response = response
10
+ @parser = parser
11
+ end
12
+ end
13
+ end
14
+ 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
@@ -0,0 +1,3 @@
1
+ module AqBanking
2
+ VERSION = "0.1.1"
3
+ 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: []