ynab_convert 1.0.2 → 1.0.5

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2e707b36e18c8b57f7b517bf6f1f29d769e22089316448da062165c35e5fb713
4
- data.tar.gz: ee5dbf483c6252085860166c6371fd7d6ba75b91d90dc17a74a9a68ddff80026
3
+ metadata.gz: 58ad75b7f1c6b3093257d8ffb95068967324a621be48bc6e08063548d4a9f2bd
4
+ data.tar.gz: 70bc35076e068aece838b52853f5b7f7656199b67e5b2f3ca78a4aec66037ffb
5
5
  SHA512:
6
- metadata.gz: 44f457015648d06a43c98da0622ae12de10eb84025636677b044020d98aa82dca85c2968f9b924541dd8260002a8e727bef22cf60515d6dc17e345d8e06cf0e5
7
- data.tar.gz: 0fadae67422faf27caa356890121600879416545043c240ef951a3bc2c9caf9a8af544f25978a50e3e479994eb020e008e99c32a99c76363d239e82d09037fcb
6
+ metadata.gz: 56621762f02d97515a0f3ce829d5b7bfe9ffb1fe862a6b0e92197ac2b555abe84102900f7eef28f67c7c0570efdf0bc3ea8005c8adb68f9c598a7f0fba94f6e9
7
+ data.tar.gz: 86a99a7d007c05b7aa3f6e6f3ed211ac009989bd8e31fefbef0db3d9a503dc765ecea825b9e1f6d237d2e26c7c59a455010f975183085037cf8bd2dcb95e8a04
data/.rubocop.yml CHANGED
@@ -1,6 +1,11 @@
1
1
  ---
2
+ # See https://github.com/rubocop/rubocop/blob/master/config/default.yml for all
3
+ # options
2
4
  require: rubocop-rake
3
5
 
6
+ AllCops:
7
+ DisplayCopNames: true
8
+
4
9
  Metrics/LineLength:
5
10
  Exclude:
6
11
  - ynab_convert.gemspec
data/.travis.yml CHANGED
@@ -3,12 +3,12 @@ language: ruby
3
3
  cache: bundler
4
4
  rvm:
5
5
  - 2.6
6
+ - 2.7
6
7
  before_install: gem install bundler
7
8
 
8
9
  jobs:
9
10
  include:
10
11
  - stage: test
11
- rvm: 2.6
12
12
  script: echo "Running tests for $(ruby -v)..." && bundle exec rake ci
13
13
  - stage: gem release
14
14
  rvm: 2.6
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- ynab_convert (1.0.2)
4
+ ynab_convert (1.0.5)
5
5
  i18n
6
6
  slop
7
7
 
@@ -44,10 +44,10 @@ GEM
44
44
  rb-inotify (~> 0.9, >= 0.9.10)
45
45
  lumberjack (1.0.13)
46
46
  method_source (0.9.2)
47
- mini_portile2 (2.6.1)
47
+ mini_portile2 (2.8.0)
48
48
  nenv (0.3.0)
49
- nokogiri (1.12.5)
50
- mini_portile2 (~> 2.6.1)
49
+ nokogiri (1.13.3)
50
+ mini_portile2 (~> 2.8.0)
51
51
  racc (~> 1.4)
52
52
  notiffany (0.1.3)
53
53
  nenv (~> 0.1)
@@ -61,7 +61,7 @@ GEM
61
61
  pry-byebug (3.7.0)
62
62
  byebug (~> 11.0)
63
63
  pry (~> 0.10)
64
- racc (1.5.2)
64
+ racc (1.6.0)
65
65
  rainbow (3.0.0)
66
66
  rake (13.0.1)
67
67
  rb-fsevent (0.10.3)
data/Guardfile CHANGED
@@ -74,7 +74,7 @@ group :red_green_refactor, halt_on_fail: true do
74
74
  end
75
75
  end
76
76
 
77
- guard :rubocop, cli: ['--auto-correct'] do
77
+ guard :rubocop, cli: ['--auto-correct', '--display-cop-names'] do
78
78
  watch('Gemfile')
79
79
  watch('Rakefile')
80
80
  watch('bin/convert')
data/README.md CHANGED
@@ -27,6 +27,7 @@ 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
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
31
32
  `ubs_chequing` | UBS Switzerland (private banking) | [ubs.ch](https://ubs.ch) | Private chequing and joint accounts
32
33
  `ubs_credit` | UBS Switzerland (credit cards) | [ubs.ch](https://ubs.ch) | Both MasterCard and Visa
@@ -44,7 +45,8 @@ https://github.com/coaxial/ynab_convert.
44
45
 
45
46
  ### Enable debug output
46
47
 
47
- Run `ynab_convert` with `YNAB_CONVERT_DEBUG=true`, or use the rake task `spec:debug`. Debug logging goes to STDERR.
48
+ Run `ynab_convert` with `YNAB_CONVERT_DEBUG=true`, or use the rake task
49
+ `spec:debug`. Debug logging goes to STDERR.
48
50
 
49
51
  ### Adding a new financial institution
50
52
 
data/bin/ynab_convert CHANGED
@@ -9,7 +9,6 @@ require 'ynab_convert/processors'
9
9
  require 'ynab_convert/logger'
10
10
  require 'ynab_convert/error'
11
11
  require 'slop/symbol'
12
- require 'pry'
13
12
 
14
13
  # Add mixins and run converter
15
14
  class Convert
@@ -6,7 +6,7 @@ module CoreExtensions
6
6
  # Adds convenience methods
7
7
  module Inflections
8
8
  def snake_case
9
- downcase.tr(' ', '_').gsub(/[^a-z_]/, '')
9
+ downcase.tr(' ', '_').gsub(/[^a-z_0-9]/, '')
10
10
  end
11
11
 
12
12
  def camel_case
@@ -5,7 +5,8 @@ require 'csv'
5
5
  require 'ynab_convert/logger'
6
6
 
7
7
  module Processor
8
- # Base class for a Processor, all processors must inherit from it
8
+ # Base class for a Processor, all processors must inherit from it.
9
+
9
10
  # rubocop:disable Metrics/ClassLength
10
11
  class Base
11
12
  include YnabLogger
@@ -13,13 +14,30 @@ module Processor
13
14
 
14
15
  attr_reader :loader_options
15
16
 
16
- # @option opts [String] :file Path to the CSV file to process
17
- def initialize(opts)
17
+ # @option options [String] :file Path to the CSV file to process
18
+ # @option options [Symbol] :format YNAB4 format to use, one of :flows or
19
+ # :amounts. :flows is useful for CSVs with separate debit and credit
20
+ # columns, :amounts is for CSVs with only one amount columns and +/-
21
+ # numbers. See
22
+ # https://docs.youneedabudget.com/article/921-formatting-csv-file
23
+ def initialize(options)
24
+ default_options = { file: '', format: :flows }
25
+ opts = default_options.merge(options)
26
+
18
27
  logger.debug "Initializing processor with options: `#{opts.to_h}'"
19
28
  raise ::Errno::ENOENT unless File.exist? opts[:file]
20
29
 
21
30
  @file = opts[:file]
22
- @headers = { transaction_date: nil, payee: nil, debit: nil, credit: nil }
31
+ @headers = { transaction_date: nil, payee: nil }
32
+ @format = opts[:format]
33
+
34
+ if @format == :amounts
35
+ amounts_columns = { amount: nil }
36
+ @headers.merge!(amounts_columns)
37
+ else
38
+ flows_columns = { inflow: nil, outflow: nil }
39
+ @headers.merge!(flows_columns)
40
+ end
23
41
  end
24
42
 
25
43
  def to_ynab!
@@ -38,15 +56,41 @@ module Processor
38
56
 
39
57
  attr_accessor :statement_from, :statement_to, :headers
40
58
 
41
- def inflow_or_outflow_missing?(row)
59
+ def amount_invalid?(row)
60
+ amount_index = 3
61
+
62
+ # If there is no amount,
63
+ # then the row is invalid.
64
+ row[amount_index].nil? || row[amount_index].empty?
65
+ end
66
+
67
+ def inflow_outflow_invalid?(row)
42
68
  inflow_index = 3
43
69
  outflow_index = 4
44
- # If there is neither inflow and outflow values, or their value is 0,
45
- # then the row is not valid to YNAB4
46
- (row[inflow_index].nil? || row[inflow_index].empty? ||
47
- row[inflow_index] == '0.00') &&
48
- (row[outflow_index].nil? || row[outflow_index].empty? ||
49
- row[outflow_index] == '0.00')
70
+
71
+ # If there is neither inflow and outflow values,
72
+ # or both the inflow and outflow amounts are 0,
73
+ # then the row is invalid.
74
+ (
75
+ row[inflow_index].nil? ||
76
+ row[inflow_index].empty? ||
77
+ row[inflow_index] == '0.00'
78
+ ) && (
79
+ row[outflow_index].nil? ||
80
+ row[outflow_index].empty? ||
81
+ row[outflow_index] == '0.00'
82
+ )
83
+ end
84
+
85
+ def amounts_missing?(row)
86
+ logger.debug "Checking for missing amount in `#{row}`"
87
+ if @format == :amounts
88
+ logger.debug 'Using `:amounts`'
89
+ amount_invalid?(row)
90
+ else
91
+ logger.debug 'Using `:flows`'
92
+ inflow_outflow_invalid?(row)
93
+ end
50
94
  end
51
95
 
52
96
  def skip_row(row)
@@ -96,14 +140,15 @@ module Processor
96
140
  def convert!
97
141
  logger.debug "Will write to `#{temp_filename}'"
98
142
 
99
- CSV.open(temp_filename, 'wb', output_options) do |converted|
100
- CSV.foreach(@file, 'rb', loader_options) do |row|
143
+ logger.debug(loader_options)
144
+ CSV.open(temp_filename, 'wb', **output_options) do |converted|
145
+ CSV.foreach(@file, 'rb', **loader_options) do |row|
101
146
  logger.debug "Parsing row: `#{row.to_h}'"
102
147
  # Some rows don't contain valid or useful data
103
148
  catch :skip_row do
104
149
  extract_header_names(row)
105
150
  ynab_row = transformers(row)
106
- if inflow_or_outflow_missing?(ynab_row) ||
151
+ if amounts_missing?(ynab_row) ||
107
152
  transaction_date_missing?(ynab_row)
108
153
  logger.debug 'Empty row, skipping it'
109
154
  skip_row(row)
@@ -151,7 +196,17 @@ module Processor
151
196
  end
152
197
 
153
198
  def ynab_headers
154
- %w[Date Payee Memo Outflow Inflow]
199
+ common_headers = %w[Date Payee Memo]
200
+
201
+ if @format == :amounts
202
+ amounts_headers = %w[Amount]
203
+ common_headers.concat(amounts_headers)
204
+ else
205
+ flows_headers = %w[Outflow Inflow]
206
+ common_headers.concat(flows_headers)
207
+ end
208
+
209
+ common_headers
155
210
  end
156
211
 
157
212
  def output_options
@@ -14,7 +14,7 @@ module Processor
14
14
  # if it's redundant. For instance, this parser is for "Example Bank" but it's
15
15
  # named "example.rb", its corresponding spec is
16
16
  # "spec/example_processor_spec.rb" and its fixture would be
17
- # "spec/fixtures/example.csv"
17
+ # "spec/fixtures/example/statement.csv"
18
18
  class Example < Processor::Base
19
19
  # @option options [String] :file Path to the CSV file to process
20
20
  def initialize(options)
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Processor
4
+ # Processes CSV files from N26
5
+ class N26 < Processor::Base
6
+ # @option options [String] :file Path to the CSV file to process
7
+ def initialize(options)
8
+ # Custom converters can be added so that the CSV data is parsed when
9
+ # loading the original file
10
+ register_custom_converters
11
+
12
+ # These are the options for the CSV module (see
13
+ # https://ruby-doc.org/stdlib-2.6/libdoc/csv/rdoc/CSV.html#method-c-new)
14
+ # They should match the format for the CSV file that the financial
15
+ # institution generates.
16
+ @loader_options = {
17
+ col_sep: ',',
18
+ quote_char: '"',
19
+ # Use your converters, if any
20
+ # converters: %i[],
21
+ headers: true,
22
+ encoding: 'bom|utf-8'
23
+ }
24
+
25
+ # This is the financial institution's full name as it calls itself. This
26
+ # usually matches the institution's letterhead and/or commercial name.
27
+ # It can happen that the same institution needs different parsers because
28
+ # its credit card CSV files are in one format, and its chequing accounts
29
+ # in another. In that case, more details can be added in parens.
30
+ # For instance:
31
+ # 'Example Bank (credit cards)' and 'Example Bank (chequing)'
32
+ @institution_name = 'N26 Bank'
33
+ # N26's CSV only has one columns for all transactions instead of separate
34
+ # debit and credit columns
35
+ additional_processor_options = { format: :amounts }
36
+
37
+ # This is mandatory.
38
+ super(options.merge(additional_processor_options))
39
+ end
40
+
41
+ private
42
+
43
+ def register_custom_converters; end
44
+
45
+ protected
46
+
47
+ def transformers(row)
48
+ transaction_date = row[headers[:transaction_date]]
49
+ payee = row[headers[:payee]]
50
+ amount = row[headers[:amount]]
51
+
52
+ converted_row = [transaction_date, payee, nil, amount]
53
+ logger.debug "Converted row: #{converted_row}"
54
+ converted_row
55
+ end
56
+
57
+ private
58
+
59
+ # Institutions love translating the column names, apparently. Rather than
60
+ # hardcoding the column name as a string, use the headers array at the
61
+ # right index.
62
+ # These lookups aren't particularly expensive but they're done on each row
63
+ # so why not memoize them with ||=
64
+ def extract_header_names(row)
65
+ headers[:transaction_date] ||= row.headers[0]
66
+ headers[:payee] ||= row.headers[1]
67
+ headers[:amount] ||= row.headers[5]
68
+ end
69
+ end
70
+ end
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Processor
4
- # Processes CSV files from UBS Personal Banking Switzerland (French)
4
+ # Processes CSV files from UBS Personal Banking Switzerland
5
5
  class UbsChequing < Processor::Base
6
6
  # @option options [String] :file Path to the CSV file to process
7
7
  def initialize(options)
@@ -59,12 +59,22 @@ module Processor
59
59
  end
60
60
 
61
61
  def transaction_payee(row)
62
- # Transaction description is spread over 3 columns
62
+ # Transaction description is spread over 3 columns.
63
+ # Moreover, UBS thought wise to append a bunch of junk information after
64
+ # the transaction details within the third description field. *Most* of
65
+ # this junk starts after the meaningful data and starts with ", OF",
66
+ # ", ON", ", ESR", two digits then five groups of five digits then ", TN"
67
+ # so we discard it; YNAB4 being unable to automatically categorize new
68
+ # transactions at the same store/payee because the payee always looks
69
+ # different (thanks to the variable nature of the appended junk).
70
+ # See `spec/fixtures/ubs_chequing/statement.csv` L18 and L2.
71
+ junk_desc_regex = /, (O[FN]|ESR|\d{2} \d{5} \d{5} \d{5} \d{5} \d{5}, TN)/
72
+
63
73
  [
64
74
  row[headers[:payee_line_1]],
65
75
  row[headers[:payee_line_2]],
66
76
  row[headers[:payee_line_3]]
67
- ].join(' ')
77
+ ].join(' ').split(junk_desc_regex).first
68
78
  end
69
79
 
70
80
  def register_custom_converters
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Processor
4
- # Processes CSV files from UBS Credit Cards Switzerland (French)
4
+ # Processes CSV files from UBS Credit Cards Switzerland
5
5
  class UbsCredit < Processor::Base
6
6
  # @option options [String] :file Path to the CSV file to process
7
7
  def initialize(options)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module YnabConvert
4
- VERSION = '1.0.2'
4
+ VERSION = '1.0.5'
5
5
  end
data/lib/ynab_convert.rb CHANGED
@@ -2,7 +2,6 @@
2
2
 
3
3
  require 'ynab_convert/version'
4
4
  require 'slop'
5
- require 'pry'
6
5
  require 'ynab_convert/logger'
7
6
  require 'core_extensions/string.rb'
8
7
 
data/ynab_convert.gemspec CHANGED
@@ -14,6 +14,16 @@ Gem::Specification.new do |spec|
14
14
  spec.homepage = 'https://github.com/coaxial/ynab_convert'
15
15
  spec.license = 'MIT'
16
16
 
17
+ spec.required_ruby_version = '~> 2.6'
18
+
19
+ spec.metadata = {
20
+ 'bug_tracker_uri' => 'https://github.com/coaxial/ynab_convert/issues',
21
+ 'documentation_uri' => 'https://rubydoc.info/github/coaxial/ynab_convert/master',
22
+ 'homepage_uri' => 'https://github.com/coaxial/ynab_convert',
23
+ 'source_code_uri' => 'https://github.com/coaxial/ynab_convert'
24
+ }
25
+ spec.post_install_message = 'Happy budgetting!'
26
+
17
27
  # Prevent pushing this gem to RubyGems.org. To allow pushes either set the 'allowed_push_host'
18
28
  # to allow pushing to a single host or delete this section to allow pushing to any host.
19
29
  if spec.respond_to?(:metadata)
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ynab_convert
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.2
4
+ version: 1.0.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - coaxial
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2022-02-24 00:00:00.000000000 Z
11
+ date: 2022-03-06 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -225,6 +225,7 @@ files:
225
225
  - lib/ynab_convert/logger.rb
226
226
  - lib/ynab_convert/processor/base.rb
227
227
  - lib/ynab_convert/processor/example.rb
228
+ - lib/ynab_convert/processor/n26.rb
228
229
  - lib/ynab_convert/processor/revolut.rb
229
230
  - lib/ynab_convert/processor/ubs_chequing.rb
230
231
  - lib/ynab_convert/processor/ubs_credit.rb
@@ -235,18 +236,20 @@ homepage: https://github.com/coaxial/ynab_convert
235
236
  licenses:
236
237
  - MIT
237
238
  metadata:
238
- allowed_push_host: https://rubygems.org
239
+ bug_tracker_uri: https://github.com/coaxial/ynab_convert/issues
240
+ documentation_uri: https://rubydoc.info/github/coaxial/ynab_convert/master
239
241
  homepage_uri: https://github.com/coaxial/ynab_convert
240
242
  source_code_uri: https://github.com/coaxial/ynab_convert
241
- post_install_message:
243
+ allowed_push_host: https://rubygems.org
244
+ post_install_message: Happy budgetting!
242
245
  rdoc_options: []
243
246
  require_paths:
244
247
  - lib
245
248
  required_ruby_version: !ruby/object:Gem::Requirement
246
249
  requirements:
247
- - - ">="
250
+ - - "~>"
248
251
  - !ruby/object:Gem::Version
249
- version: '0'
252
+ version: '2.6'
250
253
  required_rubygems_version: !ruby/object:Gem::Requirement
251
254
  requirements:
252
255
  - - ">="