mercury_banking 0.5.38 → 0.7.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.
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'mercury_banking/cli/base'
2
4
  require 'mercury_banking/cli/accounts'
3
5
  require 'mercury_banking/cli/reports'
@@ -5,7 +7,11 @@ require 'mercury_banking/cli/transactions'
5
7
  require 'mercury_banking/cli/financials'
6
8
  require 'mercury_banking/formatters/table_formatter'
7
9
  require 'mercury_banking/formatters/export_formatter'
8
- require 'mercury_banking/reports/balance_sheet'
10
+ require 'symmetric-encryption'
11
+ require 'json'
12
+ require 'base64'
13
+ require 'fileutils'
14
+ require 'securerandom'
9
15
 
10
16
  module MercuryBanking
11
17
  # CLI module for Mercury Banking
@@ -19,91 +25,91 @@ module MercuryBanking
19
25
  include MercuryBanking::CLI::Financials
20
26
  include MercuryBanking::Formatters::TableFormatter
21
27
  include MercuryBanking::Formatters::ExportFormatter
22
- include MercuryBanking::Reports::BalanceSheet
23
-
28
+
24
29
  # Add global option for JSON output
25
30
  class_option :json, type: :boolean, default: false, desc: 'Output in JSON format'
26
-
31
+
27
32
  map %w[--version -v] => :version
28
-
33
+
29
34
  # Add version banner to help output
30
35
  def self.banner(command, _namespace = nil, _subcommand = false)
31
36
  "#{basename} #{command.usage}"
32
37
  end
33
-
38
+
34
39
  def self.help(shell, _subcommand = false)
35
40
  shell.say "Mercury Banking CLI v#{MercuryBanking::VERSION} - Command-line interface for Mercury Banking API"
36
41
  shell.say
37
42
  super
38
43
  end
39
-
44
+
40
45
  # Handle Thor deprecation warning
41
46
  def self.exit_on_failure?
42
47
  true
43
48
  end
44
-
49
+
45
50
  desc 'version', 'Display Mercury Banking CLI version'
46
51
  def version
47
52
  if options[:json]
48
53
  puts JSON.pretty_generate({
49
- 'name' => 'mercury-banking',
50
- 'version' => MercuryBanking::VERSION,
51
- 'ruby_version' => RUBY_VERSION
52
- })
54
+ 'name' => 'mercury-banking',
55
+ 'version' => MercuryBanking::VERSION,
56
+ 'ruby_version' => RUBY_VERSION
57
+ })
53
58
  else
54
59
  puts "Mercury Banking CLI v#{MercuryBanking::VERSION} (Ruby #{RUBY_VERSION})"
55
60
  end
56
61
  end
57
-
62
+
58
63
  desc 'set_key', 'Sets and encrypts the API key'
59
64
  def set_key
60
65
  # Create the .mercury-banking directory if it doesn't exist
61
66
  config_dir = File.join(Dir.home, '.mercury-banking')
62
67
  FileUtils.mkdir_p(config_dir)
63
-
68
+
64
69
  # Generate a random key and IV for encryption
65
70
  key = SecureRandom.random_bytes(32)
66
71
  iv = SecureRandom.random_bytes(16)
67
-
72
+
68
73
  # Create a cipher config
69
74
  cipher_config = {
70
75
  key: Base64.strict_encode64(key),
71
76
  iv: Base64.strict_encode64(iv),
72
77
  cipher_name: 'aes-256-cbc'
73
78
  }
74
-
79
+
75
80
  # Save the cipher config to a file
76
81
  key_config_path = File.join(config_dir, 'key_config.json')
77
82
  File.write(key_config_path, JSON.pretty_generate(cipher_config))
78
-
83
+
79
84
  # Initialize the SymmetricEncryption with the generated cipher
80
85
  cipher = SymmetricEncryption::Cipher.new(
81
86
  key: key,
82
87
  iv: iv,
83
88
  cipher_name: 'aes-256-cbc'
84
89
  )
85
-
90
+
86
91
  # Set the cipher as the primary one
87
92
  SymmetricEncryption.cipher = cipher
88
-
93
+
89
94
  # Get the API key from the user
90
95
  api_key = ask('Enter your Mercury API key:')
91
-
96
+
92
97
  # Encrypt the API key
93
98
  encrypted_key = cipher.encrypt(api_key)
94
-
99
+
95
100
  # Save the encrypted API key to a file
96
101
  api_key_path = File.join(config_dir, 'api_key.enc')
97
102
  File.write(api_key_path, encrypted_key)
98
-
103
+
99
104
  puts "API key encrypted and saved to #{api_key_path}"
100
105
  end
101
-
106
+
102
107
  desc 'transactions ACCOUNT_ID_OR_NUMBER', 'List transactions for an account with their status'
103
108
  method_option :start, type: :string, default: '2020-01-01', desc: 'Start date for transactions (YYYY-MM-DD)'
104
109
  method_option :end, type: :string, desc: 'End date for transactions (YYYY-MM-DD)'
105
110
  method_option :status, type: :string, desc: 'Filter by transaction status (e.g., sent, failed, pending)'
106
- method_option :format, type: :string, default: 'table', enum: ['table', 'json'], desc: 'Output format (table or json)'
111
+ method_option :format, type: :string, default: 'table', enum: %w[table json],
112
+ desc: 'Output format (table or json)'
107
113
  method_option :search, type: :string, desc: 'Search for transactions by description'
108
114
  def transactions(account_identifier)
109
115
  with_api_client do |client|
@@ -113,36 +119,34 @@ module MercuryBanking
113
119
  begin
114
120
  account = client.find_account_by_number(account_identifier)
115
121
  account_id = account["id"]
116
- rescue => e
117
- # If not found by number, assume it's an ID
118
- account_id = account_identifier
119
- account = client.get_account(account_id)
122
+ rescue StandardError
123
+ # Handle error without assigning to unused variable
124
+ puts "Error: Could not find account with identifier #{account_identifier}"
125
+ return []
120
126
  end
121
127
  else
122
128
  account_id = account_identifier
123
129
  account = client.get_account(account_id)
124
130
  end
125
-
131
+
126
132
  # Get transactions for the account
127
133
  start_date = options[:start]
128
134
  end_date = options[:end]
129
-
135
+
130
136
  transactions = client.get_transactions(account_id, start_date)
131
-
137
+
132
138
  # Filter by end date if specified
133
139
  if end_date
134
140
  end_date_obj = Date.parse(end_date)
135
141
  transactions = transactions.select do |t|
136
- transaction_date = t["postedAt"] ? Date.parse(t["postedAt"]) : Date.parse(t["createdAt"])
142
+ transaction_date = Date.parse(t["postedAt"] || t["createdAt"])
137
143
  transaction_date <= end_date_obj
138
144
  end
139
145
  end
140
-
146
+
141
147
  # Filter by status if specified
142
- if options[:status]
143
- transactions = transactions.select { |t| t["status"] == options[:status] }
144
- end
145
-
148
+ transactions = transactions.select { |t| t["status"] == options[:status] } if options[:status]
149
+
146
150
  # Filter by search term if specified
147
151
  if options[:search]
148
152
  search_term = options[:search].downcase
@@ -151,7 +155,7 @@ module MercuryBanking
151
155
  description.downcase.include?(search_term)
152
156
  end
153
157
  end
154
-
158
+
155
159
  if options[:json] || options[:format] == 'json'
156
160
  # Format transactions for JSON output
157
161
  formatted_transactions = transactions.map do |t|
@@ -167,21 +171,21 @@ module MercuryBanking
167
171
  'reason_for_failure' => t["reasonForFailure"]
168
172
  }
169
173
  end
170
-
174
+
171
175
  puts JSON.pretty_generate({
172
- 'account_id' => account_id,
173
- 'account_name' => account['name'],
174
- 'account_number' => account['accountNumber'],
175
- 'total_transactions' => transactions.size,
176
- 'transactions' => formatted_transactions
177
- })
176
+ 'account_id' => account_id,
177
+ 'account_name' => account['name'],
178
+ 'account_number' => account['accountNumber'],
179
+ 'total_transactions' => transactions.size,
180
+ 'transactions' => formatted_transactions
181
+ })
178
182
  else
179
183
  # Display transactions in a table
180
184
  puts "Transactions for #{account['name']} (#{account['accountNumber']})"
181
185
  puts "Period: #{start_date} to #{end_date || 'present'}"
182
186
  puts "Total transactions: #{transactions.size}"
183
187
  puts
184
-
188
+
185
189
  rows = transactions.map do |t|
186
190
  date = t["postedAt"] ? Time.parse(t["postedAt"]).strftime("%Y-%m-%d") : Time.parse(t["createdAt"]).strftime("%Y-%m-%d")
187
191
  description = t["bankDescription"] || t["externalMemo"] || "Unknown transaction"
@@ -191,19 +195,20 @@ module MercuryBanking
191
195
  kind = t["kind"]
192
196
  reconciled = false
193
197
  failure_reason = t["reasonForFailure"] || ""
194
-
198
+
195
199
  [t["id"], date, description, amount, status, kind, reconciled, failure_reason]
196
200
  end
197
-
201
+
198
202
  table = ::Terminal::Table.new(
199
- headings: ['Transaction ID', 'Date', 'Description', 'Amount', 'Status', 'Type', 'Reconciled', 'Failure Reason'],
203
+ headings: ['Transaction ID', 'Date', 'Description', 'Amount', 'Status', 'Type', 'Reconciled',
204
+ 'Failure Reason'],
200
205
  rows: rows
201
206
  )
202
-
207
+
203
208
  puts table
204
209
  end
205
210
  end
206
211
  end
207
212
  end
208
213
  end
209
- end
214
+ end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module MercuryBanking
2
4
  module Formatters
3
5
  # Formatter for exporting transactions to different formats
@@ -8,7 +10,7 @@ module MercuryBanking
8
10
  file.puts "; Mercury Bank Transactions Export"
9
11
  file.puts "; Generated on #{Time.now.strftime('%Y-%m-%d %H:%M:%S')}"
10
12
  file.puts
11
-
13
+
12
14
  # Add account declarations to ensure ledger recognizes them
13
15
  accounts = transactions.map { |t| t["accountName"] || "Mercury Account" }.uniq
14
16
  accounts.each do |account|
@@ -17,7 +19,7 @@ module MercuryBanking
17
19
  file.puts "account Expenses:Unknown"
18
20
  file.puts "account Income:Unknown"
19
21
  file.puts
20
-
22
+
21
23
  # Debug: Show what accounts are being created
22
24
  if verbose
23
25
  puts "Creating ledger file with the following accounts:"
@@ -25,13 +27,13 @@ module MercuryBanking
25
27
  puts "- Assets:#{account}"
26
28
  end
27
29
  end
28
-
30
+
29
31
  # Filter out failed transactions
30
32
  valid_transactions = transactions.reject { |t| t["status"] == "failed" }
31
-
33
+
32
34
  # Sort transactions chronologically by timestamp
33
35
  sorted_transactions = sort_transactions_chronologically(valid_transactions)
34
-
36
+
35
37
  sorted_transactions.each do |t|
36
38
  # Get the full timestamp for precise ordering
37
39
  timestamp = t["postedAt"] || t["createdAt"]
@@ -41,63 +43,60 @@ module MercuryBanking
41
43
  amount = t["amount"].to_f
42
44
  account_name = t["accountName"] || "Mercury Account"
43
45
  transaction_id = t["id"]
44
-
46
+
45
47
  # Check if this transaction is reconciled
46
48
  is_reconciled = reconciled_transactions.include?(transaction_id)
47
-
49
+
48
50
  # Sanitize description for ledger format - replace newlines with spaces and escape semicolons
49
51
  description = description.gsub(/[\r\n]+/, ' ').gsub(/;/, '\\;')
50
-
52
+
51
53
  # Ensure the description doesn't start with characters that could be interpreted as a date
52
54
  # or other ledger syntax elements
53
- if description =~ /^\d/ || description =~ /^[v#]/ || description =~ /^\s*\d{4}/ || description =~ /^\s*\d{2}\/\d{2}/
55
+ if description =~ /^\d/ || description =~ /^[v#]/ || description =~ /^\s*\d{4}/ || description =~ %r{^\s*\d{2}/\d{2}}
54
56
  description = "- #{description}"
55
57
  end
56
-
58
+
57
59
  # Format the transaction with proper indentation and alignment
58
60
  file.puts "#{date} #{is_reconciled ? '* ' : ''}#{description}"
59
61
  file.puts " ; Transaction ID: #{transaction_id}"
60
62
  file.puts " ; Timestamp: #{timestamp}"
61
63
  file.puts " ; Time: #{time}"
62
- file.puts " ; Status: #{t["status"]}"
63
-
64
+ file.puts " ; Status: #{t['status']}"
65
+
64
66
  # Add reconciliation metadata
65
- if is_reconciled
66
- file.puts " ; :reconciled: true"
67
- end
68
-
67
+ file.puts " ; :reconciled: true" if is_reconciled
68
+
69
69
  # Add counterparty information if available
70
70
  if t["counterpartyName"]
71
71
  # Sanitize counterparty name
72
72
  counterparty = t["counterpartyName"].gsub(/[\r\n]+/, ' ').gsub(/;/, '\\;')
73
73
  file.puts " ; Counterparty: #{counterparty}"
74
74
  end
75
-
75
+
76
76
  # Add additional metadata if available
77
77
  if t["externalMemo"] && t["externalMemo"] != description
78
78
  # Sanitize memo
79
79
  memo = t["externalMemo"].gsub(/[\r\n]+/, ' ').gsub(/;/, '\\;')
80
80
  file.puts " ; Memo: #{memo}"
81
81
  end
82
-
82
+
83
83
  # Add the postings
84
- if amount > 0
84
+ file.puts " Assets:#{account_name} $#{format('%.2f', amount)}"
85
+ if amount.positive?
85
86
  # Credit (money coming in)
86
- file.puts " Assets:#{account_name} $#{format('%.2f', amount)}"
87
87
  file.puts " Income:Unknown $#{format('%.2f', -amount)}"
88
88
  else
89
89
  # Debit (money going out)
90
- file.puts " Assets:#{account_name} $#{format('%.2f', amount)}"
91
90
  file.puts " Expenses:Unknown $#{format('%.2f', amount.abs)}"
92
91
  end
93
92
  file.puts
94
93
  end
95
94
  end
96
-
95
+
97
96
  puts "Verifying ledger file validity..."
98
97
  verify_ledger_file(output_file, verbose)
99
98
  end
100
-
99
+
101
100
  # Sort transactions chronologically by timestamp
102
101
  def sort_transactions_chronologically(transactions)
103
102
  transactions.sort_by do |t|
@@ -106,65 +105,68 @@ module MercuryBanking
106
105
  Time.parse(timestamp)
107
106
  end
108
107
  end
109
-
108
+
110
109
  # Verify that the ledger file is valid
111
110
  def verify_ledger_file(output_file, verbose = false)
112
- begin
113
- # Debug: Verify the ledger file is valid
114
- cmd = "ledger -f #{output_file} balance"
115
- output = `#{cmd}`
116
-
117
- if $?.success?
118
- if verbose
119
- puts "Ledger verification output: #{output}"
120
-
121
- # Show all accounts in the ledger file
122
- cmd = "ledger -f #{output_file} accounts"
123
- output = `#{cmd}`
124
-
125
- if $?.success?
126
- puts "All accounts in the ledger file:"
127
- puts output
128
- else
129
- puts "Warning: Could not list accounts in the ledger file."
130
- puts "This may indicate a problem with the file format, but the balance command succeeded."
131
- end
111
+ # Debug: Verify the ledger file is valid
112
+ cmd = "ledger -f #{output_file} balance"
113
+ output = `#{cmd}`
114
+
115
+ if $?.success?
116
+ if verbose
117
+ puts "Ledger verification output: #{output}"
118
+
119
+ # Show all accounts in the ledger file
120
+ cmd = "ledger -f #{output_file} accounts"
121
+ output = `#{cmd}`
122
+
123
+ if $?.success?
124
+ puts "All accounts in the ledger file:"
125
+ puts output
126
+ else
127
+ puts "Warning: Could not list accounts in the ledger file."
128
+ puts "This may indicate a problem with the file format, but the balance command succeeded."
132
129
  end
133
- else
134
- puts "Warning: Ledger verification failed. The file may contain formatting issues."
135
- puts "Error output: #{output}"
136
-
137
- # Try to identify problematic lines
138
- cmd = "grep -n '^[0-9]' #{output_file} | head -10"
139
- problematic_lines = `#{cmd}`
140
- puts "First few transaction lines for inspection:"
141
- puts problematic_lines
142
130
  end
143
- rescue => e
144
- puts "Error during ledger verification: #{e.message}"
145
- puts "This doesn't necessarily mean the file is invalid, but there may be issues with the ledger command."
131
+ else
132
+ puts "Warning: Ledger verification failed. The file may contain formatting issues."
133
+ puts "Error output: #{output}"
134
+
135
+ # Try to identify problematic lines
136
+ cmd = "grep -n '^[0-9]' #{output_file} | head -10"
137
+ problematic_lines = `#{cmd}`
138
+ puts "First few transaction lines for inspection:"
139
+ puts problematic_lines
146
140
  end
141
+ rescue StandardError => e
142
+ puts "Error during ledger verification: #{e.message}"
143
+ puts "This doesn't necessarily mean the file is invalid, but there may be issues with the ledger command."
147
144
  end
148
-
145
+
149
146
  # Export transactions to beancount format
150
147
  def export_to_beancount(transactions, output_file, reconciled_transactions = [], verbose = false)
151
148
  File.open(output_file, 'w') do |file|
152
149
  file.puts "; Mercury Bank Transactions Export"
153
150
  file.puts "; Generated on #{Time.now.strftime('%Y-%m-%d %H:%M:%S')}"
154
151
  file.puts
155
-
152
+
156
153
  # Filter out failed transactions
157
154
  valid_transactions = transactions.reject { |t| t["status"] == "failed" }
158
-
155
+
159
156
  # Get unique account names for debug output
160
157
  if verbose
161
- accounts = valid_transactions.map { |t| t["accountName"] || "Mercury" }.map { |name| name.gsub(/[^a-zA-Z0-9]/, '-') }.uniq
158
+ # First get all account names
159
+ account_names = valid_transactions.map do |t|
160
+ t["accountName"] || "Mercury"
161
+ end
162
+ # Then clean and make unique
163
+ accounts = account_names.map { |name| name.gsub(/[^a-zA-Z0-9]/, '-') }.uniq
162
164
  puts "Creating beancount file with the following accounts:"
163
165
  accounts.each do |account|
164
166
  puts "- Assets:#{account}"
165
167
  end
166
168
  end
167
-
169
+
168
170
  valid_transactions.each do |t|
169
171
  date = t["postedAt"] ? Time.parse(t["postedAt"]).strftime("%Y-%m-%d") : Time.parse(t["createdAt"]).strftime("%Y-%m-%d")
170
172
  description = t["bankDescription"] || t["externalMemo"] || "Unknown transaction"
@@ -172,18 +174,18 @@ module MercuryBanking
172
174
  account_name = t["accountName"] || "Mercury"
173
175
  account_name = account_name.gsub(/[^a-zA-Z0-9]/, '-')
174
176
  transaction_id = t["id"]
175
-
177
+
176
178
  # Check if this transaction is reconciled
177
179
  is_reconciled = reconciled_transactions.include?(transaction_id)
178
180
  flag = is_reconciled ? "*" : "!"
179
-
181
+
180
182
  # Sanitize description for beancount format
181
183
  description = description.gsub(/[\r\n]+/, ' ').gsub(/"/, '\\"')
182
-
184
+
183
185
  file.puts "#{date} #{flag} \"#{description}\""
184
186
  file.puts " ; Transaction ID: #{transaction_id}"
185
- file.puts " ; Status: #{t["status"]}"
186
-
187
+ file.puts " ; Status: #{t['status']}"
188
+
187
189
  # Add reconciliation metadata
188
190
  if is_reconciled
189
191
  file.puts " reconciled: \"true\""
@@ -192,29 +194,29 @@ module MercuryBanking
192
194
  file.puts " reconciled: \"false\""
193
195
  file.puts " reconciled_at: \"\""
194
196
  end
195
-
196
- if amount < 0
197
- file.puts " Expenses:Unknown #{format("%.2f", amount.abs)} USD"
198
- file.puts " Assets:#{account_name} #{format("%.2f", amount)} USD"
197
+
198
+ if amount.negative?
199
+ file.puts " Expenses:Unknown #{format('%.2f', amount.abs)} USD"
200
+ file.puts " Assets:#{account_name} #{format('%.2f', amount)} USD"
199
201
  else
200
- file.puts " Assets:#{account_name} #{format("%.2f", amount)} USD"
201
- file.puts " Income:Unknown #{format("%.2f", -amount)} USD"
202
+ file.puts " Assets:#{account_name} #{format('%.2f', amount)} USD"
203
+ file.puts " Income:Unknown #{format('%.2f', -amount)} USD"
202
204
  end
203
205
  file.puts
204
206
  end
205
207
  end
206
208
  end
207
-
209
+
208
210
  # Export transactions to hledger format
209
211
  def export_to_hledger(transactions, output_file, reconciled_transactions = [], verbose = false)
210
212
  File.open(output_file, 'w') do |file|
211
213
  file.puts "; Mercury Bank Transactions Export"
212
214
  file.puts "; Generated on #{Time.now.strftime('%Y-%m-%d %H:%M:%S')}"
213
215
  file.puts
214
-
216
+
215
217
  # Filter out failed transactions
216
218
  valid_transactions = transactions.reject { |t| t["status"] == "failed" }
217
-
219
+
218
220
  # Get unique account names for debug output
219
221
  if verbose
220
222
  accounts = valid_transactions.map { |t| t["accountName"] || "Mercury Account" }.uniq
@@ -223,25 +225,25 @@ module MercuryBanking
223
225
  puts "- Assets:#{account}"
224
226
  end
225
227
  end
226
-
228
+
227
229
  valid_transactions.each do |t|
228
230
  date = t["postedAt"] ? Time.parse(t["postedAt"]).strftime("%Y-%m-%d") : Time.parse(t["createdAt"]).strftime("%Y-%m-%d")
229
231
  description = t["bankDescription"] || t["externalMemo"] || "Unknown transaction"
230
232
  amount = t["amount"].to_f
231
233
  account_name = t["accountName"] || "Mercury Account"
232
234
  transaction_id = t["id"]
233
-
235
+
234
236
  # Check if this transaction is reconciled
235
237
  is_reconciled = reconciled_transactions.include?(transaction_id)
236
238
  status_marker = is_reconciled ? " * " : " ! "
237
-
239
+
238
240
  # Sanitize description for hledger format
239
241
  description = description.gsub(/[\r\n]+/, ' ').gsub(/;/, '\\;')
240
-
242
+
241
243
  file.puts "#{date}#{status_marker}#{description}"
242
244
  file.puts " ; Transaction ID: #{transaction_id}"
243
- file.puts " ; Status: #{t["status"]}"
244
-
245
+ file.puts " ; Status: #{t['status']}"
246
+
245
247
  # Add reconciliation metadata
246
248
  if is_reconciled
247
249
  file.puts " reconciled: true"
@@ -250,57 +252,57 @@ module MercuryBanking
250
252
  file.puts " reconciled: false"
251
253
  file.puts " reconciled-at:"
252
254
  end
253
-
254
- if amount < 0
255
- file.puts " Expenses:Unknown $#{format("%.2f", amount.abs)}"
256
- file.puts " Assets:#{account_name} $#{format("%.2f", amount)}"
255
+
256
+ if amount.negative?
257
+ file.puts " Expenses:Unknown $#{format('%.2f', amount.abs)}"
258
+ file.puts " Assets:#{account_name} $#{format('%.2f', amount)}"
257
259
  else
258
- file.puts " Assets:#{account_name} $#{format("%.2f", amount)}"
259
- file.puts " Income:Unknown $#{format("%.2f", -amount)}"
260
+ file.puts " Assets:#{account_name} $#{format('%.2f', amount)}"
261
+ file.puts " Income:Unknown $#{format('%.2f', -amount)}"
260
262
  end
261
263
  file.puts
262
264
  end
263
265
  end
264
266
  end
265
-
267
+
266
268
  # Export transactions to CSV format
267
269
  def export_to_csv(transactions, output_file, reconciled_transactions = [], verbose = false)
268
270
  require 'csv'
269
-
271
+
270
272
  CSV.open(output_file, 'w') do |csv|
271
273
  # Write header
272
274
  csv << ['Date', 'Description', 'Amount', 'Account', 'Transaction ID', 'Status', 'Reconciled', 'Reconciled At']
273
-
275
+
274
276
  # Filter out failed transactions
275
277
  valid_transactions = transactions.reject { |t| t["status"] == "failed" }
276
-
278
+
277
279
  if verbose
278
280
  puts "Creating CSV file with #{valid_transactions.size} transactions"
279
281
  puts "CSV columns: Date, Description, Amount, Account, Transaction ID, Status, Reconciled, Reconciled At"
280
282
  end
281
-
283
+
282
284
  # Write transactions
283
285
  valid_transactions.each do |t|
284
286
  date = t["postedAt"] ? Time.parse(t["postedAt"]).strftime("%Y-%m-%d") : Time.parse(t["createdAt"]).strftime("%Y-%m-%d")
285
287
  description = t["bankDescription"] || t["externalMemo"] || "Unknown transaction"
286
-
288
+
287
289
  # Replace newlines with spaces for CSV format
288
290
  description = description.gsub(/[\r\n]+/, ' ')
289
-
291
+
290
292
  amount = t["amount"].to_f
291
293
  account_name = t["accountName"] || "Mercury Account"
292
294
  transaction_id = t["id"]
293
295
  status = t["status"]
294
-
296
+
295
297
  # Check if this transaction is reconciled
296
298
  is_reconciled = reconciled_transactions.include?(transaction_id)
297
299
  reconciled = is_reconciled ? "Yes" : "No"
298
300
  reconciled_at = is_reconciled ? Time.now.strftime("%Y-%m-%d") : ""
299
-
301
+
300
302
  csv << [date, description, amount, account_name, transaction_id, status, reconciled, reconciled_at]
301
303
  end
302
304
  end
303
305
  end
304
306
  end
305
307
  end
306
- end
308
+ end