ynab_convert 1.0.6 → 2.0.0
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 +76 -5
- 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 +4 -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,8 @@
|
|
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']
|
7
8
|
|
8
9
|
# The application
|
9
10
|
module YnabConvert
|
@@ -32,7 +33,7 @@ module YnabConvert
|
|
32
33
|
|
33
34
|
begin
|
34
35
|
@processor = opts[:processor].new(
|
35
|
-
|
36
|
+
filepath: @file
|
36
37
|
)
|
37
38
|
rescue Errno::ENOENT
|
38
39
|
handle_file_not_found
|
@@ -98,7 +99,7 @@ module YnabConvert
|
|
98
99
|
end
|
99
100
|
|
100
101
|
def processor_class_name
|
101
|
-
"
|
102
|
+
"Processors::#{@options[:institution].camel_case}"
|
102
103
|
end
|
103
104
|
|
104
105
|
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.0
|
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
|