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 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: []