mercury_banking 0.5.34
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.env_test +1 -0
- data/.gitignore +24 -0
- data/.rspec +3 -0
- data/.rubocop.yml +17 -0
- data/.travis.yml +6 -0
- data/CHANGELOG.md +90 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/Gemfile +26 -0
- data/Gemfile.lock +140 -0
- data/LICENSE +21 -0
- data/LINTING_REPORT.md +118 -0
- data/README.md +244 -0
- data/Rakefile +6 -0
- data/bin/console +14 -0
- data/bin/mercury +17 -0
- data/bin/setup +8 -0
- data/lib/mercury_banking/api.rb +184 -0
- data/lib/mercury_banking/cli/accounts.rb +61 -0
- data/lib/mercury_banking/cli/base.rb +68 -0
- data/lib/mercury_banking/cli/financials.rb +302 -0
- data/lib/mercury_banking/cli/reconciliation.rb +406 -0
- data/lib/mercury_banking/cli/reports.rb +265 -0
- data/lib/mercury_banking/cli/transactions.rb +222 -0
- data/lib/mercury_banking/cli.rb +209 -0
- data/lib/mercury_banking/formatters/export_formatter.rb +306 -0
- data/lib/mercury_banking/formatters/table_formatter.rb +133 -0
- data/lib/mercury_banking/multi.rb +135 -0
- data/lib/mercury_banking/recipient.rb +29 -0
- data/lib/mercury_banking/reconciliation.rb +139 -0
- data/lib/mercury_banking/reports/balance_sheet.rb +586 -0
- data/lib/mercury_banking/reports/reconciliation.rb +307 -0
- data/lib/mercury_banking/utils/command_utils.rb +18 -0
- data/lib/mercury_banking/version.rb +3 -0
- data/lib/mercury_banking.rb +19 -0
- data/mercury_banking.gemspec +37 -0
- metadata +183 -0
@@ -0,0 +1,586 @@
|
|
1
|
+
module MercuryBanking
|
2
|
+
module Reports
|
3
|
+
# Module for generating balance sheet reports
|
4
|
+
module BalanceSheet
|
5
|
+
# Generate a ledger balance sheet
|
6
|
+
def generate_ledger_balance_sheet(file_path, end_date = nil)
|
7
|
+
# Check if ledger command exists
|
8
|
+
unless command_exists?('ledger')
|
9
|
+
puts "Error: 'ledger' command not found. Please install ledger (https://www.ledger-cli.org/)."
|
10
|
+
return nil
|
11
|
+
end
|
12
|
+
|
13
|
+
# Verify the file exists
|
14
|
+
unless File.exist?(file_path)
|
15
|
+
puts "Error: Ledger file not found at #{file_path}"
|
16
|
+
return nil
|
17
|
+
end
|
18
|
+
|
19
|
+
# Verify and potentially fix the ledger file
|
20
|
+
verified_file_path = verify_ledger_file_syntax(file_path)
|
21
|
+
return nil unless verified_file_path
|
22
|
+
|
23
|
+
# Generate the balance sheet
|
24
|
+
generate_ledger_balance_report(verified_file_path, end_date)
|
25
|
+
end
|
26
|
+
|
27
|
+
# Verify the syntax of a ledger file and fix common issues if needed
|
28
|
+
def verify_ledger_file_syntax(file_path)
|
29
|
+
check_cmd = "ledger -f #{file_path} --verify"
|
30
|
+
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
|
+
|
49
|
+
# Fix common syntax issues in a ledger file
|
50
|
+
def fix_ledger_file_syntax(file_path)
|
51
|
+
fixed_file_path = "#{file_path}.fixed"
|
52
|
+
|
53
|
+
# Create a fixed version of the file with safer formatting
|
54
|
+
File.open(fixed_file_path, 'w') do |fixed_file|
|
55
|
+
File.foreach(file_path) do |line|
|
56
|
+
if line.match?(/^\d{4}[\/\-\.]\d{2}[\/\-\.]\d{2}/)
|
57
|
+
# This is a transaction line, ensure description is safe
|
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}/))
|
63
|
+
fixed_file.puts "#{date} - #{description}"
|
64
|
+
else
|
65
|
+
fixed_file.puts line
|
66
|
+
end
|
67
|
+
else
|
68
|
+
fixed_file.puts line
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
puts "Created fixed ledger file at #{fixed_file_path}"
|
74
|
+
fixed_file_path
|
75
|
+
end
|
76
|
+
|
77
|
+
# Generate a balance sheet report using ledger
|
78
|
+
def generate_ledger_balance_report(file_path, end_date = nil)
|
79
|
+
# Construct the ledger command
|
80
|
+
cmd = "ledger -f #{file_path}"
|
81
|
+
cmd += " --end #{end_date}" if end_date
|
82
|
+
cmd += " balance --flat Assets Liabilities Equity"
|
83
|
+
|
84
|
+
# Execute the command
|
85
|
+
output = `#{cmd}`
|
86
|
+
|
87
|
+
# Check if the command was successful
|
88
|
+
if $?.success?
|
89
|
+
output
|
90
|
+
else
|
91
|
+
puts "Error executing ledger command: #{cmd}"
|
92
|
+
puts "Error output: #{output}"
|
93
|
+
|
94
|
+
# Try with a more permissive approach
|
95
|
+
try_permissive_ledger_command(file_path, end_date)
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
# Try a more permissive ledger command as a fallback
|
100
|
+
def try_permissive_ledger_command(file_path, end_date = nil)
|
101
|
+
puts "Trying with more permissive options..."
|
102
|
+
cmd = "ledger -f #{file_path} --permissive"
|
103
|
+
cmd += " --end #{end_date}" if end_date
|
104
|
+
cmd += " balance --flat Assets Liabilities Equity"
|
105
|
+
|
106
|
+
output = `#{cmd}`
|
107
|
+
|
108
|
+
if $?.success?
|
109
|
+
output
|
110
|
+
else
|
111
|
+
puts "Error executing permissive ledger command."
|
112
|
+
puts "Error output: #{output}"
|
113
|
+
nil
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
# Generate a beancount balance sheet
|
118
|
+
def generate_beancount_balance_sheet(file_path, end_date = nil)
|
119
|
+
# Check if bean-report command exists
|
120
|
+
unless command_exists?('bean-report')
|
121
|
+
puts "Error: 'bean-report' command not found. Please install beancount (https://beancount.github.io/)."
|
122
|
+
return nil
|
123
|
+
end
|
124
|
+
|
125
|
+
# Construct the beancount command
|
126
|
+
cmd = "bean-report #{file_path} balances"
|
127
|
+
cmd += " --end #{end_date}" if end_date
|
128
|
+
|
129
|
+
# Execute the command
|
130
|
+
output = `#{cmd}`
|
131
|
+
|
132
|
+
# Check if the command was successful
|
133
|
+
if $?.success?
|
134
|
+
output
|
135
|
+
else
|
136
|
+
puts "Error executing bean-report command: #{cmd}"
|
137
|
+
puts output
|
138
|
+
nil
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
# Parse balance sheet output to extract account balances
|
143
|
+
def parse_balance_sheet_output(output, format, debug = false)
|
144
|
+
balances = {}
|
145
|
+
|
146
|
+
if debug
|
147
|
+
puts "\n=== Balance Sheet Parsing Debug ==="
|
148
|
+
puts "Raw output to parse:"
|
149
|
+
puts output
|
150
|
+
end
|
151
|
+
|
152
|
+
case format
|
153
|
+
when 'ledger'
|
154
|
+
balances = parse_ledger_balance_output(output, debug)
|
155
|
+
when 'beancount'
|
156
|
+
balances = parse_beancount_balance_output(output, debug)
|
157
|
+
else
|
158
|
+
puts "Unsupported format: #{format}"
|
159
|
+
end
|
160
|
+
|
161
|
+
balances
|
162
|
+
end
|
163
|
+
|
164
|
+
def parse_ledger_balance_output(output, debug = false)
|
165
|
+
balances = {}
|
166
|
+
|
167
|
+
output.each_line do |line|
|
168
|
+
puts "Parsing line: #{line.inspect}" if debug
|
169
|
+
line = line.strip
|
170
|
+
|
171
|
+
# Skip empty lines and summary lines
|
172
|
+
if line.empty? || line.include?('----')
|
173
|
+
puts " Skipping line (empty or summary)" if debug
|
174
|
+
next
|
175
|
+
end
|
176
|
+
|
177
|
+
# Extract account name and balance using a more flexible regex
|
178
|
+
# The ledger output format is typically something like:
|
179
|
+
# $18.10 Assets:Mercury Checking ••1090
|
180
|
+
if match = line.match(/\$\s*([\d,.-]+)\s+(.+)/)
|
181
|
+
balance = match[1].gsub(',', '').to_f
|
182
|
+
account = match[2].strip
|
183
|
+
|
184
|
+
puts " Extracted: Account='#{account}', Balance=$#{balance}" if debug
|
185
|
+
|
186
|
+
# Add to balances hash
|
187
|
+
balances[account] = balance
|
188
|
+
puts " Added to balances hash" if debug
|
189
|
+
else
|
190
|
+
puts " No match for balance pattern" if debug
|
191
|
+
end
|
192
|
+
end
|
193
|
+
|
194
|
+
balances
|
195
|
+
end
|
196
|
+
|
197
|
+
def parse_beancount_balance_output(output, debug = false)
|
198
|
+
balances = {}
|
199
|
+
|
200
|
+
output.each_line do |line|
|
201
|
+
# Skip header lines and summary lines
|
202
|
+
next if line.strip.empty? || line.include?('---') || line.include?('Assets:') || line.include?('Liabilities:')
|
203
|
+
|
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
|
212
|
+
end
|
213
|
+
|
214
|
+
balances
|
215
|
+
end
|
216
|
+
|
217
|
+
# Generate ledger reports (P&L, balance sheet, etc.)
|
218
|
+
def generate_ledger_reports(file_path, report_type, category = nil, end_date = nil)
|
219
|
+
# Check if ledger is installed
|
220
|
+
unless command_exists?('ledger')
|
221
|
+
puts "Error: 'ledger' command not found. Please install Ledger CLI to generate reports."
|
222
|
+
return
|
223
|
+
end
|
224
|
+
|
225
|
+
# Build date filter
|
226
|
+
date_filter = end_date ? " --end #{end_date}" : ""
|
227
|
+
|
228
|
+
# Build category filter
|
229
|
+
category_filter = category ? " #{category}" : ""
|
230
|
+
|
231
|
+
# Prepare to capture output
|
232
|
+
report_output = ""
|
233
|
+
report_output << "=== Mercury Banking Ledger Reports ===\n\n"
|
234
|
+
|
235
|
+
puts "\n=== Mercury Banking Ledger Reports ===\n"
|
236
|
+
|
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
|
+
|
257
|
+
def generate_ledger_balance_report_section(file_path, date_filter, category_filter)
|
258
|
+
section_output = ""
|
259
|
+
|
260
|
+
balance_cmd = "ledger -f #{file_path}#{date_filter} balance --flat Assets Liabilities Equity#{category_filter}"
|
261
|
+
balance_output = `#{balance_cmd}`
|
262
|
+
|
263
|
+
puts "\n=== Balance Sheet ===\n"
|
264
|
+
puts balance_output
|
265
|
+
|
266
|
+
section_output << "=== Balance Sheet ===\n"
|
267
|
+
section_output << balance_output
|
268
|
+
section_output << "\n"
|
269
|
+
|
270
|
+
section_output
|
271
|
+
end
|
272
|
+
|
273
|
+
def generate_ledger_pl_report_section(file_path, date_filter, category_filter)
|
274
|
+
section_output = ""
|
275
|
+
|
276
|
+
pl_cmd = "ledger -f #{file_path}#{date_filter} balance --flat Income Expenses#{category_filter}"
|
277
|
+
pl_output = `#{pl_cmd}`
|
278
|
+
|
279
|
+
puts "\n=== Income Statement ===\n"
|
280
|
+
puts pl_output
|
281
|
+
|
282
|
+
section_output << "=== Income Statement ===\n"
|
283
|
+
section_output << pl_output
|
284
|
+
section_output << "\n"
|
285
|
+
|
286
|
+
section_output
|
287
|
+
end
|
288
|
+
|
289
|
+
def generate_ledger_monthly_report_section(file_path, date_filter, category)
|
290
|
+
section_output = ""
|
291
|
+
|
292
|
+
if category
|
293
|
+
monthly_cmd = "ledger -f #{file_path}#{date_filter} --monthly --collapse register #{category}"
|
294
|
+
monthly_output = `#{monthly_cmd}`
|
295
|
+
|
296
|
+
puts "\n=== Monthly #{category} ===\n"
|
297
|
+
puts monthly_output
|
298
|
+
|
299
|
+
section_output << "=== Monthly #{category} ===\n"
|
300
|
+
section_output << monthly_output
|
301
|
+
section_output << "\n"
|
302
|
+
else
|
303
|
+
# Generate monthly expenses report
|
304
|
+
section_output << generate_ledger_monthly_category_report(file_path, date_filter, "Expenses")
|
305
|
+
|
306
|
+
# Generate monthly income report
|
307
|
+
section_output << generate_ledger_monthly_category_report(file_path, date_filter, "Income")
|
308
|
+
end
|
309
|
+
|
310
|
+
section_output
|
311
|
+
end
|
312
|
+
|
313
|
+
def generate_ledger_monthly_category_report(file_path, date_filter, category)
|
314
|
+
section_output = ""
|
315
|
+
|
316
|
+
monthly_cmd = "ledger -f #{file_path}#{date_filter} --monthly --collapse register #{category}"
|
317
|
+
monthly_output = `#{monthly_cmd}`
|
318
|
+
|
319
|
+
puts "\n=== Monthly #{category} ===\n"
|
320
|
+
puts monthly_output
|
321
|
+
|
322
|
+
section_output << "=== Monthly #{category} ===\n"
|
323
|
+
section_output << monthly_output
|
324
|
+
section_output << "\n"
|
325
|
+
|
326
|
+
section_output
|
327
|
+
end
|
328
|
+
|
329
|
+
def generate_ledger_register_report_section(file_path, date_filter, category)
|
330
|
+
section_output = ""
|
331
|
+
|
332
|
+
register_filter = category || "Assets Liabilities Equity Income Expenses"
|
333
|
+
register_cmd = "ledger -f #{file_path}#{date_filter} register #{register_filter}"
|
334
|
+
register_output = `#{register_cmd}`
|
335
|
+
|
336
|
+
puts "\n=== Transaction Register ===\n"
|
337
|
+
puts register_output
|
338
|
+
|
339
|
+
section_output << "=== Transaction Register ===\n"
|
340
|
+
section_output << register_output
|
341
|
+
section_output << "\n"
|
342
|
+
|
343
|
+
section_output
|
344
|
+
end
|
345
|
+
|
346
|
+
# Generate beancount reports
|
347
|
+
def generate_beancount_reports(file_path, report_type, category = nil, end_date = nil)
|
348
|
+
# Check if bean-report is installed
|
349
|
+
unless command_exists?('bean-report')
|
350
|
+
puts "Error: 'bean-report' command not found. Please install Beancount to generate reports."
|
351
|
+
return
|
352
|
+
end
|
353
|
+
|
354
|
+
# Prepare to capture output
|
355
|
+
report_output = ""
|
356
|
+
report_output << "=== Mercury Banking Beancount Reports ===\n\n"
|
357
|
+
|
358
|
+
puts "\n=== Mercury Banking Beancount Reports ===\n"
|
359
|
+
|
360
|
+
# Build date filter (beancount uses different syntax)
|
361
|
+
date_filter = end_date ? " -e #{end_date}" : ""
|
362
|
+
|
363
|
+
# 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
|
+
|
383
|
+
def generate_beancount_balance_report_section(file_path, date_filter)
|
384
|
+
section_output = ""
|
385
|
+
|
386
|
+
balance_cmd = "bean-report #{file_path}#{date_filter} balsheet"
|
387
|
+
balance_output = `#{balance_cmd}`
|
388
|
+
|
389
|
+
puts "\n=== Balance Sheet ===\n"
|
390
|
+
puts balance_output
|
391
|
+
|
392
|
+
section_output << "=== Balance Sheet ===\n"
|
393
|
+
section_output << balance_output
|
394
|
+
section_output << "\n"
|
395
|
+
|
396
|
+
section_output
|
397
|
+
end
|
398
|
+
|
399
|
+
def generate_beancount_pl_report_section(file_path, date_filter)
|
400
|
+
section_output = ""
|
401
|
+
|
402
|
+
pl_cmd = "bean-report #{file_path}#{date_filter} income"
|
403
|
+
pl_output = `#{pl_cmd}`
|
404
|
+
|
405
|
+
puts "\n=== Income Statement ===\n"
|
406
|
+
puts pl_output
|
407
|
+
|
408
|
+
section_output << "=== Income Statement ===\n"
|
409
|
+
section_output << pl_output
|
410
|
+
section_output << "\n"
|
411
|
+
|
412
|
+
section_output
|
413
|
+
end
|
414
|
+
|
415
|
+
def generate_beancount_monthly_report_section(file_path, date_filter)
|
416
|
+
section_output = ""
|
417
|
+
|
418
|
+
monthly_cmd = "bean-report #{file_path}#{date_filter} monthly"
|
419
|
+
monthly_output = `#{monthly_cmd}`
|
420
|
+
|
421
|
+
puts "\n=== Monthly Activity ===\n"
|
422
|
+
puts monthly_output
|
423
|
+
|
424
|
+
section_output << "=== Monthly Activity ===\n"
|
425
|
+
section_output << monthly_output
|
426
|
+
section_output << "\n"
|
427
|
+
|
428
|
+
section_output
|
429
|
+
end
|
430
|
+
|
431
|
+
def generate_beancount_register_report_section(file_path, date_filter, category)
|
432
|
+
section_output = ""
|
433
|
+
|
434
|
+
register_filter = category ? " --account #{category}" : ""
|
435
|
+
register_cmd = "bean-report #{file_path}#{date_filter}#{register_filter} register"
|
436
|
+
register_output = `#{register_cmd}`
|
437
|
+
|
438
|
+
puts "\n=== Transaction Register ===\n"
|
439
|
+
puts register_output
|
440
|
+
|
441
|
+
section_output << "=== Transaction Register ===\n"
|
442
|
+
section_output << register_output
|
443
|
+
section_output << "\n"
|
444
|
+
|
445
|
+
section_output
|
446
|
+
end
|
447
|
+
|
448
|
+
def export_transactions_to_ledger(transactions, file_path)
|
449
|
+
# Create a temporary file for ledger export
|
450
|
+
File.open(file_path, 'w') do |f|
|
451
|
+
write_ledger_header(f)
|
452
|
+
write_ledger_account_declarations(f, transactions)
|
453
|
+
write_ledger_transactions(f, transactions)
|
454
|
+
end
|
455
|
+
end
|
456
|
+
|
457
|
+
def write_ledger_header(file)
|
458
|
+
file.puts "; Mercury Bank Transactions Export"
|
459
|
+
file.puts "; Generated on #{Time.now.strftime('%Y-%m-%d %H:%M:%S')}"
|
460
|
+
file.puts
|
461
|
+
end
|
462
|
+
|
463
|
+
def write_ledger_account_declarations(file, transactions)
|
464
|
+
# Get unique account names from transactions
|
465
|
+
account_names = transactions.map { |t| t["accountName"] || "Unknown Account" }.uniq
|
466
|
+
|
467
|
+
# Define accounts dynamically based on transaction data
|
468
|
+
account_names.each do |account_name|
|
469
|
+
file.puts "account Assets:#{account_name}"
|
470
|
+
end
|
471
|
+
file.puts "account Expenses:Unknown"
|
472
|
+
file.puts "account Income:Unknown"
|
473
|
+
file.puts
|
474
|
+
end
|
475
|
+
|
476
|
+
def write_ledger_transactions(file, transactions)
|
477
|
+
# Filter out failed transactions
|
478
|
+
valid_transactions = transactions.reject { |t| t["status"] == "failed" }
|
479
|
+
|
480
|
+
# Sort transactions by date
|
481
|
+
valid_transactions.sort_by! { |t| t["postedAt"] || t["createdAt"] }.each do |t|
|
482
|
+
write_ledger_transaction(file, t)
|
483
|
+
end
|
484
|
+
end
|
485
|
+
|
486
|
+
def write_ledger_transaction(file, transaction)
|
487
|
+
date = transaction["postedAt"] ?
|
488
|
+
Time.parse(transaction["postedAt"]).strftime("%Y/%m/%d") :
|
489
|
+
Time.parse(transaction["createdAt"]).strftime("%Y/%m/%d")
|
490
|
+
description = transaction["bankDescription"] || transaction["externalMemo"] || "Unknown transaction"
|
491
|
+
amount = transaction["amount"]
|
492
|
+
account_name = transaction["accountName"] || "Unknown Account"
|
493
|
+
|
494
|
+
file.puts "#{date} #{description}"
|
495
|
+
file.puts " ; Transaction ID: #{transaction["id"]}"
|
496
|
+
file.puts " ; Status: #{transaction["status"]}"
|
497
|
+
file.puts " ; Reconciled: No"
|
498
|
+
|
499
|
+
write_ledger_postings(file, amount, account_name)
|
500
|
+
file.puts
|
501
|
+
end
|
502
|
+
|
503
|
+
def write_ledger_postings(file, amount, account_name)
|
504
|
+
if amount > 0
|
505
|
+
file.puts " Income:Unknown $-#{format("%.2f", amount)}"
|
506
|
+
file.puts " Assets:#{account_name} $#{format("%.2f", amount)}"
|
507
|
+
else
|
508
|
+
file.puts " Expenses:Unknown $#{format("%.2f", amount.abs)}"
|
509
|
+
file.puts " Assets:#{account_name} $-#{format("%.2f", amount.abs)}"
|
510
|
+
end
|
511
|
+
end
|
512
|
+
|
513
|
+
def export_transactions_to_beancount(transactions, file_path)
|
514
|
+
# Create a temporary file for beancount export
|
515
|
+
File.open(file_path, 'w') do |f|
|
516
|
+
write_beancount_header(f)
|
517
|
+
write_beancount_account_declarations(f, transactions)
|
518
|
+
write_beancount_transactions(f, transactions)
|
519
|
+
end
|
520
|
+
end
|
521
|
+
|
522
|
+
def write_beancount_header(file)
|
523
|
+
file.puts "; Mercury Bank Transactions Export"
|
524
|
+
file.puts "; Generated on #{Time.now.strftime('%Y-%m-%d %H:%M:%S')}"
|
525
|
+
file.puts
|
526
|
+
file.puts "option \"title\" \"Mercury Bank Transactions\""
|
527
|
+
file.puts "option \"operating_currency\" \"USD\""
|
528
|
+
file.puts
|
529
|
+
end
|
530
|
+
|
531
|
+
def write_beancount_account_declarations(file, transactions)
|
532
|
+
# Get unique account names from transactions
|
533
|
+
account_names = transactions.map { |t| t["accountName"] || "Unknown Account" }.uniq
|
534
|
+
|
535
|
+
# Define accounts dynamically based on transaction data
|
536
|
+
account_names.each do |account_name|
|
537
|
+
# Sanitize account name for beancount format
|
538
|
+
safe_account_name = account_name.gsub(/[^a-zA-Z0-9]/, '-')
|
539
|
+
file.puts "1970-01-01 open Assets:#{safe_account_name}"
|
540
|
+
end
|
541
|
+
file.puts "1970-01-01 open Expenses:Unknown"
|
542
|
+
file.puts "1970-01-01 open Income:Unknown"
|
543
|
+
file.puts
|
544
|
+
end
|
545
|
+
|
546
|
+
def write_beancount_transactions(file, transactions)
|
547
|
+
# Filter out failed transactions
|
548
|
+
valid_transactions = transactions.reject { |t| t["status"] == "failed" }
|
549
|
+
|
550
|
+
# Sort transactions by date
|
551
|
+
valid_transactions.sort_by! { |t| t["postedAt"] || t["createdAt"] }.each do |t|
|
552
|
+
write_beancount_transaction(file, t)
|
553
|
+
end
|
554
|
+
end
|
555
|
+
|
556
|
+
def write_beancount_transaction(file, transaction)
|
557
|
+
date = transaction["postedAt"] ?
|
558
|
+
Time.parse(transaction["postedAt"]).strftime("%Y-%m-%d") :
|
559
|
+
Time.parse(transaction["createdAt"]).strftime("%Y-%m-%d")
|
560
|
+
description = transaction["bankDescription"] || transaction["externalMemo"] || "Unknown transaction"
|
561
|
+
amount = transaction["amount"]
|
562
|
+
account_name = transaction["accountName"] || "Unknown Account"
|
563
|
+
# Sanitize account name for beancount format
|
564
|
+
safe_account_name = account_name.gsub(/[^a-zA-Z0-9]/, '-')
|
565
|
+
|
566
|
+
file.puts "#{date} * \"#{description}\""
|
567
|
+
file.puts " ; Transaction ID: #{transaction["id"]}"
|
568
|
+
file.puts " ; Status: #{transaction["status"]}"
|
569
|
+
file.puts " ; Reconciled: No"
|
570
|
+
|
571
|
+
write_beancount_postings(file, amount, safe_account_name)
|
572
|
+
file.puts
|
573
|
+
end
|
574
|
+
|
575
|
+
def write_beancount_postings(file, amount, account_name)
|
576
|
+
if amount > 0
|
577
|
+
file.puts " Income:Unknown -#{format("%.2f", amount)} USD"
|
578
|
+
file.puts " Assets:#{account_name} #{format("%.2f", amount)} USD"
|
579
|
+
else
|
580
|
+
file.puts " Expenses:Unknown #{format("%.2f", amount.abs)} USD"
|
581
|
+
file.puts " Assets:#{account_name} -#{format("%.2f", amount.abs)} USD"
|
582
|
+
end
|
583
|
+
end
|
584
|
+
end
|
585
|
+
end
|
586
|
+
end
|