ynab_convert 1.0.7 → 2.0.1

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 +82 -7
  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 +22 -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: 3d04e0b626a2fc75009e2eb5aee5d7693b0c7882bc82a493380ea697df2f0dc1
4
- data.tar.gz: e79c99994f87108164b22a75a4a5fb806380bd56af092405a653d40487bdbd71
3
+ metadata.gz: b10705667e423e280b312986f423664a84450c6d7aa9f47e8cd99063b8c653c7
4
+ data.tar.gz: 87563fa9a29b8a01145e6406d713114ad275a2f33ab9b27c64bc771a4379f34e
5
5
  SHA512:
6
- metadata.gz: 0f99dbb991b1cb948579e5531e2deb2a6353fca1b16709822122c24a2afe40854e12136a8eec8c0167e807c3ce32e4d4cf659c8380fa1700654d535e3ac8e44e
7
- data.tar.gz: 250e64b972b77f55e4805ec8ba53b45bcc348bd3e2e7447dc781d5487979205a4f9d0f157730920e69d08e86f3c06618e6a1921aeb387fb994d6b08bbc642793
6
+ metadata.gz: 2cd94babe8d7aef861dcfc84d85ba0401b5807af87a3df4c17724663f69ec0d756288929f80089ce7e083068e0d7cc2de4fa45df6767651dbb4929797d59006a
7
+ data.tar.gz: 11562fdf39e805dce87016f28b5f442a5de8716dbda6a5e06d7f0b7208f1ee84f08cdae17908de72c418b221d71a561a34e584b0c2ab7d29c604b19c1b57cc9e
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.7)
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
- 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,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
- ### Enable debug output
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
- 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.
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
@@ -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