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
  module MercuryBanking
2
4
  module Reports
3
5
  # Module for generating balance sheet reports
@@ -9,57 +11,55 @@ module MercuryBanking
9
11
  puts "Error: 'ledger' command not found. Please install ledger (https://www.ledger-cli.org/)."
10
12
  return nil
11
13
  end
12
-
14
+
13
15
  # Verify the file exists
14
16
  unless File.exist?(file_path)
15
17
  puts "Error: Ledger file not found at #{file_path}"
16
18
  return nil
17
19
  end
18
-
20
+
19
21
  # Verify and potentially fix the ledger file
20
22
  verified_file_path = verify_ledger_file_syntax(file_path)
21
23
  return nil unless verified_file_path
22
-
24
+
23
25
  # Generate the balance sheet
24
26
  generate_ledger_balance_report(verified_file_path, end_date)
25
27
  end
26
-
28
+
27
29
  # Verify the syntax of a ledger file and fix common issues if needed
28
30
  def verify_ledger_file_syntax(file_path)
29
31
  check_cmd = "ledger -f #{file_path} --verify"
30
32
  check_output = `#{check_cmd}`
31
-
32
- if $?.success?
33
- return file_path
34
- else
35
- puts "Warning: Ledger file verification failed. The file may contain syntax errors."
36
- puts "Error output: #{check_output}"
37
-
38
- # Try to identify problematic lines
39
- problematic_lines = `grep -n '^[0-9]' #{file_path} | head -10`
40
- puts "First few transaction lines for inspection:"
41
- puts problematic_lines
42
-
43
- # Try to fix common issues
44
- puts "Attempting to fix common issues in the ledger file..."
45
- fix_ledger_file_syntax(file_path)
46
- end
47
- end
48
-
33
+
34
+ return file_path if $?.success?
35
+
36
+ puts "Warning: Ledger file verification failed. The file may contain syntax errors."
37
+ puts "Error output: #{check_output}"
38
+
39
+ # Try to identify problematic lines
40
+ problematic_lines = `grep -n '^[0-9]' #{file_path} | head -10`
41
+ puts "First few transaction lines for inspection:"
42
+ puts problematic_lines
43
+
44
+ # Try to fix common issues
45
+ puts "Attempting to fix common issues in the ledger file..."
46
+ fix_ledger_file_syntax(file_path)
47
+ end
48
+
49
49
  # Fix common syntax issues in a ledger file
50
50
  def fix_ledger_file_syntax(file_path)
51
51
  fixed_file_path = "#{file_path}.fixed"
52
-
52
+
53
53
  # Create a fixed version of the file with safer formatting
54
54
  File.open(fixed_file_path, 'w') do |fixed_file|
55
55
  File.foreach(file_path) do |line|
56
- if line.match?(/^\d{4}[\/\-\.]\d{2}[\/\-\.]\d{2}/)
56
+ if line.match?(%r{^\d{4}[/\-.]\d{2}[/\-.]\d{2}})
57
57
  # This is a transaction line, ensure description is safe
58
58
  date, description = line.strip.split(' ', 2)
59
- if description && (description.match?(/^\d/) ||
60
- description.match?(/^[v#]/) ||
61
- description.match?(/^\s*\d{4}/) ||
62
- description.match?(/^\s*\d{2}\/\d{2}/))
59
+ if description && (description.match?(/^\d/) ||
60
+ description.match?(/^[v#]/) ||
61
+ description.match?(/^\s*\d{4}/) ||
62
+ description.match?(%r{^\s*\d{2}/\d{2}}))
63
63
  fixed_file.puts "#{date} - #{description}"
64
64
  else
65
65
  fixed_file.puts line
@@ -69,42 +69,42 @@ module MercuryBanking
69
69
  end
70
70
  end
71
71
  end
72
-
72
+
73
73
  puts "Created fixed ledger file at #{fixed_file_path}"
74
74
  fixed_file_path
75
75
  end
76
-
76
+
77
77
  # Generate a balance sheet report using ledger
78
78
  def generate_ledger_balance_report(file_path, end_date = nil)
79
79
  # Construct the ledger command
80
80
  cmd = "ledger -f #{file_path}"
81
81
  cmd += " --end #{end_date}" if end_date
82
82
  cmd += " balance --flat Assets Liabilities Equity"
83
-
83
+
84
84
  # Execute the command
85
85
  output = `#{cmd}`
86
-
86
+
87
87
  # Check if the command was successful
88
88
  if $?.success?
89
89
  output
90
90
  else
91
91
  puts "Error executing ledger command: #{cmd}"
92
92
  puts "Error output: #{output}"
93
-
93
+
94
94
  # Try with a more permissive approach
95
95
  try_permissive_ledger_command(file_path, end_date)
96
96
  end
97
97
  end
98
-
98
+
99
99
  # Try a more permissive ledger command as a fallback
100
100
  def try_permissive_ledger_command(file_path, end_date = nil)
101
101
  puts "Trying with more permissive options..."
102
102
  cmd = "ledger -f #{file_path} --permissive"
103
103
  cmd += " --end #{end_date}" if end_date
104
104
  cmd += " balance --flat Assets Liabilities Equity"
105
-
105
+
106
106
  output = `#{cmd}`
107
-
107
+
108
108
  if $?.success?
109
109
  output
110
110
  else
@@ -113,7 +113,7 @@ module MercuryBanking
113
113
  nil
114
114
  end
115
115
  end
116
-
116
+
117
117
  # Generate a beancount balance sheet
118
118
  def generate_beancount_balance_sheet(file_path, end_date = nil)
119
119
  # Check if bean-report command exists
@@ -121,14 +121,14 @@ module MercuryBanking
121
121
  puts "Error: 'bean-report' command not found. Please install beancount (https://beancount.github.io/)."
122
122
  return nil
123
123
  end
124
-
124
+
125
125
  # Construct the beancount command
126
126
  cmd = "bean-report #{file_path} balances"
127
127
  cmd += " --end #{end_date}" if end_date
128
-
128
+
129
129
  # Execute the command
130
130
  output = `#{cmd}`
131
-
131
+
132
132
  # Check if the command was successful
133
133
  if $?.success?
134
134
  output
@@ -138,17 +138,17 @@ module MercuryBanking
138
138
  nil
139
139
  end
140
140
  end
141
-
141
+
142
142
  # Parse balance sheet output to extract account balances
143
143
  def parse_balance_sheet_output(output, format, debug = false)
144
144
  balances = {}
145
-
145
+
146
146
  if debug
147
147
  puts "\n=== Balance Sheet Parsing Debug ==="
148
148
  puts "Raw output to parse:"
149
149
  puts output
150
150
  end
151
-
151
+
152
152
  case format
153
153
  when 'ledger'
154
154
  balances = parse_ledger_balance_output(output, debug)
@@ -157,63 +157,63 @@ module MercuryBanking
157
157
  else
158
158
  puts "Unsupported format: #{format}"
159
159
  end
160
-
160
+
161
161
  balances
162
162
  end
163
-
163
+
164
164
  def parse_ledger_balance_output(output, debug = false)
165
165
  balances = {}
166
-
166
+
167
167
  output.each_line do |line|
168
168
  puts "Parsing line: #{line.inspect}" if debug
169
169
  line = line.strip
170
-
170
+
171
171
  # Skip empty lines and summary lines
172
172
  if line.empty? || line.include?('----')
173
173
  puts " Skipping line (empty or summary)" if debug
174
174
  next
175
175
  end
176
-
176
+
177
177
  # Extract account name and balance using a more flexible regex
178
178
  # The ledger output format is typically something like:
179
179
  # $18.10 Assets:Mercury Checking ••1090
180
- if match = line.match(/\$\s*([\d,.-]+)\s+(.+)/)
180
+ if (match = line.match(/\$\s*([\d,.-]+)\s+(.+)/))
181
181
  balance = match[1].gsub(',', '').to_f
182
182
  account = match[2].strip
183
-
183
+
184
184
  puts " Extracted: Account='#{account}', Balance=$#{balance}" if debug
185
-
185
+
186
186
  # Add to balances hash
187
187
  balances[account] = balance
188
188
  puts " Added to balances hash" if debug
189
- else
190
- puts " No match for balance pattern" if debug
189
+ elsif debug
190
+ puts " No match for balance pattern"
191
191
  end
192
192
  end
193
-
193
+
194
194
  balances
195
195
  end
196
-
197
- def parse_beancount_balance_output(output, debug = false)
196
+
197
+ def parse_beancount_balance_output(output, _debug = false)
198
198
  balances = {}
199
-
199
+
200
200
  output.each_line do |line|
201
201
  # Skip header lines and summary lines
202
202
  next if line.strip.empty? || line.include?('---') || line.include?('Assets:') || line.include?('Liabilities:')
203
-
203
+
204
204
  # Extract account name and balance
205
- if line =~ /(\S.*?)\s+([\d,.-]+)\s+USD/
206
- account = $1.strip
207
- balance = $2.gsub(',', '').to_f
208
-
209
- # Add to balances hash
210
- balances[account] = balance
211
- end
205
+ next unless line =~ /(\S.*?)\s+([\d,.-]+)\s+USD/
206
+
207
+ account = ::Regexp.last_match(1).strip
208
+ balance = ::Regexp.last_match(2).gsub(',', '').to_f
209
+
210
+ # Add to balances hash
211
+ balances[account] = balance
212
212
  end
213
-
213
+
214
214
  balances
215
215
  end
216
-
216
+
217
217
  # Generate ledger reports (P&L, balance sheet, etc.)
218
218
  def generate_ledger_reports(file_path, report_type, category = nil, end_date = nil)
219
219
  # Check if ledger is installed
@@ -221,128 +221,120 @@ module MercuryBanking
221
221
  puts "Error: 'ledger' command not found. Please install Ledger CLI to generate reports."
222
222
  return
223
223
  end
224
-
224
+
225
225
  # Build date filter
226
226
  date_filter = end_date ? " --end #{end_date}" : ""
227
-
227
+
228
228
  # Build category filter
229
229
  category_filter = category ? " #{category}" : ""
230
-
230
+
231
231
  # Prepare to capture output
232
232
  report_output = ""
233
233
  report_output << "=== Mercury Banking Ledger Reports ===\n\n"
234
-
234
+
235
235
  puts "\n=== Mercury Banking Ledger Reports ===\n"
236
-
236
+
237
237
  # Generate requested reports
238
- if ['all', 'balance'].include?(report_type)
239
- report_output << generate_ledger_balance_report_section(file_path, date_filter, category_filter)
240
- end
241
-
242
- if ['all', 'pl'].include?(report_type)
243
- report_output << generate_ledger_pl_report_section(file_path, date_filter, category_filter)
244
- end
245
-
246
- if ['all', 'monthly'].include?(report_type)
247
- report_output << generate_ledger_monthly_report_section(file_path, date_filter, category)
248
- end
249
-
250
- if report_type == 'register'
251
- report_output << generate_ledger_register_report_section(file_path, date_filter, category)
252
- end
253
-
254
- return report_output
255
- end
256
-
238
+ report_output << generate_ledger_balance_report_section(file_path, date_filter, category_filter) if %w[all balance].include?(report_type)
239
+
240
+ report_output << generate_ledger_pl_report_section(file_path, date_filter, category_filter) if %w[all pl].include?(report_type)
241
+
242
+ report_output << generate_ledger_monthly_report_section(file_path, date_filter, category) if %w[all monthly].include?(report_type)
243
+
244
+ report_output << generate_ledger_register_report_section(file_path, date_filter, category) if report_type == 'register'
245
+
246
+ report_output
247
+ end
248
+
257
249
  def generate_ledger_balance_report_section(file_path, date_filter, category_filter)
258
250
  section_output = ""
259
-
251
+
260
252
  balance_cmd = "ledger -f #{file_path}#{date_filter} balance --flat Assets Liabilities Equity#{category_filter}"
261
253
  balance_output = `#{balance_cmd}`
262
-
254
+
263
255
  puts "\n=== Balance Sheet ===\n"
264
256
  puts balance_output
265
-
257
+
266
258
  section_output << "=== Balance Sheet ===\n"
267
259
  section_output << balance_output
268
260
  section_output << "\n"
269
-
261
+
270
262
  section_output
271
263
  end
272
-
264
+
273
265
  def generate_ledger_pl_report_section(file_path, date_filter, category_filter)
274
266
  section_output = ""
275
-
267
+
276
268
  pl_cmd = "ledger -f #{file_path}#{date_filter} balance --flat Income Expenses#{category_filter}"
277
269
  pl_output = `#{pl_cmd}`
278
-
270
+
279
271
  puts "\n=== Income Statement ===\n"
280
272
  puts pl_output
281
-
273
+
282
274
  section_output << "=== Income Statement ===\n"
283
275
  section_output << pl_output
284
276
  section_output << "\n"
285
-
277
+
286
278
  section_output
287
279
  end
288
-
280
+
289
281
  def generate_ledger_monthly_report_section(file_path, date_filter, category)
290
282
  section_output = ""
291
-
283
+
292
284
  if category
293
285
  monthly_cmd = "ledger -f #{file_path}#{date_filter} --monthly --collapse register #{category}"
294
286
  monthly_output = `#{monthly_cmd}`
295
-
287
+
296
288
  puts "\n=== Monthly #{category} ===\n"
297
289
  puts monthly_output
298
-
290
+
299
291
  section_output << "=== Monthly #{category} ===\n"
300
292
  section_output << monthly_output
301
293
  section_output << "\n"
302
294
  else
303
295
  # Generate monthly expenses report
304
296
  section_output << generate_ledger_monthly_category_report(file_path, date_filter, "Expenses")
305
-
297
+
306
298
  # Generate monthly income report
307
299
  section_output << generate_ledger_monthly_category_report(file_path, date_filter, "Income")
308
300
  end
309
-
301
+
310
302
  section_output
311
303
  end
312
-
304
+
313
305
  def generate_ledger_monthly_category_report(file_path, date_filter, category)
314
306
  section_output = ""
315
-
307
+
316
308
  monthly_cmd = "ledger -f #{file_path}#{date_filter} --monthly --collapse register #{category}"
317
309
  monthly_output = `#{monthly_cmd}`
318
-
310
+
319
311
  puts "\n=== Monthly #{category} ===\n"
320
312
  puts monthly_output
321
-
313
+
322
314
  section_output << "=== Monthly #{category} ===\n"
323
315
  section_output << monthly_output
324
316
  section_output << "\n"
325
-
317
+
326
318
  section_output
327
319
  end
328
-
320
+
329
321
  def generate_ledger_register_report_section(file_path, date_filter, category)
330
322
  section_output = ""
331
-
323
+
332
324
  register_filter = category || "Assets Liabilities Equity Income Expenses"
333
325
  register_cmd = "ledger -f #{file_path}#{date_filter} register #{register_filter}"
334
326
  register_output = `#{register_cmd}`
335
-
327
+
336
328
  puts "\n=== Transaction Register ===\n"
337
329
  puts register_output
338
-
330
+
339
331
  section_output << "=== Transaction Register ===\n"
340
332
  section_output << register_output
341
333
  section_output << "\n"
342
-
334
+
343
335
  section_output
344
336
  end
345
-
337
+
346
338
  # Generate beancount reports
347
339
  def generate_beancount_reports(file_path, report_type, category = nil, end_date = nil)
348
340
  # Check if bean-report is installed
@@ -350,101 +342,93 @@ module MercuryBanking
350
342
  puts "Error: 'bean-report' command not found. Please install Beancount to generate reports."
351
343
  return
352
344
  end
353
-
345
+
354
346
  # Prepare to capture output
355
347
  report_output = ""
356
348
  report_output << "=== Mercury Banking Beancount Reports ===\n\n"
357
-
349
+
358
350
  puts "\n=== Mercury Banking Beancount Reports ===\n"
359
-
351
+
360
352
  # Build date filter (beancount uses different syntax)
361
353
  date_filter = end_date ? " -e #{end_date}" : ""
362
-
354
+
363
355
  # Generate requested reports
364
- if ['all', 'balance'].include?(report_type)
365
- report_output << generate_beancount_balance_report_section(file_path, date_filter)
366
- end
367
-
368
- if ['all', 'pl'].include?(report_type)
369
- report_output << generate_beancount_pl_report_section(file_path, date_filter)
370
- end
371
-
372
- if ['all', 'monthly'].include?(report_type)
373
- report_output << generate_beancount_monthly_report_section(file_path, date_filter)
374
- end
375
-
376
- if report_type == 'register'
377
- report_output << generate_beancount_register_report_section(file_path, date_filter, category)
378
- end
379
-
380
- return report_output
381
- end
382
-
356
+ report_output << generate_beancount_balance_report_section(file_path, date_filter) if %w[all balance].include?(report_type)
357
+
358
+ report_output << generate_beancount_pl_report_section(file_path, date_filter) if %w[all pl].include?(report_type)
359
+
360
+ report_output << generate_beancount_monthly_report_section(file_path, date_filter) if %w[all monthly].include?(report_type)
361
+
362
+ report_output << generate_beancount_register_report_section(file_path, date_filter, category) if report_type == 'register'
363
+
364
+ report_output
365
+ end
366
+
383
367
  def generate_beancount_balance_report_section(file_path, date_filter)
384
368
  section_output = ""
385
-
369
+
386
370
  balance_cmd = "bean-report #{file_path}#{date_filter} balsheet"
387
371
  balance_output = `#{balance_cmd}`
388
-
372
+
389
373
  puts "\n=== Balance Sheet ===\n"
390
374
  puts balance_output
391
-
375
+
392
376
  section_output << "=== Balance Sheet ===\n"
393
377
  section_output << balance_output
394
378
  section_output << "\n"
395
-
379
+
396
380
  section_output
397
381
  end
398
-
382
+
399
383
  def generate_beancount_pl_report_section(file_path, date_filter)
400
384
  section_output = ""
401
-
385
+
402
386
  pl_cmd = "bean-report #{file_path}#{date_filter} income"
403
387
  pl_output = `#{pl_cmd}`
404
-
388
+
405
389
  puts "\n=== Income Statement ===\n"
406
390
  puts pl_output
407
-
391
+
408
392
  section_output << "=== Income Statement ===\n"
409
393
  section_output << pl_output
410
394
  section_output << "\n"
411
-
395
+
412
396
  section_output
413
397
  end
414
-
398
+
415
399
  def generate_beancount_monthly_report_section(file_path, date_filter)
416
400
  section_output = ""
417
-
401
+
418
402
  monthly_cmd = "bean-report #{file_path}#{date_filter} monthly"
419
403
  monthly_output = `#{monthly_cmd}`
420
-
404
+
421
405
  puts "\n=== Monthly Activity ===\n"
422
406
  puts monthly_output
423
-
407
+
424
408
  section_output << "=== Monthly Activity ===\n"
425
409
  section_output << monthly_output
426
410
  section_output << "\n"
427
-
411
+
428
412
  section_output
429
413
  end
430
-
414
+
431
415
  def generate_beancount_register_report_section(file_path, date_filter, category)
432
416
  section_output = ""
433
-
417
+
434
418
  register_filter = category ? " --account #{category}" : ""
435
419
  register_cmd = "bean-report #{file_path}#{date_filter}#{register_filter} register"
436
420
  register_output = `#{register_cmd}`
437
-
421
+
438
422
  puts "\n=== Transaction Register ===\n"
439
423
  puts register_output
440
-
424
+
441
425
  section_output << "=== Transaction Register ===\n"
442
426
  section_output << register_output
443
427
  section_output << "\n"
444
-
428
+
445
429
  section_output
446
430
  end
447
-
431
+
448
432
  def export_transactions_to_ledger(transactions, file_path)
449
433
  # Create a temporary file for ledger export
450
434
  File.open(file_path, 'w') do |f|
@@ -453,86 +437,90 @@ module MercuryBanking
453
437
  write_ledger_transactions(f, transactions)
454
438
  end
455
439
  end
456
-
440
+
457
441
  def write_ledger_header(file)
458
442
  file.puts "; Mercury Bank Transactions Export"
459
443
  file.puts "; Generated on #{Time.now.strftime('%Y-%m-%d %H:%M:%S')}"
460
444
  file.puts
461
445
  end
462
-
446
+
463
447
  def write_ledger_account_declarations(file, transactions)
464
- # Get unique accounts from transactions
465
- accounts = transactions.map do |t|
448
+ # First extract account info from transactions
449
+ account_info = transactions.map do |t|
466
450
  {
467
451
  "name" => t["accountName"] || "Unknown Account",
468
452
  "number" => t["accountNumber"]
469
453
  }
470
- end.uniq { |a| [a["name"], a["number"]] }
471
-
454
+ end
455
+ # Then make unique based on name and number
456
+ accounts = account_info.uniq { |a| [a["name"], a["number"]] }
457
+
472
458
  # Define accounts dynamically based on transaction data
473
459
  accounts.each do |account|
474
460
  account_name = account["name"]
475
461
  account_number = account["number"]
476
-
462
+
477
463
  # Format the account name to include the account number if available
478
464
  formatted_account_name = if account_number
479
- "#{account_name} #{account_number.to_s[-4..-1]}"
465
+ "#{account_name} #{account_number.to_s[-4..]}"
480
466
  else
481
467
  account_name
482
468
  end
483
-
469
+
484
470
  file.puts "account Assets:#{formatted_account_name}"
485
471
  end
486
472
  file.puts "account Expenses:Unknown"
487
473
  file.puts "account Income:Unknown"
488
474
  file.puts
489
475
  end
490
-
476
+
491
477
  def write_ledger_transactions(file, transactions)
492
478
  # Filter out failed transactions
493
479
  valid_transactions = transactions.reject { |t| t["status"] == "failed" }
494
-
480
+
495
481
  # Sort transactions by date
496
482
  valid_transactions.sort_by! { |t| t["postedAt"] || t["createdAt"] }.each do |t|
497
483
  write_ledger_transaction(file, t)
498
484
  end
499
485
  end
500
-
486
+
501
487
  def write_ledger_transaction(file, transaction)
502
- date = transaction["postedAt"] ?
503
- Time.parse(transaction["postedAt"]).strftime("%Y/%m/%d") :
504
- Time.parse(transaction["createdAt"]).strftime("%Y/%m/%d")
488
+ date = if transaction["postedAt"]
489
+ Time.parse(transaction["postedAt"]).strftime("%Y/%m/%d")
490
+ else
491
+ Time.parse(transaction["createdAt"]).strftime("%Y/%m/%d")
492
+ end
505
493
  description = transaction["bankDescription"] || transaction["externalMemo"] || "Unknown transaction"
506
494
  amount = transaction["amount"]
507
495
  account_name = transaction["accountName"] || "Unknown Account"
508
496
  account_number = transaction["accountNumber"]
509
-
497
+
510
498
  file.puts "#{date} #{description}"
511
- file.puts " ; Transaction ID: #{transaction["id"]}"
512
- file.puts " ; Status: #{transaction["status"]}"
499
+ file.puts " ; Transaction ID: #{transaction['id']}"
500
+ file.puts " ; Status: #{transaction['status']}"
513
501
  file.puts " ; Reconciled: No"
514
-
502
+
515
503
  write_ledger_postings(file, amount, account_name, account_number)
516
504
  file.puts
517
505
  end
518
-
506
+
519
507
  def write_ledger_postings(file, amount, account_name, account_number = nil)
520
508
  # Format the account name to include the account number if available
521
509
  formatted_account_name = if account_number
522
- "#{account_name} #{account_number.to_s[-4..-1]}"
510
+ "#{account_name} #{account_number.to_s[-4..]}"
523
511
  else
524
512
  account_name
525
513
  end
526
-
527
- if amount > 0
528
- file.puts " Income:Unknown $-#{format("%.2f", amount)}"
529
- file.puts " Assets:#{formatted_account_name} $#{format("%.2f", amount)}"
514
+
515
+ if amount.positive?
516
+ file.puts " Income:Unknown $-#{format('%.2f', amount)}"
517
+ file.puts " Assets:#{formatted_account_name} $#{format('%.2f', amount)}"
530
518
  else
531
- file.puts " Expenses:Unknown $#{format("%.2f", amount.abs)}"
532
- file.puts " Assets:#{formatted_account_name} $-#{format("%.2f", amount.abs)}"
519
+ file.puts " Expenses:Unknown $#{format('%.2f', amount.abs)}"
520
+ file.puts " Assets:#{formatted_account_name} $-#{format('%.2f', amount.abs)}"
533
521
  end
534
522
  end
535
-
523
+
536
524
  def export_transactions_to_beancount(transactions, file_path)
537
525
  # Create a temporary file for beancount export
538
526
  File.open(file_path, 'w') do |f|
@@ -541,7 +529,7 @@ module MercuryBanking
541
529
  write_beancount_transactions(f, transactions)
542
530
  end
543
531
  end
544
-
532
+
545
533
  def write_beancount_header(file)
546
534
  file.puts "; Mercury Bank Transactions Export"
547
535
  file.puts "; Generated on #{Time.now.strftime('%Y-%m-%d %H:%M:%S')}"
@@ -550,85 +538,89 @@ module MercuryBanking
550
538
  file.puts "option \"operating_currency\" \"USD\""
551
539
  file.puts
552
540
  end
553
-
541
+
554
542
  def write_beancount_account_declarations(file, transactions)
555
- # Get unique accounts from transactions
556
- accounts = transactions.map do |t|
543
+ # First extract account info from transactions
544
+ account_info = transactions.map do |t|
557
545
  {
558
546
  "name" => t["accountName"] || "Unknown Account",
559
547
  "number" => t["accountNumber"]
560
548
  }
561
- end.uniq { |a| [a["name"], a["number"]] }
562
-
549
+ end
550
+ # Then make unique based on name and number
551
+ accounts = account_info.uniq { |a| [a["name"], a["number"]] }
552
+
563
553
  # Define accounts dynamically based on transaction data
564
554
  accounts.each do |account|
565
555
  account_name = account["name"]
566
556
  account_number = account["number"]
567
-
557
+
568
558
  # Sanitize account name for beancount format
569
559
  safe_account_name = account_name.gsub(/[^a-zA-Z0-9]/, '-')
570
-
560
+
571
561
  # Format the account name to include the account number if available
572
562
  formatted_account_name = if account_number
573
- "#{safe_account_name}-#{account_number.to_s[-4..-1]}"
563
+ "#{safe_account_name}-#{account_number.to_s[-4..]}"
574
564
  else
575
565
  safe_account_name
576
566
  end
577
-
567
+
578
568
  file.puts "1970-01-01 open Assets:#{formatted_account_name}"
579
569
  end
580
570
  file.puts "1970-01-01 open Expenses:Unknown"
581
571
  file.puts "1970-01-01 open Income:Unknown"
582
572
  file.puts
583
573
  end
584
-
574
+
585
575
  def write_beancount_transactions(file, transactions)
586
576
  # Filter out failed transactions
587
577
  valid_transactions = transactions.reject { |t| t["status"] == "failed" }
588
-
578
+
589
579
  # Sort transactions by date
590
580
  valid_transactions.sort_by! { |t| t["postedAt"] || t["createdAt"] }.each do |t|
591
581
  write_beancount_transaction(file, t)
592
582
  end
593
583
  end
594
-
584
+
595
585
  def write_beancount_transaction(file, transaction)
596
- date = transaction["postedAt"] ?
597
- Time.parse(transaction["postedAt"]).strftime("%Y-%m-%d") :
598
- Time.parse(transaction["createdAt"]).strftime("%Y-%m-%d")
586
+ date = if transaction["postedAt"]
587
+ Time.parse(transaction["postedAt"]).strftime("%Y-%m-%d")
588
+ else
589
+ Time.parse(transaction["createdAt"]).strftime("%Y-%m-%d")
590
+ end
599
591
  description = transaction["bankDescription"] || transaction["externalMemo"] || "Unknown transaction"
600
592
  amount = transaction["amount"]
601
593
  account_name = transaction["accountName"] || "Unknown Account"
602
594
  account_number = transaction["accountNumber"]
603
-
595
+
604
596
  # Sanitize account name for beancount format
605
597
  safe_account_name = account_name.gsub(/[^a-zA-Z0-9]/, '-')
606
-
598
+
607
599
  file.puts "#{date} * \"#{description}\""
608
- file.puts " ; Transaction ID: #{transaction["id"]}"
609
- file.puts " ; Status: #{transaction["status"]}"
600
+ file.puts " ; Transaction ID: #{transaction['id']}"
601
+ file.puts " ; Status: #{transaction['status']}"
610
602
  file.puts " ; Reconciled: No"
611
-
603
+
612
604
  write_beancount_postings(file, amount, safe_account_name, account_number)
613
605
  file.puts
614
606
  end
615
-
607
+
616
608
  def write_beancount_postings(file, amount, account_name, account_number = nil)
617
609
  # Format the account name to include the account number if available
618
610
  formatted_account_name = if account_number
619
- "#{account_name}-#{account_number.to_s[-4..-1]}"
611
+ "#{account_name}-#{account_number.to_s[-4..]}"
620
612
  else
621
613
  account_name
622
614
  end
623
-
624
- if amount > 0
625
- file.puts " Income:Unknown -#{format("%.2f", amount)} USD"
626
- file.puts " Assets:#{formatted_account_name} #{format("%.2f", amount)} USD"
615
+
616
+ if amount.positive?
617
+ file.puts " Income:Unknown -#{format('%.2f', amount)} USD"
618
+ file.puts " Assets:#{formatted_account_name} #{format('%.2f', amount)} USD"
627
619
  else
628
- file.puts " Expenses:Unknown #{format("%.2f", amount.abs)} USD"
629
- file.puts " Assets:#{formatted_account_name} -#{format("%.2f", amount.abs)} USD"
620
+ file.puts " Expenses:Unknown #{format('%.2f', amount.abs)} USD"
621
+ file.puts " Assets:#{formatted_account_name} -#{format('%.2f', amount.abs)} USD"
630
622
  end
631
623
  end
632
624
  end
633
625
  end
634
- end
626
+ end