mercury_banking 0.5.37 → 0.6.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'
@@ -20,90 +22,91 @@ module MercuryBanking
20
22
  include MercuryBanking::Formatters::TableFormatter
21
23
  include MercuryBanking::Formatters::ExportFormatter
22
24
  include MercuryBanking::Reports::BalanceSheet
23
-
25
+
24
26
  # Add global option for JSON output
25
27
  class_option :json, type: :boolean, default: false, desc: 'Output in JSON format'
26
-
28
+
27
29
  map %w[--version -v] => :version
28
-
30
+
29
31
  # Add version banner to help output
30
32
  def self.banner(command, _namespace = nil, _subcommand = false)
31
33
  "#{basename} #{command.usage}"
32
34
  end
33
-
35
+
34
36
  def self.help(shell, _subcommand = false)
35
37
  shell.say "Mercury Banking CLI v#{MercuryBanking::VERSION} - Command-line interface for Mercury Banking API"
36
38
  shell.say
37
39
  super
38
40
  end
39
-
41
+
40
42
  # Handle Thor deprecation warning
41
43
  def self.exit_on_failure?
42
44
  true
43
45
  end
44
-
46
+
45
47
  desc 'version', 'Display Mercury Banking CLI version'
46
48
  def version
47
49
  if options[:json]
48
50
  puts JSON.pretty_generate({
49
- 'name' => 'mercury-banking',
50
- 'version' => MercuryBanking::VERSION,
51
- 'ruby_version' => RUBY_VERSION
52
- })
51
+ 'name' => 'mercury-banking',
52
+ 'version' => MercuryBanking::VERSION,
53
+ 'ruby_version' => RUBY_VERSION
54
+ })
53
55
  else
54
56
  puts "Mercury Banking CLI v#{MercuryBanking::VERSION} (Ruby #{RUBY_VERSION})"
55
57
  end
56
58
  end
57
-
59
+
58
60
  desc 'set_key', 'Sets and encrypts the API key'
59
61
  def set_key
60
62
  # Create the .mercury-banking directory if it doesn't exist
61
63
  config_dir = File.join(Dir.home, '.mercury-banking')
62
64
  FileUtils.mkdir_p(config_dir)
63
-
65
+
64
66
  # Generate a random key and IV for encryption
65
67
  key = SecureRandom.random_bytes(32)
66
68
  iv = SecureRandom.random_bytes(16)
67
-
69
+
68
70
  # Create a cipher config
69
71
  cipher_config = {
70
72
  key: Base64.strict_encode64(key),
71
73
  iv: Base64.strict_encode64(iv),
72
74
  cipher_name: 'aes-256-cbc'
73
75
  }
74
-
76
+
75
77
  # Save the cipher config to a file
76
78
  key_config_path = File.join(config_dir, 'key_config.json')
77
79
  File.write(key_config_path, JSON.pretty_generate(cipher_config))
78
-
80
+
79
81
  # Initialize the SymmetricEncryption with the generated cipher
80
82
  cipher = SymmetricEncryption::Cipher.new(
81
83
  key: key,
82
84
  iv: iv,
83
85
  cipher_name: 'aes-256-cbc'
84
86
  )
85
-
87
+
86
88
  # Set the cipher as the primary one
87
89
  SymmetricEncryption.cipher = cipher
88
-
90
+
89
91
  # Get the API key from the user
90
92
  api_key = ask('Enter your Mercury API key:')
91
-
93
+
92
94
  # Encrypt the API key
93
95
  encrypted_key = cipher.encrypt(api_key)
94
-
96
+
95
97
  # Save the encrypted API key to a file
96
98
  api_key_path = File.join(config_dir, 'api_key.enc')
97
99
  File.write(api_key_path, encrypted_key)
98
-
100
+
99
101
  puts "API key encrypted and saved to #{api_key_path}"
100
102
  end
101
-
103
+
102
104
  desc 'transactions ACCOUNT_ID_OR_NUMBER', 'List transactions for an account with their status'
103
105
  method_option :start, type: :string, default: '2020-01-01', desc: 'Start date for transactions (YYYY-MM-DD)'
104
106
  method_option :end, type: :string, desc: 'End date for transactions (YYYY-MM-DD)'
105
107
  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)'
108
+ method_option :format, type: :string, default: 'table', enum: %w[table json],
109
+ desc: 'Output format (table or json)'
107
110
  method_option :search, type: :string, desc: 'Search for transactions by description'
108
111
  def transactions(account_identifier)
109
112
  with_api_client do |client|
@@ -113,36 +116,34 @@ module MercuryBanking
113
116
  begin
114
117
  account = client.find_account_by_number(account_identifier)
115
118
  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)
119
+ rescue StandardError
120
+ # Handle error without assigning to unused variable
121
+ puts "Error: Could not find account with identifier #{account_identifier}"
122
+ return []
120
123
  end
121
124
  else
122
125
  account_id = account_identifier
123
126
  account = client.get_account(account_id)
124
127
  end
125
-
128
+
126
129
  # Get transactions for the account
127
130
  start_date = options[:start]
128
131
  end_date = options[:end]
129
-
132
+
130
133
  transactions = client.get_transactions(account_id, start_date)
131
-
134
+
132
135
  # Filter by end date if specified
133
136
  if end_date
134
137
  end_date_obj = Date.parse(end_date)
135
138
  transactions = transactions.select do |t|
136
- transaction_date = t["postedAt"] ? Date.parse(t["postedAt"]) : Date.parse(t["createdAt"])
139
+ transaction_date = Date.parse(t["postedAt"] || t["createdAt"])
137
140
  transaction_date <= end_date_obj
138
141
  end
139
142
  end
140
-
143
+
141
144
  # Filter by status if specified
142
- if options[:status]
143
- transactions = transactions.select { |t| t["status"] == options[:status] }
144
- end
145
-
145
+ transactions = transactions.select { |t| t["status"] == options[:status] } if options[:status]
146
+
146
147
  # Filter by search term if specified
147
148
  if options[:search]
148
149
  search_term = options[:search].downcase
@@ -151,7 +152,7 @@ module MercuryBanking
151
152
  description.downcase.include?(search_term)
152
153
  end
153
154
  end
154
-
155
+
155
156
  if options[:json] || options[:format] == 'json'
156
157
  # Format transactions for JSON output
157
158
  formatted_transactions = transactions.map do |t|
@@ -167,21 +168,21 @@ module MercuryBanking
167
168
  'reason_for_failure' => t["reasonForFailure"]
168
169
  }
169
170
  end
170
-
171
+
171
172
  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
- })
173
+ 'account_id' => account_id,
174
+ 'account_name' => account['name'],
175
+ 'account_number' => account['accountNumber'],
176
+ 'total_transactions' => transactions.size,
177
+ 'transactions' => formatted_transactions
178
+ })
178
179
  else
179
180
  # Display transactions in a table
180
181
  puts "Transactions for #{account['name']} (#{account['accountNumber']})"
181
182
  puts "Period: #{start_date} to #{end_date || 'present'}"
182
183
  puts "Total transactions: #{transactions.size}"
183
184
  puts
184
-
185
+
185
186
  rows = transactions.map do |t|
186
187
  date = t["postedAt"] ? Time.parse(t["postedAt"]).strftime("%Y-%m-%d") : Time.parse(t["createdAt"]).strftime("%Y-%m-%d")
187
188
  description = t["bankDescription"] || t["externalMemo"] || "Unknown transaction"
@@ -191,19 +192,20 @@ module MercuryBanking
191
192
  kind = t["kind"]
192
193
  reconciled = false
193
194
  failure_reason = t["reasonForFailure"] || ""
194
-
195
+
195
196
  [t["id"], date, description, amount, status, kind, reconciled, failure_reason]
196
197
  end
197
-
198
+
198
199
  table = ::Terminal::Table.new(
199
- headings: ['Transaction ID', 'Date', 'Description', 'Amount', 'Status', 'Type', 'Reconciled', 'Failure Reason'],
200
+ headings: ['Transaction ID', 'Date', 'Description', 'Amount', 'Status', 'Type', 'Reconciled',
201
+ 'Failure Reason'],
200
202
  rows: rows
201
203
  )
202
-
204
+
203
205
  puts table
204
206
  end
205
207
  end
206
208
  end
207
209
  end
208
210
  end
209
- end
211
+ 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