ynab_convert 1.0.8 → 2.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.gitignore +2 -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 -137
- data/lib/ynab_convert/processor/ubs_credit.rb +0 -83
@@ -1,124 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module Processor
|
4
|
-
# An example of how to implement a custom processor
|
5
|
-
# Processes CSV files with this format:
|
6
|
-
# <<~ROWS
|
7
|
-
# "Date","Payee","Memo","Outflow","Inflow"
|
8
|
-
# "23/12/2019","coaxial","","1000000.00",""
|
9
|
-
# "30/12/2019","Santa","","50000.00",""
|
10
|
-
# "02/02/2020","Someone Else","","45.00",""
|
11
|
-
# ROWS
|
12
|
-
# The file name for the processor should be the institution name in
|
13
|
-
# camel case. It's ok to skip "Bank" or "Credit Union" when naming the file
|
14
|
-
# if it's redundant. For instance, this parser is for "Example Bank" but it's
|
15
|
-
# named "example.rb", its corresponding spec is
|
16
|
-
# "spec/example_processor_spec.rb" and its fixture would be
|
17
|
-
# "spec/fixtures/example/statement.csv"
|
18
|
-
class Example < Processor::Base
|
19
|
-
# @option options [String] :file Path to the CSV file to process
|
20
|
-
def initialize(options)
|
21
|
-
# Custom converters can be added so that the CSV data is parsed when
|
22
|
-
# loading the original file
|
23
|
-
register_custom_converters
|
24
|
-
|
25
|
-
# These are the options for the CSV module (see
|
26
|
-
# https://ruby-doc.org/stdlib-2.6/libdoc/csv/rdoc/CSV.html#method-c-new)
|
27
|
-
# They should match the format for the CSV file that the financial
|
28
|
-
# institution generates.
|
29
|
-
@loader_options = {
|
30
|
-
col_sep: ';',
|
31
|
-
# Use your converters, if any
|
32
|
-
converters: %i[transaction_date my_converter],
|
33
|
-
headers: true
|
34
|
-
}
|
35
|
-
|
36
|
-
# This is the financial institution's full name as it calls itself. This
|
37
|
-
# usually matches the institution's letterhead and/or commercial name.
|
38
|
-
# It can happen that the same institution needs different parsers because
|
39
|
-
# its credit card CSV files are in one format, and its chequing accounts
|
40
|
-
# in another. In that case, more details can be added in parens.
|
41
|
-
# For instance:
|
42
|
-
# 'Example Bank (credit cards)' and 'Example Bank (chequing)'
|
43
|
-
@institution_name = 'Example Bank'
|
44
|
-
|
45
|
-
# This is mandatory.
|
46
|
-
super(options)
|
47
|
-
end
|
48
|
-
|
49
|
-
private
|
50
|
-
|
51
|
-
def register_custom_converters
|
52
|
-
CSV::Converters[:transaction_date] = lambda { |s|
|
53
|
-
# Only match strings that have two digits, a dot, two digits, a dot,
|
54
|
-
# two digits, i.e. the dates in this institution's CSV files.
|
55
|
-
date_regex = /^\d{2}\.\d{2}\.\d{2}$/
|
56
|
-
|
57
|
-
if !s.nil? && s.match(date_regex)
|
58
|
-
parsed_date = Date.strptime(s, '%d.%m.%y')
|
59
|
-
logger.debug "Converted `#{s.inspect}' into date "\
|
60
|
-
"`#{parsed_date}'"
|
61
|
-
return parsed_date
|
62
|
-
end
|
63
|
-
|
64
|
-
s
|
65
|
-
}
|
66
|
-
|
67
|
-
CSV::Converters[:my_converter] = lambda { |s|
|
68
|
-
# A contrived example, just to illustrate multiple converters
|
69
|
-
if s.respond_to?(:downcase)
|
70
|
-
converted_s = s.downcase
|
71
|
-
logger.debug "Converted `#{s.inspect}' into downcased string "\
|
72
|
-
"`#{converted_s}'"
|
73
|
-
return converted_s
|
74
|
-
end
|
75
|
-
|
76
|
-
s
|
77
|
-
}
|
78
|
-
end
|
79
|
-
|
80
|
-
protected
|
81
|
-
|
82
|
-
# Converts the institution's CSV rows into YNAB4 rows.
|
83
|
-
# The YNAB4 columns are:
|
84
|
-
# "Date', "Payee", "Memo", "Outflow", "Inflow"
|
85
|
-
# which match Example Bank's "transaction_date" (after parsing),
|
86
|
-
# "beneficiary", nothing, "debit", and "credit" respectively.
|
87
|
-
# Note that Example Bank doesn't include any relevant column for YNAB4's
|
88
|
-
# "Memo" column so it's skipped and gets '' as its value.
|
89
|
-
def transformers(row)
|
90
|
-
# Convert the original transaction_date to DD/MM/YYYY as YNAB4 expects
|
91
|
-
# it.
|
92
|
-
unless row[headers[:transaction_date]].nil?
|
93
|
-
transaction_date = row[headers[:transaction_date]].strftime('%d/%m/%Y')
|
94
|
-
end
|
95
|
-
payee = row[headers[:payee]]
|
96
|
-
debit = row[headers[:debit]]
|
97
|
-
credit = row[headers[:credit]]
|
98
|
-
|
99
|
-
# CSV files can have funny data in them, including invalid or empty rows.
|
100
|
-
# These rows can be skipped from the converted YNAB4 file by calling
|
101
|
-
# skip_row when detected. In this particular case, if there is no
|
102
|
-
# transaction date, it means the row is empty or invalid and we discard
|
103
|
-
# it.
|
104
|
-
skip_row(row) if transaction_date.nil?
|
105
|
-
converted_row = [transaction_date, payee, nil, debit, credit]
|
106
|
-
logger.debug "Converted row: #{converted_row}"
|
107
|
-
converted_row
|
108
|
-
end
|
109
|
-
|
110
|
-
private
|
111
|
-
|
112
|
-
# Institutions love translating the column names, apparently. Rather than
|
113
|
-
# hardcoding the column name as a string, use the headers array at the
|
114
|
-
# right index.
|
115
|
-
# These lookups aren't particularly expensive but they're done on each row
|
116
|
-
# so why not memoize them with ||=
|
117
|
-
def extract_header_names(row)
|
118
|
-
headers[:transaction_date] ||= row.headers[0]
|
119
|
-
headers[:payee] ||= row.headers[2]
|
120
|
-
headers[:debit] ||= row.headers[3]
|
121
|
-
headers[:credit] ||= row.headers[4]
|
122
|
-
end
|
123
|
-
end
|
124
|
-
end
|
@@ -1,70 +0,0 @@
|
|
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,103 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require 'i18n'
|
4
|
-
|
5
|
-
module Processor
|
6
|
-
# Processes CSV files from Revolut
|
7
|
-
class Revolut < Processor::Base
|
8
|
-
# @option options [String] :file Path to the CSV file to process
|
9
|
-
def initialize(options)
|
10
|
-
register_custom_converters
|
11
|
-
@loader_options = {
|
12
|
-
col_sep: ';',
|
13
|
-
converters: %i[amounts transaction_dates],
|
14
|
-
quote_char: nil,
|
15
|
-
encoding: Encoding::UTF_8,
|
16
|
-
headers: true
|
17
|
-
}
|
18
|
-
@institution_name = 'Revolut'
|
19
|
-
|
20
|
-
super(options)
|
21
|
-
end
|
22
|
-
|
23
|
-
protected
|
24
|
-
|
25
|
-
def transformers(row)
|
26
|
-
date = extract_transaction_date(row).strftime('%d/%m/%Y')
|
27
|
-
payee = row[headers[:payee]]
|
28
|
-
unless row[headers[:debit]].nil?
|
29
|
-
debit = format('%<amount>.2f', amount: row[headers[:debit]])
|
30
|
-
end
|
31
|
-
unless row[headers[:credit]].nil?
|
32
|
-
credit = format('%<amount>.2f', amount: row[headers[:credit]])
|
33
|
-
end
|
34
|
-
|
35
|
-
ynab_row = [
|
36
|
-
date,
|
37
|
-
payee,
|
38
|
-
nil,
|
39
|
-
debit,
|
40
|
-
credit
|
41
|
-
]
|
42
|
-
|
43
|
-
logger.debug "Converted row: #{ynab_row}"
|
44
|
-
ynab_row
|
45
|
-
end
|
46
|
-
|
47
|
-
private
|
48
|
-
|
49
|
-
def extract_header_names(row)
|
50
|
-
@headers[:transaction_date] ||= row.headers[0]
|
51
|
-
@headers[:payee] ||= row.headers[1]
|
52
|
-
@headers[:debit] ||= row.headers[2]
|
53
|
-
@headers[:credit] ||= row.headers[3]
|
54
|
-
end
|
55
|
-
|
56
|
-
def register_custom_converters
|
57
|
-
CSV::Converters[:amounts] = lambda { |s|
|
58
|
-
# Yes, amount come with a non breaking trailing space... Which is
|
59
|
-
# matched with \p{Zs} (c.f.
|
60
|
-
# https://ruby-doc.org/core-2.6/Regexp.html#class-Regexp-label-Character+Properties)
|
61
|
-
# Also, thousands separators can be non breaking spaces.
|
62
|
-
amount_regex = /^[\d'\.,\p{Zs}]+[\.,]\d{2}\p{Zs}$/
|
63
|
-
# narrow_nbsp = "\0xE2\0x80\0xAF"
|
64
|
-
narrow_nbsp = "\u{202F}"
|
65
|
-
readability_separators = "',. #{narrow_nbsp}"
|
66
|
-
|
67
|
-
if !s.nil? && s.match(amount_regex)
|
68
|
-
# This is a bit hacky because we don't have the luxury of Rails' i18n
|
69
|
-
# helpers. If we have an amount, strip all the separators in it, turn
|
70
|
-
# it to a float, and divide by 100 to get the right amount back
|
71
|
-
amount = s.delete(readability_separators).to_f / 100
|
72
|
-
logger.debug "Converted `#{s}' into amount `#{amount}'"
|
73
|
-
return amount
|
74
|
-
end
|
75
|
-
|
76
|
-
logger.debug "Not an amount, not parsing `#{s.inspect}'"
|
77
|
-
s
|
78
|
-
}
|
79
|
-
|
80
|
-
# rubocop:disable Style/AsciiComments
|
81
|
-
CSV::Converters[:transaction_dates] = lambda { |s|
|
82
|
-
begin
|
83
|
-
# Date.parse('6 decembre') is fine, but Date.parse('6 décembre') is
|
84
|
-
# an invalid date so we must remove diacritics before trying to parse
|
85
|
-
I18n.available_locales = [:en]
|
86
|
-
transliterated_s = I18n.transliterate s
|
87
|
-
logger.debug "Converted `#{s.inspect}' into date "\
|
88
|
-
"`#{Date.parse(transliterated_s)}'"
|
89
|
-
Date.parse(transliterated_s)
|
90
|
-
rescue StandardError
|
91
|
-
logger.debug "Not a date, not parsing #{s.inspect}"
|
92
|
-
s
|
93
|
-
end
|
94
|
-
}
|
95
|
-
# rubocop:enable Style/AsciiComments
|
96
|
-
end
|
97
|
-
|
98
|
-
def missing_transaction_date?(row)
|
99
|
-
# If It's missing a transaction date, it's most likely invalid
|
100
|
-
row[headers[:transaction_date]].nil?
|
101
|
-
end
|
102
|
-
end
|
103
|
-
end
|
@@ -1,137 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module Processor
|
4
|
-
# Processes CSV files from UBS Personal Banking Switzerland
|
5
|
-
class UbsChequing < Processor::Base
|
6
|
-
# @option options [String] :file Path to the CSV file to process
|
7
|
-
def initialize(options)
|
8
|
-
register_custom_converters
|
9
|
-
@loader_options = {
|
10
|
-
col_sep: ';',
|
11
|
-
converters: %i[amounts transaction_dates],
|
12
|
-
quote_char: nil,
|
13
|
-
encoding: Encoding::UTF_8,
|
14
|
-
headers: true
|
15
|
-
}
|
16
|
-
@institution_name = 'UBS (Chequing)'
|
17
|
-
|
18
|
-
super(options)
|
19
|
-
end
|
20
|
-
|
21
|
-
protected
|
22
|
-
|
23
|
-
def transformers(row)
|
24
|
-
date = extract_transaction_date(row).strftime('%d/%m/%Y')
|
25
|
-
payee = transaction_payee(row)
|
26
|
-
unless row[headers[:debit]].nil?
|
27
|
-
debit = format('%<amount>.2f', amount: row[headers[:debit]])
|
28
|
-
end
|
29
|
-
unless row[headers[:credit]].nil?
|
30
|
-
credit = format('%<amount>.2f', amount: row[headers[:credit]])
|
31
|
-
end
|
32
|
-
|
33
|
-
converted_row = [
|
34
|
-
date,
|
35
|
-
payee,
|
36
|
-
nil,
|
37
|
-
debit,
|
38
|
-
credit
|
39
|
-
]
|
40
|
-
|
41
|
-
logger.debug "Converted row: #{converted_row}"
|
42
|
-
converted_row
|
43
|
-
end
|
44
|
-
|
45
|
-
def extract_transaction_date(row)
|
46
|
-
skip_row(row) if row[headers[:transaction_date]].nil?
|
47
|
-
row[headers[:transaction_date]]
|
48
|
-
end
|
49
|
-
|
50
|
-
private
|
51
|
-
|
52
|
-
def extract_header_names(row)
|
53
|
-
headers[:transaction_date] ||= row.headers[9]
|
54
|
-
headers[:payee_line_1] ||= row.headers[12]
|
55
|
-
headers[:payee_line_2] ||= row.headers[13]
|
56
|
-
headers[:payee_line_3] ||= row.headers[14]
|
57
|
-
headers[:debit] ||= row.headers[18]
|
58
|
-
headers[:credit] ||= row.headers[19]
|
59
|
-
end
|
60
|
-
|
61
|
-
def transaction_payee(row)
|
62
|
-
raw_payee_line = [
|
63
|
-
row[headers[:payee_line_2]],
|
64
|
-
row[headers[:payee_line_3]]
|
65
|
-
]
|
66
|
-
|
67
|
-
# Transaction description is spread over 3 columns.
|
68
|
-
# There are two types of entries:
|
69
|
-
# 1. only the first column contains data
|
70
|
-
# 2. all three columns contain data, most of it junk
|
71
|
-
#
|
72
|
-
# Cleaning them up means dropping the first column if there is anything
|
73
|
-
# in the other columns;
|
74
|
-
# removing the CARD 00000000-0 0000 at the beginning of debit card
|
75
|
-
# payment entries;
|
76
|
-
# removing the rest of the junk appended after the worthwhile data (see
|
77
|
-
# below for details on that)
|
78
|
-
if row[headers[:payee_line_2]].nil?
|
79
|
-
# Make it an Array, for consistency
|
80
|
-
raw_payee_line = [row[headers[:payee_line_1]]]
|
81
|
-
end
|
82
|
-
|
83
|
-
concat_payee_line = raw_payee_line.join(' ')
|
84
|
-
|
85
|
-
# Moreover, UBS thought wise to append a bunch of junk information after
|
86
|
-
# the transaction details within the third description field. *Most* of
|
87
|
-
# this junk starts after the meaningful data and starts with ", OF",
|
88
|
-
# ", ON", ", ESR", ", QRR", two digits then five groups of five digits
|
89
|
-
# then ", TN" so we discard it; YNAB4 being unable to automatically
|
90
|
-
# categorize new transactions at the same store/payee because the payee
|
91
|
-
# always looks different (thanks to the variable nature of the appended
|
92
|
-
# junk).
|
93
|
-
# See `spec/fixtures/ubs_chequing/statement.csv` L2 and L18--22
|
94
|
-
|
95
|
-
# rubocop:disable Metrics/LineLength
|
96
|
-
junk_desc_regex = /,? (O[FN]|ESR|QRR|\d{2} \d{5} \d{5} \d{5} \d{5} \d{5}, TN).*/
|
97
|
-
# rubocop:enable Metrics/LineLength
|
98
|
-
|
99
|
-
# Of course, it wouldn't be complete with more junk information at the
|
100
|
-
# beginning of *some* lines (debit card payments)
|
101
|
-
debit_card_junk_regex = /CARD \d{8}\-\d \d{4} /
|
102
|
-
|
103
|
-
concat_payee_line.sub(junk_desc_regex, '').sub(debit_card_junk_regex, '')
|
104
|
-
end
|
105
|
-
|
106
|
-
def register_custom_converters
|
107
|
-
CSV::Converters[:amounts] = lambda { |s|
|
108
|
-
# Regex checks if string has only digits, apostrophes, and ends with a
|
109
|
-
# dot and two digits
|
110
|
-
amount_regex = /^[\d'?]+\.\d{2}$/
|
111
|
-
|
112
|
-
if !s.nil? && s.match(amount_regex)
|
113
|
-
amount = s.delete("'") .to_f
|
114
|
-
logger.debug "Converted `#{s}' into amount `#{amount}'"
|
115
|
-
return amount
|
116
|
-
end
|
117
|
-
|
118
|
-
logger.debug "Not an amount, not parsing `#{s.inspect}'"
|
119
|
-
s
|
120
|
-
}
|
121
|
-
|
122
|
-
CSV::Converters[:transaction_dates] = lambda { |s|
|
123
|
-
date_regex = /^\d{2}\.\d{2}\.\d{4}$/
|
124
|
-
|
125
|
-
if !s.nil? && s.match(date_regex)
|
126
|
-
parsed_date = Date.parse(s)
|
127
|
-
logger.debug "Converted `#{s.inspect}' into date "\
|
128
|
-
"`#{parsed_date}'"
|
129
|
-
parsed_date
|
130
|
-
else
|
131
|
-
logger.debug "Not a date, not parsing #{s.inspect}"
|
132
|
-
s
|
133
|
-
end
|
134
|
-
}
|
135
|
-
end
|
136
|
-
end
|
137
|
-
end
|
@@ -1,83 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module Processor
|
4
|
-
# Processes CSV files from UBS Credit Cards Switzerland
|
5
|
-
class UbsCredit < Processor::Base
|
6
|
-
# @option options [String] :file Path to the CSV file to process
|
7
|
-
def initialize(options)
|
8
|
-
register_custom_converters
|
9
|
-
@loader_options = {
|
10
|
-
col_sep: ';',
|
11
|
-
converters: %i[amounts transaction_dates],
|
12
|
-
quote_char: nil,
|
13
|
-
encoding: "#{Encoding::ISO_8859_1}:#{Encoding::UTF_8}",
|
14
|
-
headers: true,
|
15
|
-
# CSV FTW, the first line in these files is not the headers but the
|
16
|
-
# separator specification
|
17
|
-
skip_lines: 'sep=;'
|
18
|
-
}
|
19
|
-
@institution_name = 'UBS (Credit cards)'
|
20
|
-
|
21
|
-
super(options)
|
22
|
-
end
|
23
|
-
|
24
|
-
protected
|
25
|
-
|
26
|
-
def transformers(row)
|
27
|
-
unless row[headers[:transaction_date]].nil?
|
28
|
-
date = row[headers[:transaction_date]].strftime('%d/%m/%Y')
|
29
|
-
end
|
30
|
-
payee = row[headers[:payee]]
|
31
|
-
unless row[headers[:debit]].nil?
|
32
|
-
debit = format('%<amount>.2f', amount: row[headers[:debit]])
|
33
|
-
end
|
34
|
-
unless row[headers[:credit]].nil?
|
35
|
-
credit = format('%<amount>.2f', amount: row[headers[:credit]])
|
36
|
-
end
|
37
|
-
|
38
|
-
converted_row = [date, payee, nil, debit, credit]
|
39
|
-
logger.debug "Converted row: #{converted_row}"
|
40
|
-
converted_row
|
41
|
-
end
|
42
|
-
|
43
|
-
private
|
44
|
-
|
45
|
-
def extract_header_names(row)
|
46
|
-
headers[:transaction_date] ||= row.headers[3]
|
47
|
-
headers[:payee] ||= row.headers[4]
|
48
|
-
headers[:debit] ||= row.headers[10]
|
49
|
-
headers[:credit] ||= row.headers[11]
|
50
|
-
end
|
51
|
-
|
52
|
-
def register_custom_converters
|
53
|
-
CSV::Converters[:amounts] = lambda { |s|
|
54
|
-
# Regex checks if string has only digits, apostrophes, and ends with a
|
55
|
-
# dot and two digits
|
56
|
-
amount_regex = /^[\d'?]+(\.\d{2})$/
|
57
|
-
|
58
|
-
if !s.nil? && s.match(amount_regex)
|
59
|
-
amount = s.delete("'") .to_f
|
60
|
-
logger.debug "Converted `#{s}' into amount `#{amount}'"
|
61
|
-
return amount
|
62
|
-
end
|
63
|
-
|
64
|
-
logger.debug "Not an amount, not parsing `#{s.inspect}'"
|
65
|
-
s
|
66
|
-
}
|
67
|
-
|
68
|
-
CSV::Converters[:transaction_dates] = lambda { |s|
|
69
|
-
date_regex = /^\d{2}\.\d{2}\.\d{4}$/
|
70
|
-
|
71
|
-
if !s.nil? && s.match(date_regex)
|
72
|
-
parsed_date = Date.parse(s)
|
73
|
-
logger.debug "Converted `#{s.inspect}' into date "\
|
74
|
-
"`#{parsed_date}'"
|
75
|
-
parsed_date
|
76
|
-
else
|
77
|
-
logger.debug "Not a date, not parsing #{s.inspect}"
|
78
|
-
s
|
79
|
-
end
|
80
|
-
}
|
81
|
-
end
|
82
|
-
end
|
83
|
-
end
|