ynab_convert 1.0.6 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (51) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +5 -0
  3. data/.rubocop.yml +10 -2
  4. data/Gemfile.lock +37 -12
  5. data/Guardfile +1 -29
  6. data/README.md +76 -5
  7. data/lib/ynab_convert/api_clients/api_client.rb +24 -0
  8. data/lib/ynab_convert/api_clients/currency_api.rb +66 -0
  9. data/lib/ynab_convert/documents/statements/example_statement.rb +16 -0
  10. data/lib/ynab_convert/documents/statements/n26_statement.rb +24 -0
  11. data/lib/ynab_convert/documents/statements/statement.rb +39 -0
  12. data/lib/ynab_convert/documents/statements/ubs_chequing_statement.rb +20 -0
  13. data/lib/ynab_convert/documents/statements/ubs_credit_statement.rb +19 -0
  14. data/lib/ynab_convert/documents/statements/wise_statement.rb +17 -0
  15. data/lib/ynab_convert/documents/ynab4_files/ynab4_file.rb +58 -0
  16. data/lib/ynab_convert/documents.rb +17 -0
  17. data/lib/ynab_convert/logger.rb +1 -1
  18. data/lib/ynab_convert/processors/example_processor.rb +24 -0
  19. data/lib/ynab_convert/processors/n26_processor.rb +26 -0
  20. data/lib/ynab_convert/processors/processor.rb +75 -0
  21. data/lib/ynab_convert/processors/ubs_chequing_processor.rb +21 -0
  22. data/lib/ynab_convert/processors/ubs_credit_processor.rb +17 -0
  23. data/lib/ynab_convert/processors/wise_processor.rb +19 -0
  24. data/lib/ynab_convert/processors.rb +2 -2
  25. data/lib/ynab_convert/transformers/cleaners/cleaner.rb +17 -0
  26. data/lib/ynab_convert/transformers/cleaners/n26_cleaner.rb +13 -0
  27. data/lib/ynab_convert/transformers/cleaners/ubs_chequing_cleaner.rb +98 -0
  28. data/lib/ynab_convert/transformers/cleaners/ubs_credit_cleaner.rb +45 -0
  29. data/lib/ynab_convert/transformers/cleaners/wise_cleaner.rb +39 -0
  30. data/lib/ynab_convert/transformers/enhancers/enhancer.rb +20 -0
  31. data/lib/ynab_convert/transformers/enhancers/n26_enhancer.rb +74 -0
  32. data/lib/ynab_convert/transformers/enhancers/wise_enhancer.rb +87 -0
  33. data/lib/ynab_convert/transformers/formatters/example_formatter.rb +12 -0
  34. data/lib/ynab_convert/transformers/formatters/formatter.rb +91 -0
  35. data/lib/ynab_convert/transformers/formatters/n26_formatter.rb +19 -0
  36. data/lib/ynab_convert/transformers/formatters/ubs_chequing_formatter.rb +12 -0
  37. data/lib/ynab_convert/transformers/formatters/ubs_credit_formatter.rb +12 -0
  38. data/lib/ynab_convert/transformers/formatters/wise_formatter.rb +35 -0
  39. data/lib/ynab_convert/transformers.rb +18 -0
  40. data/lib/ynab_convert/validators/ynab4_row_validator.rb +83 -0
  41. data/lib/ynab_convert/validators.rb +9 -0
  42. data/lib/ynab_convert/version.rb +1 -1
  43. data/lib/ynab_convert.rb +4 -3
  44. data/ynab_convert.gemspec +4 -0
  45. metadata +91 -8
  46. data/lib/ynab_convert/processor/base.rb +0 -226
  47. data/lib/ynab_convert/processor/example.rb +0 -124
  48. data/lib/ynab_convert/processor/n26.rb +0 -70
  49. data/lib/ynab_convert/processor/revolut.rb +0 -103
  50. data/lib/ynab_convert/processor/ubs_chequing.rb +0 -115
  51. 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: 697f0d80eaec169e83bea48407e9d00c1b000a362aaa6147160be65bafef72a7
4
- data.tar.gz: 8e45ffa5ec9094c54057ebba3f241c00764fc0e256ec19bdfa0d9395f590a156
3
+ metadata.gz: 142532dddc82b58043e383e388306801471835c48004fd55b7804709a545999c
4
+ data.tar.gz: e7de2e56779760e27e41a3cf3bcbaf265bd66377a80525ad00f7133b57c1b05a
5
5
  SHA512:
6
- metadata.gz: 8150a18508c46b6267b38ccf56ff6b542e82df49100c4fff164959a24445a36e8271cfca6bd07abbc583f76d372bfc77f9ab830a88dd9e37eb5d95af15ab6ef5
7
- data.tar.gz: 7490a9741e4bf27fd21e526fe4aff6c777cca8882f64a9558ceb698f16b7c080f92ae24ebf136a21c3768f5e76077c4ccbe6536a81cdc0cd0dec1453e279a40d
6
+ metadata.gz: a4e37509faa4be4db330876e0c6af4bb2deb0927b4bb4aa8feb9b1943a69690d8765087cfe72ebeb49161b708999253e7443f5b3275f5cfc42bc3f23e93232cf
7
+ data.tar.gz: 4a0eb0cee6a35847ba39d747f6343c39c5cb0750ca8347f9af0ad70fb3d8382ac5e098bbb6b47c8d931b1d095d4829442fd0315b697d6005003ccc8aa7685638
data/.gitignore CHANGED
@@ -9,3 +9,8 @@
9
9
 
10
10
  # rspec failure tracking
11
11
  .rspec_status
12
+
13
+ # test artifacts
14
+ *_ynab4.csv
15
+ .byebug_history
16
+ ynab_convert.yml
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: rubocop-rake
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
- Metrics/LineLength:
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 (1.0.6)
4
+ ynab_convert (2.0.0)
5
5
  i18n
6
6
  slop
7
+ timecop
7
8
 
8
9
  GEM
9
10
  remote: https://rubygems.org/
10
11
  specs:
11
- ast (2.4.0)
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.19.0)
56
- parser (2.6.5.0)
57
- ast (~> 2.4.0)
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.0.0)
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.76.0)
86
- jaro_winkler (~> 1.5.1)
94
+ rubocop (0.93.1)
87
95
  parallel (~> 1.10)
88
- parser (>= 2.6)
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, < 1.7)
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
- ruby-progressbar (1.10.1)
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
- unicode-display_width (1.6.0)
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,106 @@ 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
30
+ `n26` | N26 | [n26.com](n26.com) | N26 CSV statements, will convert EUR amounts to CHF (hardcoded for now)
31
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
32
32
  `ubs_chequing` | UBS Switzerland (private banking) | [ubs.ch](https://ubs.ch) | Private chequing and joint accounts
33
33
  `ubs_credit` | UBS Switzerland (credit cards) | [ubs.ch](https://ubs.ch) | Both MasterCard and Visa
34
+ `wise` | Wise (Transferwise) cards | [wise.com](https://wise.com) | Performs currency conversion (hardcoded to CHF for now)
34
35
 
35
36
  ## Contributing
36
37
 
37
38
  After checking out the repo, run `bin/setup` to install dependencies. Then, run
38
39
  `rake spec` to run the tests. You can also run `bin/console` for an interactive
39
- prompt that will allow you to experiment.
40
+ prompt that will allow you to experiment. To run Rubocop and RSpec in watch
41
+ mode, use `bundle exec guard`.
40
42
 
41
43
  To install this gem onto your local machine, run `bundle exec rake install`.
44
+ Alternatively, the gem can also be run from `bin/ynab_convert`.
42
45
 
43
46
  Bug reports and pull requests are welcome on GitHub at
44
47
  https://github.com/coaxial/ynab_convert.
45
48
 
46
- ### Enable debug output
49
+ ### Architecture
50
+
51
+ Here is the class diagram:
52
+ ```mermaid
53
+ classDiagram
54
+ Documents <|-- Statement
55
+ Documents <|-- YNAB4File
56
+ Transformers <|-- Cleaner
57
+ Transformers <|-- Formatter
58
+ Transformers <|-- Enhancer
59
+ Validators <|-- YNAB4Row
60
+ Processors <|-- Processor
61
+
62
+ class Statement{
63
+ #Hash csv_import_options
64
+ #String filepath
65
+ #String institution_name
66
+ }
67
+
68
+ class YNAB4File{
69
+ #Hash csv_export_options
70
+ #String filename
71
+ #update_dates(row)
72
+ }
73
+
74
+ class Processor{
75
+ #to_ynab!()
76
+ }
77
+
78
+ class Cleaner{
79
+ #run(row)
80
+ }
81
+
82
+ class Enhancer{
83
+ #run(row)
84
+ }
85
+
86
+ class Formatter{
87
+ #run(row)
88
+ }
89
+
90
+ class YNAB4Row{
91
+ +valid?(row)
92
+ }
93
+ ```
94
+
95
+ Each financial institution gets its own class for most of these base classes.
96
+
97
+ For instance, adding "Some Bank" would require creating the following new
98
+ classes:
99
+
100
+ - `class Processors::SomeBank < Processor`
101
+ - `class Transformers::Cleaners::SomeBank < Cleaner`
102
+ - `class Transformers::Formatter::SomeBank < Formatter`
103
+ - `class Transformers::Enhancer::SomeBank < Enhancer`
104
+ - `class Documents::Statements::SomeBank < Statement`
105
+
106
+ Each of these classes would implement the expected interface for its type, and
107
+ the `Processor::SomeBank` would instantiate them all. `Validators` and `YNAB4File`
108
+ aren't related to a particular institution, there is no need to derive a child
109
+ class for each bank.
110
+
111
+ Note that any of the `Transformers::` classes are optional, and it is possible
112
+ that some institution only requires a `Cleaner` but no `Formatter` or
113
+ `Enhancer` (for example).
114
+
115
+ ### Debugging
47
116
 
48
117
  Run `ynab_convert` with `YNAB_CONVERT_DEBUG=true`, or use the rake task
49
118
  `spec:debug`. Debug logging goes to STDERR.
50
119
 
120
+ Or add `byebug` or `pry` statements in the code (works with guard and with rspec).
121
+
51
122
  ### Adding a new financial institution
52
123
 
53
124
  If there is no processor for your financial institution, you can contribute one
54
125
  to the project.
55
126
 
56
127
  There is a commented example processor located at
57
- `lib/ynab_convert/processor/example.rb`. Looking at the other, real-world
58
- processors in that directory can also help.
128
+ `lib/ynab_convert/processors/example_processor.rb`. Looking at the other,
129
+ real-world processors in that directory can also help.
59
130
 
60
131
  Be sure to add tests to your processor as well before you make a PR.
61
132
 
@@ -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
@@ -6,7 +6,7 @@ require 'logger'
6
6
  module YnabLogger
7
7
  def logger
8
8
  @logger unless @logger.nil?
9
- @logger ||= Logger.new(STDERR)
9
+ @logger ||= Logger.new($stderr)
10
10
  @logger.level = Logger::FATAL
11
11
  @logger.level = Logger::DEBUG if ENV['YNAB_CONVERT_DEBUG'] == 'true'
12
12
  @logger
@@ -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