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.
- 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
@@ -0,0 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'ynab_convert/transformers'
|
4
|
+
|
5
|
+
module Transformers
|
6
|
+
module Formatters
|
7
|
+
# Formats N26 statement values to YNAB4 value
|
8
|
+
class N26 < Formatter
|
9
|
+
def initialize
|
10
|
+
super({ date: [0], payee: [1], amount: [5] })
|
11
|
+
end
|
12
|
+
|
13
|
+
# All amounts are always in EUR
|
14
|
+
def memo(_row)
|
15
|
+
'EUR'
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Transformers
|
4
|
+
module Formatters
|
5
|
+
# UBS Switzerland Chequing accounts formatter
|
6
|
+
class UBSChequing < Formatter
|
7
|
+
def initialize
|
8
|
+
super({ date: [9], payee: [12], outflow: [18], inflow: [19] })
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Transformers
|
4
|
+
module Formatters
|
5
|
+
# UBS Switzerland Credit Card accounts formatter
|
6
|
+
class UBSCredit < Formatter
|
7
|
+
def initialize
|
8
|
+
super({ date: [3], payee: [4], outflow: [10], inflow: [11] })
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Transformers
|
4
|
+
module Formatters
|
5
|
+
# Wise card accounts formatter
|
6
|
+
class Wise < Formatter
|
7
|
+
def initialize
|
8
|
+
super({ date: [1], payee: [13], amount: [2] })
|
9
|
+
end
|
10
|
+
|
11
|
+
def payee(row)
|
12
|
+
merchant = row[13]
|
13
|
+
description = row[4]
|
14
|
+
|
15
|
+
return description if merchant.nil?
|
16
|
+
|
17
|
+
merchant
|
18
|
+
end
|
19
|
+
|
20
|
+
def memo(row)
|
21
|
+
# Description goes in Memo because we'll need to extract the original
|
22
|
+
# amount from it in the enhancer.
|
23
|
+
description = row[4]
|
24
|
+
amount_currency = row[3]
|
25
|
+
original_amount = description.scan(/\d+\.\d{2}\s\w{3}/).first
|
26
|
+
|
27
|
+
memo = amount_currency
|
28
|
+
# Topups don't have an original amount
|
29
|
+
memo = "#{memo},#{original_amount}" unless original_amount.nil?
|
30
|
+
|
31
|
+
memo
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Regroups all the classes involved in transforming a given Statement into a
|
4
|
+
# YNAB4File
|
5
|
+
module Transformers
|
6
|
+
transformers = %w[cleaner enhancer formatter]
|
7
|
+
|
8
|
+
# Load all known Transformers
|
9
|
+
transformers.each do |t|
|
10
|
+
# Require the base classes first so that its children can find the parent
|
11
|
+
# class since files are otherwise loaded in alphabetical order
|
12
|
+
require File.join(__dir__, 'transformers', "#{t}s", "#{t}.rb")
|
13
|
+
|
14
|
+
Dir[File.join(__dir__, 'transformers', "#{t}s", '*.rb')].sort.each do |file|
|
15
|
+
require file
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,83 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Validators
|
4
|
+
# Checks YNAB4 row for validity. A row is valid if it has a Date, Payee, and
|
5
|
+
# one of Amount, Outflow, Inflow.
|
6
|
+
module YNAB4Row
|
7
|
+
# Validates a row
|
8
|
+
# @param row [Array<String, Numeric, Date>] The row to validate
|
9
|
+
# @return [Boolean] Whether the row is valid
|
10
|
+
def self.valid?(row)
|
11
|
+
# we are dealing with a YNAB4 row:
|
12
|
+
# %w[Date Payee Memo Amount|Outflow Inflow]
|
13
|
+
amount_valid?(row) &&
|
14
|
+
transaction_date_valid?(row) &&
|
15
|
+
payee_valid?(row)
|
16
|
+
end
|
17
|
+
|
18
|
+
# Indicates which format the row is in (:flows or :amounts)
|
19
|
+
# @param row [CSV::Row] the row to check
|
20
|
+
# @return [:flows, :amounts] the row's format
|
21
|
+
def self.row_format(row)
|
22
|
+
format = :flows
|
23
|
+
# :flows has 5 columns: Date, Payee, Memo, Outflow, Inflow
|
24
|
+
# :amounts has 4 columns: Date, Payee, Memo, Amount
|
25
|
+
format = :amounts if row.length == 4
|
26
|
+
|
27
|
+
format
|
28
|
+
end
|
29
|
+
|
30
|
+
# Indicates whether the amount on the row is valid
|
31
|
+
# @param row [CSV::Row] the row to check
|
32
|
+
# @return [Boolean] whether the amount is invalid
|
33
|
+
def self.amount_valid?(row)
|
34
|
+
format = row_format(row)
|
35
|
+
indices = [3]
|
36
|
+
indices << 4 if format == :flows
|
37
|
+
|
38
|
+
if format == :amounts
|
39
|
+
return indices.reduce(true) do |valid, i|
|
40
|
+
valid && value_valid?(row[i])
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
indices.reduce(false) do |valid, i|
|
45
|
+
valid || value_valid?(row[i])
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
# Indicates whether a value is valid
|
50
|
+
# @note Prefer using the #valid? method
|
51
|
+
# @param value [#zero?, #nil?, #to_s] the value to check
|
52
|
+
# @return [Boolean] whether the value is valid
|
53
|
+
def self.value_valid?(value)
|
54
|
+
if value.respond_to? :zero?
|
55
|
+
!value.zero?
|
56
|
+
else
|
57
|
+
!value.nil? && !value.to_s.empty?
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
# Validates the Date value
|
62
|
+
# @note Prefer using the #valid? method
|
63
|
+
# @param row [Array<String, Numeric, Date] The row to validate
|
64
|
+
# @return [Boolean] Whether the row's Date is invalid
|
65
|
+
def self.transaction_date_valid?(row)
|
66
|
+
date_index = 0
|
67
|
+
date = row[date_index]
|
68
|
+
|
69
|
+
value_valid?(date)
|
70
|
+
end
|
71
|
+
|
72
|
+
# Validates the Payee value
|
73
|
+
# @note Prefer using the #valid? method
|
74
|
+
# @param row [Array<String, Numeric, Date] The row to validate
|
75
|
+
# @return [Boolean] Whether the row's Payee is valid
|
76
|
+
def self.payee_valid?(row)
|
77
|
+
payee_index = 1
|
78
|
+
payee = row[payee_index]
|
79
|
+
|
80
|
+
value_valid?(payee)
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
data/lib/ynab_convert/version.rb
CHANGED
data/lib/ynab_convert.rb
CHANGED
@@ -3,7 +3,11 @@
|
|
3
3
|
require 'ynab_convert/version'
|
4
4
|
require 'slop'
|
5
5
|
require 'ynab_convert/logger'
|
6
|
-
require 'core_extensions/string
|
6
|
+
require 'core_extensions/string'
|
7
|
+
require 'byebug' if ENV['YNAB_CONVERT_DEBUG']
|
8
|
+
|
9
|
+
# FIXME: The architecture in here is not the greatest... It should be
|
10
|
+
# redesigned entirely.
|
7
11
|
|
8
12
|
# The application
|
9
13
|
module YnabConvert
|
@@ -32,7 +36,7 @@ module YnabConvert
|
|
32
36
|
|
33
37
|
begin
|
34
38
|
@processor = opts[:processor].new(
|
35
|
-
|
39
|
+
filepath: @file
|
36
40
|
)
|
37
41
|
rescue Errno::ENOENT
|
38
42
|
handle_file_not_found
|
@@ -98,7 +102,22 @@ module YnabConvert
|
|
98
102
|
end
|
99
103
|
|
100
104
|
def processor_class_name
|
101
|
-
|
105
|
+
# Processor class names don't always match camelcasing the `-i` argument
|
106
|
+
# from the command line. For those classes that don't, a lookup is
|
107
|
+
# performed to find the proper class name.
|
108
|
+
institution = @options[:institution].to_sym
|
109
|
+
institution_to_classname = {
|
110
|
+
ubs_chequing: 'UBSChequing',
|
111
|
+
ubs_credit: 'UBSCredit'
|
112
|
+
}
|
113
|
+
|
114
|
+
classname = institution_to_classname.fetch(institution) do |el|
|
115
|
+
# If the class name is "regular", it will be found by camelcasing the
|
116
|
+
# name passed as the `-i` argument from the command line.
|
117
|
+
el.to_s.camel_case
|
118
|
+
end
|
119
|
+
|
120
|
+
"Processors::#{classname}"
|
102
121
|
end
|
103
122
|
|
104
123
|
def processor
|
data/ynab_convert.gemspec
CHANGED
@@ -59,9 +59,13 @@ Gem::Specification.new do |spec|
|
|
59
59
|
spec.add_development_dependency 'rspec-core'
|
60
60
|
spec.add_development_dependency 'rubocop'
|
61
61
|
spec.add_development_dependency 'rubocop-rake'
|
62
|
+
spec.add_development_dependency 'rubocop-rspec'
|
62
63
|
spec.add_development_dependency 'simplecov'
|
63
64
|
spec.add_development_dependency 'solargraph'
|
65
|
+
spec.add_development_dependency 'vcr'
|
66
|
+
spec.add_development_dependency 'webmock'
|
64
67
|
|
65
68
|
spec.add_dependency 'i18n'
|
66
69
|
spec.add_dependency 'slop'
|
70
|
+
spec.add_dependency 'timecop'
|
67
71
|
end
|
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:
|
4
|
+
version: 2.0.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- coaxial
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2022-03-
|
11
|
+
date: 2022-03-20 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
@@ -136,6 +136,20 @@ dependencies:
|
|
136
136
|
- - ">="
|
137
137
|
- !ruby/object:Gem::Version
|
138
138
|
version: '0'
|
139
|
+
- !ruby/object:Gem::Dependency
|
140
|
+
name: rubocop-rspec
|
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'
|
139
153
|
- !ruby/object:Gem::Dependency
|
140
154
|
name: simplecov
|
141
155
|
requirement: !ruby/object:Gem::Requirement
|
@@ -164,6 +178,34 @@ dependencies:
|
|
164
178
|
- - ">="
|
165
179
|
- !ruby/object:Gem::Version
|
166
180
|
version: '0'
|
181
|
+
- !ruby/object:Gem::Dependency
|
182
|
+
name: vcr
|
183
|
+
requirement: !ruby/object:Gem::Requirement
|
184
|
+
requirements:
|
185
|
+
- - ">="
|
186
|
+
- !ruby/object:Gem::Version
|
187
|
+
version: '0'
|
188
|
+
type: :development
|
189
|
+
prerelease: false
|
190
|
+
version_requirements: !ruby/object:Gem::Requirement
|
191
|
+
requirements:
|
192
|
+
- - ">="
|
193
|
+
- !ruby/object:Gem::Version
|
194
|
+
version: '0'
|
195
|
+
- !ruby/object:Gem::Dependency
|
196
|
+
name: webmock
|
197
|
+
requirement: !ruby/object:Gem::Requirement
|
198
|
+
requirements:
|
199
|
+
- - ">="
|
200
|
+
- !ruby/object:Gem::Version
|
201
|
+
version: '0'
|
202
|
+
type: :development
|
203
|
+
prerelease: false
|
204
|
+
version_requirements: !ruby/object:Gem::Requirement
|
205
|
+
requirements:
|
206
|
+
- - ">="
|
207
|
+
- !ruby/object:Gem::Version
|
208
|
+
version: '0'
|
167
209
|
- !ruby/object:Gem::Dependency
|
168
210
|
name: i18n
|
169
211
|
requirement: !ruby/object:Gem::Requirement
|
@@ -192,6 +234,20 @@ dependencies:
|
|
192
234
|
- - ">="
|
193
235
|
- !ruby/object:Gem::Version
|
194
236
|
version: '0'
|
237
|
+
- !ruby/object:Gem::Dependency
|
238
|
+
name: timecop
|
239
|
+
requirement: !ruby/object:Gem::Requirement
|
240
|
+
requirements:
|
241
|
+
- - ">="
|
242
|
+
- !ruby/object:Gem::Version
|
243
|
+
version: '0'
|
244
|
+
type: :runtime
|
245
|
+
prerelease: false
|
246
|
+
version_requirements: !ruby/object:Gem::Requirement
|
247
|
+
requirements:
|
248
|
+
- - ">="
|
249
|
+
- !ruby/object:Gem::Version
|
250
|
+
version: '0'
|
195
251
|
description: |
|
196
252
|
Utility to convert CSV statements into the YNAB4 format for easier
|
197
253
|
transation import. Supports several banks and can easily be extended to
|
@@ -224,15 +280,42 @@ files:
|
|
224
280
|
- lib/core_extensions/string.rb
|
225
281
|
- lib/slop/symbol.rb
|
226
282
|
- lib/ynab_convert.rb
|
283
|
+
- lib/ynab_convert/api_clients/api_client.rb
|
284
|
+
- lib/ynab_convert/api_clients/currency_api.rb
|
285
|
+
- lib/ynab_convert/documents.rb
|
286
|
+
- lib/ynab_convert/documents/statements/example_statement.rb
|
287
|
+
- lib/ynab_convert/documents/statements/n26_statement.rb
|
288
|
+
- lib/ynab_convert/documents/statements/statement.rb
|
289
|
+
- lib/ynab_convert/documents/statements/ubs_chequing_statement.rb
|
290
|
+
- lib/ynab_convert/documents/statements/ubs_credit_statement.rb
|
291
|
+
- lib/ynab_convert/documents/statements/wise_statement.rb
|
292
|
+
- lib/ynab_convert/documents/ynab4_files/ynab4_file.rb
|
227
293
|
- lib/ynab_convert/error.rb
|
228
294
|
- lib/ynab_convert/logger.rb
|
229
|
-
- lib/ynab_convert/processor/base.rb
|
230
|
-
- lib/ynab_convert/processor/example.rb
|
231
|
-
- lib/ynab_convert/processor/n26.rb
|
232
|
-
- lib/ynab_convert/processor/revolut.rb
|
233
|
-
- lib/ynab_convert/processor/ubs_chequing.rb
|
234
|
-
- lib/ynab_convert/processor/ubs_credit.rb
|
235
295
|
- lib/ynab_convert/processors.rb
|
296
|
+
- lib/ynab_convert/processors/example_processor.rb
|
297
|
+
- lib/ynab_convert/processors/n26_processor.rb
|
298
|
+
- lib/ynab_convert/processors/processor.rb
|
299
|
+
- lib/ynab_convert/processors/ubs_chequing_processor.rb
|
300
|
+
- lib/ynab_convert/processors/ubs_credit_processor.rb
|
301
|
+
- lib/ynab_convert/processors/wise_processor.rb
|
302
|
+
- lib/ynab_convert/transformers.rb
|
303
|
+
- lib/ynab_convert/transformers/cleaners/cleaner.rb
|
304
|
+
- lib/ynab_convert/transformers/cleaners/n26_cleaner.rb
|
305
|
+
- lib/ynab_convert/transformers/cleaners/ubs_chequing_cleaner.rb
|
306
|
+
- lib/ynab_convert/transformers/cleaners/ubs_credit_cleaner.rb
|
307
|
+
- lib/ynab_convert/transformers/cleaners/wise_cleaner.rb
|
308
|
+
- lib/ynab_convert/transformers/enhancers/enhancer.rb
|
309
|
+
- lib/ynab_convert/transformers/enhancers/n26_enhancer.rb
|
310
|
+
- lib/ynab_convert/transformers/enhancers/wise_enhancer.rb
|
311
|
+
- lib/ynab_convert/transformers/formatters/example_formatter.rb
|
312
|
+
- lib/ynab_convert/transformers/formatters/formatter.rb
|
313
|
+
- lib/ynab_convert/transformers/formatters/n26_formatter.rb
|
314
|
+
- lib/ynab_convert/transformers/formatters/ubs_chequing_formatter.rb
|
315
|
+
- lib/ynab_convert/transformers/formatters/ubs_credit_formatter.rb
|
316
|
+
- lib/ynab_convert/transformers/formatters/wise_formatter.rb
|
317
|
+
- lib/ynab_convert/validators.rb
|
318
|
+
- lib/ynab_convert/validators/ynab4_row_validator.rb
|
236
319
|
- lib/ynab_convert/version.rb
|
237
320
|
- ynab_convert.gemspec
|
238
321
|
homepage: https://github.com/coaxial/ynab_convert
|
@@ -1,226 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require 'core_extensions/string'
|
4
|
-
require 'csv'
|
5
|
-
require 'ynab_convert/logger'
|
6
|
-
|
7
|
-
module Processor
|
8
|
-
# Base class for a Processor, all processors must inherit from it.
|
9
|
-
|
10
|
-
# rubocop:disable Metrics/ClassLength
|
11
|
-
class Base
|
12
|
-
include YnabLogger
|
13
|
-
include CoreExtensions::String::Inflections
|
14
|
-
|
15
|
-
attr_reader :loader_options
|
16
|
-
|
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
|
-
|
27
|
-
logger.debug "Initializing processor with options: `#{opts.to_h}'"
|
28
|
-
raise ::Errno::ENOENT unless File.exist? opts[:file]
|
29
|
-
|
30
|
-
@file = opts[:file]
|
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
|
41
|
-
end
|
42
|
-
|
43
|
-
def to_ynab!
|
44
|
-
begin
|
45
|
-
convert!
|
46
|
-
rename_file
|
47
|
-
rescue YnabConvert::Error
|
48
|
-
invalid_csv_file
|
49
|
-
end
|
50
|
-
ensure
|
51
|
-
logger.debug "Deleting temp file `#{temp_filename}'"
|
52
|
-
delete_temp_csv
|
53
|
-
end
|
54
|
-
|
55
|
-
protected
|
56
|
-
|
57
|
-
attr_accessor :statement_from, :statement_to, :headers
|
58
|
-
|
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)
|
68
|
-
inflow_index = 3
|
69
|
-
outflow_index = 4
|
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
|
94
|
-
end
|
95
|
-
|
96
|
-
def skip_row(row)
|
97
|
-
logger.debug "Found empty row, skipping it: #{row.to_h}"
|
98
|
-
throw :skip_row
|
99
|
-
end
|
100
|
-
|
101
|
-
def delete_temp_csv
|
102
|
-
FileUtils.remove_file temp_filename, force: true
|
103
|
-
end
|
104
|
-
|
105
|
-
def transaction_date_missing?(ynab_row)
|
106
|
-
ynab_row[0].nil? || [0].empty?
|
107
|
-
end
|
108
|
-
|
109
|
-
def extract_transaction_date(ynab_row)
|
110
|
-
transaction_date_index = 0
|
111
|
-
ynab_row[transaction_date_index]
|
112
|
-
end
|
113
|
-
|
114
|
-
def record_statement_interval_dates(ynab_row)
|
115
|
-
transaction_date_index = 0
|
116
|
-
date = Date.parse(ynab_row[transaction_date_index])
|
117
|
-
|
118
|
-
if date_is_further_away?(date)
|
119
|
-
logger.debug "Replacing statement_from `#{statement_from.inspect}' "\
|
120
|
-
"with `#{date}'"
|
121
|
-
self.statement_from = date
|
122
|
-
end
|
123
|
-
# rubocop:disable Style/GuardClause
|
124
|
-
if date_is_more_recent?(date)
|
125
|
-
logger.debug "Replacing statement_to `#{statement_to.inspect}' with "\
|
126
|
-
"`#{date}'"
|
127
|
-
self.statement_to = date
|
128
|
-
end
|
129
|
-
# rubocop:enable Style/GuardClause
|
130
|
-
end
|
131
|
-
|
132
|
-
def date_is_more_recent?(date)
|
133
|
-
statement_to.nil? || statement_to < date
|
134
|
-
end
|
135
|
-
|
136
|
-
def date_is_further_away?(date)
|
137
|
-
statement_from.nil? || statement_from > date
|
138
|
-
end
|
139
|
-
|
140
|
-
def convert!
|
141
|
-
logger.debug "Will write to `#{temp_filename}'"
|
142
|
-
|
143
|
-
logger.debug(loader_options)
|
144
|
-
CSV.open(temp_filename, 'wb', **output_options) do |converted|
|
145
|
-
CSV.foreach(@file, 'rb', **loader_options) do |row|
|
146
|
-
logger.debug "Parsing row: `#{row.to_h}'"
|
147
|
-
# Some rows don't contain valid or useful data
|
148
|
-
catch :skip_row do
|
149
|
-
extract_header_names(row)
|
150
|
-
ynab_row = transformers(row)
|
151
|
-
if amounts_missing?(ynab_row) ||
|
152
|
-
transaction_date_missing?(ynab_row)
|
153
|
-
logger.debug 'Empty row, skipping it'
|
154
|
-
skip_row(row)
|
155
|
-
end
|
156
|
-
converted << ynab_row
|
157
|
-
record_statement_interval_dates(ynab_row)
|
158
|
-
end
|
159
|
-
|
160
|
-
logger.debug 'Done converting'
|
161
|
-
end
|
162
|
-
end
|
163
|
-
end
|
164
|
-
|
165
|
-
def rename_file
|
166
|
-
File.rename(temp_filename, output_filename)
|
167
|
-
logger.debug "Renamed temp file `#{temp_filename}' to "\
|
168
|
-
"`#{output_filename}'"
|
169
|
-
end
|
170
|
-
|
171
|
-
def invalid_csv_file
|
172
|
-
raise YnabConvert::Error, "Unable to parse file `#{@file}'. Is it a "\
|
173
|
-
"valid CSV file from #{@institution_name}?"
|
174
|
-
end
|
175
|
-
|
176
|
-
def file_uid
|
177
|
-
@file_uid ||= rand(36**8).to_s(36)
|
178
|
-
end
|
179
|
-
|
180
|
-
def temp_filename
|
181
|
-
"#{File.basename(@file, '.csv')}_#{@institution_name.snake_case}_"\
|
182
|
-
"#{file_uid}_ynab4.csv"
|
183
|
-
end
|
184
|
-
|
185
|
-
def output_filename
|
186
|
-
# If the file contained no parsable CSV data, from and to dates will be
|
187
|
-
# nil.
|
188
|
-
# This is to avoid a NoMethodError on NilClass.
|
189
|
-
raise YnabConvert::Error if statement_from.nil? || statement_to.nil?
|
190
|
-
|
191
|
-
from = statement_from.strftime('%Y%m%d')
|
192
|
-
to = statement_to.strftime('%Y%m%d')
|
193
|
-
|
194
|
-
"#{File.basename(@file, '.csv')}_#{@institution_name.snake_case}_"\
|
195
|
-
"#{from}-#{to}_ynab4.csv"
|
196
|
-
end
|
197
|
-
|
198
|
-
def ynab_headers
|
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
|
210
|
-
end
|
211
|
-
|
212
|
-
def output_options
|
213
|
-
{
|
214
|
-
converters: %i[numeric date],
|
215
|
-
force_quotes: true,
|
216
|
-
write_headers: true,
|
217
|
-
headers: ynab_headers
|
218
|
-
}
|
219
|
-
end
|
220
|
-
|
221
|
-
def transformers
|
222
|
-
raise NotImplementedError, :transformers
|
223
|
-
end
|
224
|
-
end
|
225
|
-
# rubocop:enable Metrics/ClassLength
|
226
|
-
end
|