ynab_convert 1.0.7 → 2.0.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 +4 -4
- data/.gitignore +5 -0
- data/.rubocop.yml +10 -2
- data/Gemfile.lock +37 -12
- data/Guardfile +1 -29
- data/README.md +82 -7
- data/lib/ynab_convert/api_clients/api_client.rb +24 -0
- data/lib/ynab_convert/api_clients/currency_api.rb +66 -0
- data/lib/ynab_convert/documents/statements/example_statement.rb +16 -0
- data/lib/ynab_convert/documents/statements/n26_statement.rb +24 -0
- data/lib/ynab_convert/documents/statements/statement.rb +39 -0
- data/lib/ynab_convert/documents/statements/ubs_chequing_statement.rb +20 -0
- data/lib/ynab_convert/documents/statements/ubs_credit_statement.rb +19 -0
- data/lib/ynab_convert/documents/statements/wise_statement.rb +17 -0
- data/lib/ynab_convert/documents/ynab4_files/ynab4_file.rb +58 -0
- data/lib/ynab_convert/documents.rb +17 -0
- data/lib/ynab_convert/logger.rb +1 -1
- data/lib/ynab_convert/processors/example_processor.rb +24 -0
- data/lib/ynab_convert/processors/n26_processor.rb +26 -0
- data/lib/ynab_convert/processors/processor.rb +75 -0
- data/lib/ynab_convert/processors/ubs_chequing_processor.rb +21 -0
- data/lib/ynab_convert/processors/ubs_credit_processor.rb +17 -0
- data/lib/ynab_convert/processors/wise_processor.rb +19 -0
- data/lib/ynab_convert/processors.rb +2 -2
- data/lib/ynab_convert/transformers/cleaners/cleaner.rb +17 -0
- data/lib/ynab_convert/transformers/cleaners/n26_cleaner.rb +13 -0
- data/lib/ynab_convert/transformers/cleaners/ubs_chequing_cleaner.rb +98 -0
- data/lib/ynab_convert/transformers/cleaners/ubs_credit_cleaner.rb +45 -0
- data/lib/ynab_convert/transformers/cleaners/wise_cleaner.rb +39 -0
- data/lib/ynab_convert/transformers/enhancers/enhancer.rb +20 -0
- data/lib/ynab_convert/transformers/enhancers/n26_enhancer.rb +74 -0
- data/lib/ynab_convert/transformers/enhancers/wise_enhancer.rb +87 -0
- data/lib/ynab_convert/transformers/formatters/example_formatter.rb +12 -0
- data/lib/ynab_convert/transformers/formatters/formatter.rb +91 -0
- data/lib/ynab_convert/transformers/formatters/n26_formatter.rb +19 -0
- data/lib/ynab_convert/transformers/formatters/ubs_chequing_formatter.rb +12 -0
- data/lib/ynab_convert/transformers/formatters/ubs_credit_formatter.rb +12 -0
- data/lib/ynab_convert/transformers/formatters/wise_formatter.rb +35 -0
- data/lib/ynab_convert/transformers.rb +18 -0
- data/lib/ynab_convert/validators/ynab4_row_validator.rb +83 -0
- data/lib/ynab_convert/validators.rb +9 -0
- data/lib/ynab_convert/version.rb +1 -1
- data/lib/ynab_convert.rb +22 -3
- data/ynab_convert.gemspec +4 -0
- metadata +91 -8
- data/lib/ynab_convert/processor/base.rb +0 -226
- data/lib/ynab_convert/processor/example.rb +0 -124
- data/lib/ynab_convert/processor/n26.rb +0 -70
- data/lib/ynab_convert/processor/revolut.rb +0 -103
- data/lib/ynab_convert/processor/ubs_chequing.rb +0 -115
- data/lib/ynab_convert/processor/ubs_credit.rb +0 -83
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: b10705667e423e280b312986f423664a84450c6d7aa9f47e8cd99063b8c653c7
|
4
|
+
data.tar.gz: 87563fa9a29b8a01145e6406d713114ad275a2f33ab9b27c64bc771a4379f34e
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 2cd94babe8d7aef861dcfc84d85ba0401b5807af87a3df4c17724663f69ec0d756288929f80089ce7e083068e0d7cc2de4fa45df6767651dbb4929797d59006a
|
7
|
+
data.tar.gz: 11562fdf39e805dce87016f28b5f442a5de8716dbda6a5e06d7f0b7208f1ee84f08cdae17908de72c418b221d71a561a34e584b0c2ab7d29c604b19c1b57cc9e
|
data/.gitignore
CHANGED
data/.rubocop.yml
CHANGED
@@ -1,14 +1,22 @@
|
|
1
1
|
---
|
2
2
|
# See https://github.com/rubocop/rubocop/blob/master/config/default.yml for all
|
3
3
|
# options
|
4
|
-
require:
|
4
|
+
require:
|
5
|
+
- rubocop-rake
|
6
|
+
- rubocop-rspec
|
5
7
|
|
6
8
|
AllCops:
|
7
9
|
DisplayCopNames: true
|
10
|
+
NewCops: enable
|
11
|
+
TargetRubyVersion: 2.6
|
8
12
|
|
9
|
-
|
13
|
+
Layout/LineLength:
|
14
|
+
AllowHeredoc: true
|
15
|
+
AllowURI: true
|
16
|
+
AutoCorrect: true
|
10
17
|
Exclude:
|
11
18
|
- ynab_convert.gemspec
|
19
|
+
Max: 80
|
12
20
|
|
13
21
|
Metrics/BlockLength:
|
14
22
|
Exclude:
|
data/Gemfile.lock
CHANGED
@@ -1,18 +1,23 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
ynab_convert (
|
4
|
+
ynab_convert (2.0.1)
|
5
5
|
i18n
|
6
6
|
slop
|
7
|
+
timecop
|
7
8
|
|
8
9
|
GEM
|
9
10
|
remote: https://rubygems.org/
|
10
11
|
specs:
|
11
|
-
|
12
|
+
addressable (2.8.0)
|
13
|
+
public_suffix (>= 2.0.2, < 5.0)
|
14
|
+
ast (2.4.2)
|
12
15
|
backport (1.1.2)
|
13
16
|
byebug (11.0.1)
|
14
17
|
coderay (1.1.2)
|
15
18
|
concurrent-ruby (1.1.9)
|
19
|
+
crack (0.4.5)
|
20
|
+
rexml
|
16
21
|
diff-lcs (1.3)
|
17
22
|
docile (1.3.2)
|
18
23
|
ffi (1.11.2)
|
@@ -34,6 +39,7 @@ GEM
|
|
34
39
|
guard-rubocop (1.3.0)
|
35
40
|
guard (~> 2.0)
|
36
41
|
rubocop (~> 0.20)
|
42
|
+
hashdiff (1.0.1)
|
37
43
|
htmlentities (4.3.4)
|
38
44
|
i18n (1.10.0)
|
39
45
|
concurrent-ruby (~> 1.0)
|
@@ -52,23 +58,26 @@ GEM
|
|
52
58
|
notiffany (0.1.3)
|
53
59
|
nenv (~> 0.1)
|
54
60
|
shellany (~> 0.0)
|
55
|
-
parallel (1.
|
56
|
-
parser (2.
|
57
|
-
ast (~> 2.4.
|
61
|
+
parallel (1.21.0)
|
62
|
+
parser (2.7.2.0)
|
63
|
+
ast (~> 2.4.1)
|
58
64
|
pry (0.12.2)
|
59
65
|
coderay (~> 1.1.0)
|
60
66
|
method_source (~> 0.9.0)
|
61
67
|
pry-byebug (3.7.0)
|
62
68
|
byebug (~> 11.0)
|
63
69
|
pry (~> 0.10)
|
70
|
+
public_suffix (4.0.6)
|
64
71
|
racc (1.6.0)
|
65
|
-
rainbow (3.
|
72
|
+
rainbow (3.1.1)
|
66
73
|
rake (13.0.1)
|
67
74
|
rb-fsevent (0.10.3)
|
68
75
|
rb-inotify (0.10.0)
|
69
76
|
ffi (~> 1.0)
|
77
|
+
regexp_parser (2.2.1)
|
70
78
|
reverse_markdown (1.3.0)
|
71
79
|
nokogiri
|
80
|
+
rexml (3.2.5)
|
72
81
|
rspec (3.9.0)
|
73
82
|
rspec-core (~> 3.9.0)
|
74
83
|
rspec-expectations (~> 3.9.0)
|
@@ -82,16 +91,23 @@ GEM
|
|
82
91
|
diff-lcs (>= 1.2.0, < 2.0)
|
83
92
|
rspec-support (~> 3.9.0)
|
84
93
|
rspec-support (3.9.0)
|
85
|
-
rubocop (0.
|
86
|
-
jaro_winkler (~> 1.5.1)
|
94
|
+
rubocop (0.93.1)
|
87
95
|
parallel (~> 1.10)
|
88
|
-
parser (>= 2.
|
96
|
+
parser (>= 2.7.1.5)
|
89
97
|
rainbow (>= 2.2.2, < 4.0)
|
98
|
+
regexp_parser (>= 1.8)
|
99
|
+
rexml
|
100
|
+
rubocop-ast (>= 0.6.0)
|
90
101
|
ruby-progressbar (~> 1.7)
|
91
|
-
unicode-display_width (>= 1.4.0, <
|
102
|
+
unicode-display_width (>= 1.4.0, < 2.0)
|
103
|
+
rubocop-ast (1.4.1)
|
104
|
+
parser (>= 2.7.1.5)
|
92
105
|
rubocop-rake (0.5.0)
|
93
106
|
rubocop
|
94
|
-
|
107
|
+
rubocop-rspec (1.44.1)
|
108
|
+
rubocop (~> 0.87)
|
109
|
+
rubocop-ast (>= 0.7.1)
|
110
|
+
ruby-progressbar (1.11.0)
|
95
111
|
shellany (0.0.1)
|
96
112
|
simplecov (0.17.1)
|
97
113
|
docile (~> 1.1)
|
@@ -113,7 +129,13 @@ GEM
|
|
113
129
|
yard (~> 0.9)
|
114
130
|
thor (0.20.3)
|
115
131
|
tilt (2.0.10)
|
116
|
-
|
132
|
+
timecop (0.9.5)
|
133
|
+
unicode-display_width (1.8.0)
|
134
|
+
vcr (6.1.0)
|
135
|
+
webmock (3.14.0)
|
136
|
+
addressable (>= 2.8.0)
|
137
|
+
crack (>= 0.3.2)
|
138
|
+
hashdiff (>= 0.4.0, < 2.0.0)
|
117
139
|
yard (0.9.20)
|
118
140
|
|
119
141
|
PLATFORMS
|
@@ -129,8 +151,11 @@ DEPENDENCIES
|
|
129
151
|
rspec-core
|
130
152
|
rubocop
|
131
153
|
rubocop-rake
|
154
|
+
rubocop-rspec
|
132
155
|
simplecov
|
133
156
|
solargraph
|
157
|
+
vcr
|
158
|
+
webmock
|
134
159
|
ynab_convert!
|
135
160
|
|
136
161
|
BUNDLED WITH
|
data/Guardfile
CHANGED
@@ -44,37 +44,9 @@ group :red_green_refactor, halt_on_fail: true do
|
|
44
44
|
# Ruby files
|
45
45
|
ruby = dsl.ruby
|
46
46
|
dsl.watch_spec_files_for(ruby.lib_files)
|
47
|
-
|
48
|
-
# Rails files
|
49
|
-
rails = dsl.rails(view_extensions: %w[erb haml slim])
|
50
|
-
dsl.watch_spec_files_for(rails.app_files)
|
51
|
-
dsl.watch_spec_files_for(rails.views)
|
52
|
-
|
53
|
-
watch(rails.controllers) do |m|
|
54
|
-
[
|
55
|
-
rspec.spec.call("routing/#{m[1]}_routing"),
|
56
|
-
rspec.spec.call("controllers/#{m[1]}_controller"),
|
57
|
-
rspec.spec.call("acceptance/#{m[1]}")
|
58
|
-
]
|
59
|
-
end
|
60
|
-
|
61
|
-
# Rails config changes
|
62
|
-
watch(rails.spec_helper) { rspec.spec_dir }
|
63
|
-
watch(rails.routes) { "#{rspec.spec_dir}/routing" }
|
64
|
-
watch(rails.app_controller) { "#{rspec.spec_dir}/controllers" }
|
65
|
-
|
66
|
-
# Capybara features specs
|
67
|
-
watch(rails.view_dirs) { |m| rspec.spec.call("features/#{m[1]}") }
|
68
|
-
watch(rails.layouts) { |m| rspec.spec.call("features/#{m[1]}") }
|
69
|
-
|
70
|
-
# Turnip features and steps
|
71
|
-
watch(%r{^spec/acceptance/(.+)\.feature$})
|
72
|
-
watch(%r{^spec/acceptance/steps/(.+)_steps\.rb$}) do |m|
|
73
|
-
Dir[File.join("**/#{m[1]}.feature")][0] || 'spec/acceptance'
|
74
|
-
end
|
75
47
|
end
|
76
48
|
|
77
|
-
guard :rubocop, cli: ['--auto-correct', '--display-cop-names'] do
|
49
|
+
guard :rubocop, cli: ['--auto-correct-all', '--display-cop-names'] do
|
78
50
|
watch('Gemfile')
|
79
51
|
watch('Rakefile')
|
80
52
|
watch('bin/convert')
|
data/README.md
CHANGED
@@ -27,35 +27,110 @@ latest one on 2019-12-01.
|
|
27
27
|
`-i` argument | Institution's full name | Institution's website | Remarks
|
28
28
|
---|---|---|---
|
29
29
|
`example` | Example Bank | N/A | Reference processor implementation, not a real institution
|
30
|
-
`n26` | N26 | [n26.com](n26.com) | N26 CSV statements
|
31
|
-
`revolut` | Revolut Ltd | [revolut.com](https://www.revolut.com/) | The processor isn't aware of currencies. Make sure the statements processed with `revolut` are in the same currency that your YNAB is in
|
30
|
+
`n26` | N26 | [n26.com](n26.com) | N26 CSV statements, will convert EUR amounts to CHF (hardcoded for now)
|
32
31
|
`ubs_chequing` | UBS Switzerland (private banking) | [ubs.ch](https://ubs.ch) | Private chequing and joint accounts
|
33
32
|
`ubs_credit` | UBS Switzerland (credit cards) | [ubs.ch](https://ubs.ch) | Both MasterCard and Visa
|
33
|
+
`wise` | Wise (Transferwise) cards | [wise.com](https://wise.com) | Performs currency conversion (hardcoded to CHF for now)
|
34
34
|
|
35
35
|
## Contributing
|
36
36
|
|
37
37
|
After checking out the repo, run `bin/setup` to install dependencies. Then, run
|
38
38
|
`rake spec` to run the tests. You can also run `bin/console` for an interactive
|
39
|
-
prompt that will allow you to experiment.
|
39
|
+
prompt that will allow you to experiment. To run Rubocop and RSpec in watch
|
40
|
+
mode, use `bundle exec guard`.
|
40
41
|
|
41
42
|
To install this gem onto your local machine, run `bundle exec rake install`.
|
43
|
+
Alternatively, the gem can also be run from `bin/ynab_convert`.
|
42
44
|
|
43
45
|
Bug reports and pull requests are welcome on GitHub at
|
44
46
|
https://github.com/coaxial/ynab_convert.
|
45
47
|
|
46
|
-
###
|
48
|
+
### Architecture
|
49
|
+
|
50
|
+
Here is the class diagram:
|
51
|
+
```mermaid
|
52
|
+
classDiagram
|
53
|
+
Documents <|-- Statement
|
54
|
+
Documents <|-- YNAB4File
|
55
|
+
Transformers <|-- Cleaner
|
56
|
+
Transformers <|-- Formatter
|
57
|
+
Transformers <|-- Enhancer
|
58
|
+
Validators <|-- YNAB4Row
|
59
|
+
Processors <|-- Processor
|
60
|
+
|
61
|
+
class Statement{
|
62
|
+
#Hash csv_import_options
|
63
|
+
#String filepath
|
64
|
+
#String institution_name
|
65
|
+
}
|
66
|
+
|
67
|
+
class YNAB4File{
|
68
|
+
#Hash csv_export_options
|
69
|
+
#String filename
|
70
|
+
#update_dates(row)
|
71
|
+
}
|
72
|
+
|
73
|
+
class Processor{
|
74
|
+
#to_ynab!()
|
75
|
+
}
|
76
|
+
|
77
|
+
class Cleaner{
|
78
|
+
#run(row)
|
79
|
+
}
|
80
|
+
|
81
|
+
class Enhancer{
|
82
|
+
#run(row)
|
83
|
+
}
|
84
|
+
|
85
|
+
class Formatter{
|
86
|
+
#run(row)
|
87
|
+
}
|
88
|
+
|
89
|
+
class YNAB4Row{
|
90
|
+
+valid?(row)
|
91
|
+
}
|
92
|
+
```
|
93
|
+
|
94
|
+
Each financial institution gets its own class for most of these base classes.
|
95
|
+
|
96
|
+
For instance, adding "Some Bank" would require creating the following new
|
97
|
+
classes:
|
98
|
+
|
99
|
+
- `class Processors::SomeBank < Processor`
|
100
|
+
- `class Transformers::Cleaners::SomeBank < Cleaner`
|
101
|
+
- `class Transformers::Formatter::SomeBank < Formatter`
|
102
|
+
- `class Transformers::Enhancer::SomeBank < Enhancer`
|
103
|
+
- `class Documents::Statements::SomeBank < Statement`
|
104
|
+
|
105
|
+
Each of these classes would implement the expected interface for its type, and
|
106
|
+
the `Processor::SomeBank` would instantiate them all. `Validators` and `YNAB4File`
|
107
|
+
aren't related to a particular institution, there is no need to derive a child
|
108
|
+
class for each bank.
|
109
|
+
|
110
|
+
Note that any of the `Transformers::` classes are optional, and it is possible
|
111
|
+
that some institution only requires a `Cleaner` but no `Formatter` or
|
112
|
+
`Enhancer` (for example).
|
113
|
+
|
114
|
+
### Debugging
|
47
115
|
|
48
116
|
Run `ynab_convert` with `YNAB_CONVERT_DEBUG=true`, or use the rake task
|
49
117
|
`spec:debug`. Debug logging goes to STDERR.
|
50
118
|
|
119
|
+
Or add `byebug` or `pry` statements in the code (works with guard and with rspec).
|
120
|
+
|
51
121
|
### Adding a new financial institution
|
52
122
|
|
53
123
|
If there is no processor for your financial institution, you can contribute one
|
54
124
|
to the project.
|
55
125
|
|
56
|
-
|
57
|
-
|
58
|
-
|
126
|
+
Looking at the other, real-world processors in `lib/processors` is helpful.
|
127
|
+
|
128
|
+
Note that if the processor name's case cannot be camel cased from its lowercase
|
129
|
+
string, it will need to be added manually in `lib/ynab_convert.rb` in the
|
130
|
+
`processor_class_name` method. For instance, the USB Chequing processor is
|
131
|
+
called with `-i ubs_chequing` from the command line. That makes the gem try to
|
132
|
+
use `Processors::UbsChequing` as the processor class, but it's actually called
|
133
|
+
`Processors::UBSChequing`.
|
59
134
|
|
60
135
|
Be sure to add tests to your processor as well before you make a PR.
|
61
136
|
|
@@ -0,0 +1,24 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'net/http'
|
4
|
+
require 'json'
|
5
|
+
|
6
|
+
module APIClients
|
7
|
+
# Base APIClient
|
8
|
+
class APIClient
|
9
|
+
# @param api_base_path [String] Base path to the API
|
10
|
+
def initialize(api_base_path:)
|
11
|
+
@api_base_path = api_base_path
|
12
|
+
end
|
13
|
+
|
14
|
+
private
|
15
|
+
|
16
|
+
def make_request(endpoint:)
|
17
|
+
uri = URI(URI.join(@api_base_path, endpoint))
|
18
|
+
|
19
|
+
response = Net::HTTP.get_response(uri)
|
20
|
+
|
21
|
+
JSON.parse(response.body, symbolize_names: true)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,66 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'ynab_convert/api_clients/api_client'
|
4
|
+
|
5
|
+
module APIClients
|
6
|
+
# Client for currency-api
|
7
|
+
# (https://github.com/fawazahmed0/currency-api#readme)
|
8
|
+
class CurrencyAPI < APIClient
|
9
|
+
# The days that are missing from the API's otherwise normally available
|
10
|
+
# range
|
11
|
+
MISSING_DAYS = { '2021-09-14' => true }.freeze
|
12
|
+
|
13
|
+
def initialize
|
14
|
+
api_base_path = 'https://cdn.jsdelivr.net/gh/fawazahmed0/currency-api@1/'
|
15
|
+
@available_date_range = {
|
16
|
+
min: Date.parse('2020-11-22'),
|
17
|
+
max: Date.today - 1 # yesterday
|
18
|
+
}
|
19
|
+
|
20
|
+
super(api_base_path: api_base_path)
|
21
|
+
end
|
22
|
+
|
23
|
+
# @param base_currency [Symbol] ISO symbol for base currency
|
24
|
+
# @param date [Date, String] The date on which to get the rates for
|
25
|
+
# @return [Hash<Symbol, Numeric>] The rates for that day in base_currency
|
26
|
+
def historical(base_currency:, date:)
|
27
|
+
parsed_date = date.is_a?(Date) ? date : Date.parse(date)
|
28
|
+
handle_date_out_of_bounds(parsed_date) if out_of_bounds?(parsed_date)
|
29
|
+
# Some days are missing from the API, use the previous day's rate if
|
30
|
+
# a missing day is requested
|
31
|
+
parsed_date -= 1 if missing_day?(date)
|
32
|
+
currency = base_currency.downcase
|
33
|
+
endpoint = "#{parsed_date}/currencies/#{currency}.min.json"
|
34
|
+
rates = make_request(endpoint: endpoint)
|
35
|
+
|
36
|
+
rates[currency]
|
37
|
+
end
|
38
|
+
|
39
|
+
private
|
40
|
+
|
41
|
+
# The currency-api only has rates since 2020-11-22 and until yesterday
|
42
|
+
# (the current day's rate are updated at 23:59 on that day). This method
|
43
|
+
# ensures the requested date falls within the available range.
|
44
|
+
# @param date [Date] The date to check
|
45
|
+
# @return [Boolean] Whether the date is out of bounds for this API
|
46
|
+
def out_of_bounds?(date)
|
47
|
+
date < @available_date_range[:min] || date > @available_date_range[:max]
|
48
|
+
end
|
49
|
+
|
50
|
+
# @param date [Date] The date to show in the error message
|
51
|
+
def handle_date_out_of_bounds(date)
|
52
|
+
error_message = "#{date} is out of the currency-api available date "\
|
53
|
+
"range (#{@available_date_range[:min]}–#{@available_date_range[:max]})"
|
54
|
+
|
55
|
+
raise Errno::EDOM, error_message
|
56
|
+
end
|
57
|
+
|
58
|
+
# Indicates whether a date is missing from the API's normally available
|
59
|
+
# date range
|
60
|
+
# @param date [Date] the date to check
|
61
|
+
# @return [Boolean] whether the date is unavailable in the API
|
62
|
+
def missing_day?(date)
|
63
|
+
MISSING_DAYS.key?(date.to_s)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'ynab_convert/documents/statements/statement'
|
4
|
+
|
5
|
+
module Documents
|
6
|
+
module Statements
|
7
|
+
# Example of a Statement
|
8
|
+
class Example < Statement
|
9
|
+
def initialize(filepath:)
|
10
|
+
csv_import_options = { col_sep: ';', quote_char: nil, headers: true }
|
11
|
+
|
12
|
+
super(filepath: filepath, csv_import_options: csv_import_options)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'ynab_convert/documents/statements/statement'
|
4
|
+
|
5
|
+
module Documents
|
6
|
+
module Statements
|
7
|
+
# Represents a statement from N26 Bank
|
8
|
+
class N26 < Statement
|
9
|
+
# @param filepath [String] Path to CSV statement
|
10
|
+
# @return [void]
|
11
|
+
def initialize(filepath:)
|
12
|
+
csv_import_options = {
|
13
|
+
col_sep: ',',
|
14
|
+
quote_char: '"',
|
15
|
+
headers: true,
|
16
|
+
encoding: 'bom|utf-8'
|
17
|
+
}
|
18
|
+
|
19
|
+
super(filepath: filepath,
|
20
|
+
csv_import_options: csv_import_options,)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Documents
|
4
|
+
module Statements
|
5
|
+
# The base Statement class from which other Statements inherit.
|
6
|
+
# Represents a CSV statement from a financial institution, typically from
|
7
|
+
# its online banking portal.
|
8
|
+
class Statement
|
9
|
+
attr_reader :csv_import_options, :filepath
|
10
|
+
|
11
|
+
# @param filepath [String] path to the CSV file
|
12
|
+
# @param csv_import_options [CSV::DEFAULT_OPTIONS] options describing
|
13
|
+
# the particular CSV flavour (column separator, etc). Any
|
14
|
+
# CSV::DEFAULT_OPTIONS is valid.
|
15
|
+
def initialize(filepath:, csv_import_options: CSV::DEFAULT_OPTIONS)
|
16
|
+
validate(filepath)
|
17
|
+
|
18
|
+
default_options = CSV::DEFAULT_OPTIONS.merge(converters: %i[numeric
|
19
|
+
date])
|
20
|
+
@filepath = filepath
|
21
|
+
@csv_import_options = default_options.merge(csv_import_options)
|
22
|
+
end
|
23
|
+
|
24
|
+
def institution_name
|
25
|
+
self.class.name.split('::').last
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
# Verifies that the file exists at path, raises an error if not.
|
31
|
+
# @param path [String] path to the file
|
32
|
+
def validate(path)
|
33
|
+
return if ::File.exist?(path)
|
34
|
+
|
35
|
+
raise Errno::ENOENT, "file not found #{path}"
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Documents
|
4
|
+
module Statements
|
5
|
+
# UBS Switzerland Chequing accounts statement
|
6
|
+
class UBSChequing < Statement
|
7
|
+
# @param filepath [String] path to CSV statement
|
8
|
+
def initialize(filepath:)
|
9
|
+
csv_import_options = {
|
10
|
+
col_sep: ';',
|
11
|
+
quote_char: nil,
|
12
|
+
encoding: Encoding::UTF_8,
|
13
|
+
headers: true
|
14
|
+
}
|
15
|
+
|
16
|
+
super(filepath: filepath, csv_import_options: csv_import_options)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Documents
|
4
|
+
module Statements
|
5
|
+
# UBS Switzerland Credit Card accounts statement
|
6
|
+
class UBSCredit < Statement
|
7
|
+
def initialize(filepath:)
|
8
|
+
csv_import_options = {
|
9
|
+
col_sep: ';',
|
10
|
+
quote_char: nil,
|
11
|
+
headers: true,
|
12
|
+
encoding: "#{Encoding::ISO_8859_1}:#{Encoding::UTF_8}",
|
13
|
+
skip_lines: 'sep=;'
|
14
|
+
}
|
15
|
+
super(filepath: filepath, csv_import_options: csv_import_options)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Documents
|
4
|
+
module Statements
|
5
|
+
# Wise card accounts statement
|
6
|
+
class Wise < Statement
|
7
|
+
def initialize(filepath:)
|
8
|
+
csv_import_options = {
|
9
|
+
col_sep: ',',
|
10
|
+
quote_char: '"',
|
11
|
+
headers: true
|
12
|
+
}
|
13
|
+
super(filepath: filepath, csv_import_options: csv_import_options)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Documents
|
4
|
+
module YNAB4Files
|
5
|
+
# Represents the YNAB4 formatted CSV data for importing into YNAB4
|
6
|
+
class YNAB4File
|
7
|
+
attr_reader :csv_export_options
|
8
|
+
|
9
|
+
def initialize(institution_name:, format: :flows)
|
10
|
+
@format = format
|
11
|
+
@institution_name = institution_name
|
12
|
+
@csv_export_options = {
|
13
|
+
converters: %i[numeric date],
|
14
|
+
force_quotes: true,
|
15
|
+
write_headers: true,
|
16
|
+
headers: headers
|
17
|
+
}
|
18
|
+
end
|
19
|
+
|
20
|
+
def update_dates(row)
|
21
|
+
date_index = 0
|
22
|
+
transaction_date = row[date_index]
|
23
|
+
unless transaction_date.is_a?(Date)
|
24
|
+
transaction_date = Date.parse(transaction_date)
|
25
|
+
end
|
26
|
+
|
27
|
+
update_start_date(transaction_date)
|
28
|
+
update_end_date(transaction_date)
|
29
|
+
end
|
30
|
+
|
31
|
+
def filename
|
32
|
+
from_date = @start_date.strftime('%Y%m%d')
|
33
|
+
to_date = @end_date.strftime('%Y%m%d')
|
34
|
+
|
35
|
+
"#{@institution_name.snake_case}_#{from_date}-#{to_date}_ynab4.csv"
|
36
|
+
end
|
37
|
+
|
38
|
+
private
|
39
|
+
|
40
|
+
def update_start_date(date)
|
41
|
+
@start_date = date if @start_date.nil? || date < @start_date
|
42
|
+
end
|
43
|
+
|
44
|
+
def update_end_date(date)
|
45
|
+
@end_date = date if @end_date.nil? || date > @end_date
|
46
|
+
end
|
47
|
+
|
48
|
+
def headers
|
49
|
+
base_headers = %w[Date Payee Memo]
|
50
|
+
extra_headers = %w[Outflow Inflow]
|
51
|
+
|
52
|
+
extra_headers = %w[Amount] if @format == :amounts
|
53
|
+
|
54
|
+
base_headers.concat(extra_headers)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Groups Statements and YNAB4File
|
4
|
+
module Documents
|
5
|
+
documents = %w[statement ynab4_file]
|
6
|
+
|
7
|
+
# Load all known Documents
|
8
|
+
documents.each do |d|
|
9
|
+
# Require the base classes first so that its children can find the parent
|
10
|
+
# class since files are otherwise loaded in alphabetical order
|
11
|
+
require File.join(__dir__, 'documents', "#{d}s", "#{d}.rb")
|
12
|
+
|
13
|
+
Dir[File.join(__dir__, 'documents', "#{d}s", '*.rb')].sort.each do |file|
|
14
|
+
require file
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
data/lib/ynab_convert/logger.rb
CHANGED
@@ -0,0 +1,24 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'ynab_convert/documents'
|
4
|
+
require 'ynab_convert/transformers'
|
5
|
+
require 'ynab_convert/processors/processor'
|
6
|
+
|
7
|
+
module Processors
|
8
|
+
# Example Processor
|
9
|
+
class Example < Processor
|
10
|
+
# @param filepath [String] path to the CSV file
|
11
|
+
def initialize(filepath:)
|
12
|
+
transformers = [
|
13
|
+
Transformers::Formatters::Example.new
|
14
|
+
]
|
15
|
+
statement = Documents::Statements::Example.new(filepath: filepath)
|
16
|
+
ynab4_file = Documents::YNAB4Files::YNAB4File.new(
|
17
|
+
format: :flows, institution_name: statement.institution_name
|
18
|
+
)
|
19
|
+
|
20
|
+
super(statement: statement, ynab4_file: ynab4_file, transformers:
|
21
|
+
transformers)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|