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/formatters/export_formatter'
3
5
  require 'mercury_banking/reports/balance_sheet'
@@ -11,202 +13,124 @@ module MercuryBanking
11
13
  include MercuryBanking::CLI::Base
12
14
  include MercuryBanking::Formatters::ExportFormatter
13
15
  include MercuryBanking::Reports::BalanceSheet
14
-
16
+ # BalanceSheetHelper will be included after it's defined
17
+
15
18
  desc "balancesheet", "Generate a balance sheet report and cross-check with Mercury account balances"
16
19
  method_option :format, type: :string, default: 'ledger', desc: 'Accounting format to use (ledger or beancount)'
17
20
  method_option :start, type: :string, default: '2020-01-01', desc: 'Start date for transactions (YYYY-MM-DD)'
18
21
  method_option :end, type: :string, desc: 'End date for transactions (YYYY-MM-DD)'
19
22
  method_option :save_report, type: :string, desc: 'Save report output to specified file'
20
23
  method_option :verbose, type: :boolean, default: false, desc: 'Show detailed debug information'
21
- method_option :ledger_file, type: :string, desc: 'Use an existing ledger file instead of fetching transactions from Mercury'
24
+ method_option :ledger_file, type: :string,
25
+ desc: 'Use an existing ledger file instead of fetching transactions from Mercury'
22
26
  def balancesheet
23
27
  # Call the balance_sheet method directly
24
28
  balance_sheet
25
29
  end
26
-
30
+
27
31
  # Methods that should not be exposed as commands
28
32
  no_commands do
29
33
  # Implementation of the balance sheet functionality
30
34
  def balance_sheet
31
- # If a ledger file is provided, use it directly
32
- if options[:ledger_file]
33
- ledger_file = options[:ledger_file]
34
- format = options[:format]
35
- end_date = options[:end]
36
- verbose = options[:verbose]
37
-
38
- puts "Using existing ledger file: #{ledger_file}"
39
-
40
- if !File.exist?(ledger_file)
41
- puts "Error: Ledger file not found at #{ledger_file}"
42
- return
43
- end
44
-
45
- # Generate balance sheet from the ledger file
46
- case format
47
- when 'ledger'
48
- balance_sheet_output = generate_ledger_balance_sheet(ledger_file, end_date)
49
- if balance_sheet_output
50
- puts "\n=== Balance Sheet ===\n"
51
- puts balance_sheet_output
52
-
53
- # Save report to file if requested
54
- if options[:save_report]
55
- File.write(options[:save_report], balance_sheet_output)
56
- puts "\nReport saved to #{options[:save_report]}"
57
- end
58
- else
59
- puts "Failed to generate balance sheet from ledger file."
60
- end
61
- when 'beancount'
62
- balance_sheet_output = generate_beancount_balance_sheet(ledger_file, end_date)
63
- if balance_sheet_output
64
- puts "\n=== Balance Sheet ===\n"
65
- puts balance_sheet_output
66
-
67
- # Save report to file if requested
68
- if options[:save_report]
69
- File.write(options[:save_report], balance_sheet_output)
70
- puts "\nReport saved to #{options[:save_report]}"
71
- end
72
- else
73
- puts "Failed to generate balance sheet from beancount file."
74
- end
75
- else
76
- puts "Unsupported format: #{format}. Please use 'ledger' or 'beancount'."
77
- end
78
-
35
+ client = MercuryBanking::Client.new
36
+ accounts = client.get_accounts
37
+
38
+ if options[:ledger_file] && File.exist?(options[:ledger_file])
39
+ process_existing_ledger_file(options[:ledger_file], options[:format], options[:save_report], options[:verbose])
79
40
  return
80
41
  end
81
-
82
- # Otherwise, fetch data from Mercury API
83
- with_api_client do |client|
84
- # Get current account balances from Mercury
85
- accounts = client.accounts
86
-
87
- # Create a mapping of account names to balances
88
- mercury_balances = {}
89
- accounts.each do |account|
90
- mercury_balances[account['name']] = account['currentBalance']
91
- end
92
-
93
- # Get all transactions for the balance sheet
94
- start_date = options[:start]
95
- end_date = options[:end]
96
- format = options[:format]
97
- verbose = options[:verbose]
98
-
99
- date_range = "since #{start_date}"
100
- date_range += " until #{end_date}" if end_date
101
-
102
- puts "Fetching transactions for all accounts #{date_range}..."
103
-
104
- transactions = client.get_all_transactions(start_date)
105
-
106
- # Filter by end date if specified
107
- if end_date
108
- end_date_obj = Date.parse(end_date)
109
- transactions = transactions.select do |t|
110
- transaction_date = t["postedAt"] ? Date.parse(t["postedAt"]) : Date.parse(t["createdAt"])
111
- transaction_date <= end_date_obj
112
- end
113
- end
114
-
115
- if transactions.empty?
116
- puts "No transactions found to generate balance sheet."
117
- return
118
- end
119
-
120
- # Create a temporary file
121
- require 'tempfile'
122
- temp_file = Tempfile.new(['mercury_transactions', ".#{format}"])
123
- output_file = temp_file.path
124
-
125
- # Export transactions in the specified format
126
- case format
127
- when 'ledger'
128
- export_to_ledger(transactions, output_file, [], verbose)
129
- balance_sheet_output = generate_ledger_balance_sheet(output_file, end_date)
130
- when 'beancount'
131
- export_to_beancount(transactions, output_file, [], verbose)
132
- balance_sheet_output = generate_beancount_balance_sheet(output_file, end_date)
133
- else
134
- puts "Unsupported format: #{format}. Please use 'ledger' or 'beancount'."
135
- temp_file.unlink
136
- return
137
- end
138
-
139
- # Parse the balance sheet output to extract account balances
140
- ledger_balances = parse_balance_sheet_output(balance_sheet_output, format, verbose)
141
-
142
- # Cross-check Mercury balances with ledger balances
143
- puts "\n=== Balance Sheet Cross-Check ==="
144
- puts "Mercury Account".ljust(30) + "Mercury Balance".ljust(15) + "Ledger Balance".ljust(15) + "Difference"
145
- puts "-" * 75
146
-
147
- total_diff = 0
148
- mercury_balances.each do |account_name, mercury_balance|
149
- # Find the corresponding ledger account (might be prefixed with Assets:)
150
- ledger_account_key = ledger_balances.keys.find { |k| k.include?(account_name) }
151
-
152
- # Debug information
153
- if verbose
154
- puts "Looking for Mercury account '#{account_name}' in ledger accounts:"
155
- ledger_balances.keys.each do |k|
156
- puts " - #{k} (match: #{k.include?(account_name) ? 'Yes' : 'No'})"
157
- end
158
- end
159
-
160
- ledger_balance = ledger_account_key ? ledger_balances[ledger_account_key] : 0
161
-
162
- # Calculate difference
163
- diff = mercury_balance - ledger_balance
164
- total_diff += diff.abs
165
-
166
- # Format for display
167
- mercury_balance_str = format("$%.2f", mercury_balance)
168
- ledger_balance_str = format("$%.2f", ledger_balance)
169
- diff_str = format("$%.2f", diff)
170
-
171
- # Add warning marker for differences
172
- diff_marker = diff.abs > 0.01 ? " ⚠️" : ""
173
-
174
- puts account_name.ljust(30) + mercury_balance_str.ljust(15) + ledger_balance_str.ljust(15) + diff_str + diff_marker
175
- end
176
-
177
- puts "-" * 75
178
- puts "Total Discrepancy: #{format("$%.2f", total_diff)}"
179
-
180
- if total_diff > 0.01
181
- puts "\n⚠️ Warning: There are discrepancies between Mercury account balances and the ledger balance sheet."
182
- puts "This could be due to:"
183
- puts " - Transactions not yet recorded in the ledger"
184
- puts " - Incorrect categorization of transactions"
185
- puts " - Timing differences between when transactions were recorded"
186
- else
187
- puts "\n✓ Balance sheet matches Mercury account balances."
188
- end
189
-
190
- # Save report to file if requested
191
- if options[:save_report] && balance_sheet_output
192
- full_report = balance_sheet_output + "\n\n" + "=== Balance Sheet Cross-Check ===\n"
193
- mercury_balances.each do |account_name, mercury_balance|
194
- ledger_account_key = ledger_balances.keys.find { |k| k.include?(account_name) }
195
- ledger_balance = ledger_account_key ? ledger_balances[ledger_account_key] : 0
196
- diff = mercury_balance - ledger_balance
197
- full_report += "#{account_name}: Mercury $#{format("%.2f", mercury_balance)} vs Ledger $#{format("%.2f", ledger_balance)} (Diff: $#{format("%.2f", diff)})\n"
198
- end
199
-
200
- File.write(options[:save_report], full_report)
201
- puts "\nReport saved to #{options[:save_report]}"
202
- end
203
-
204
- # Clean up temporary file
42
+
43
+ # Fetch all transactions from all accounts
44
+ transactions = fetch_all_transactions(client, accounts, options[:end])
45
+ puts "Found #{transactions.length} transactions across #{accounts.length} accounts."
46
+
47
+ # Get current balances for all Mercury accounts
48
+ mercury_balances = get_mercury_balances(accounts)
49
+
50
+ # Create a temporary file for exporting transactions
51
+ temp_file, output_file = create_temp_file(options[:format])
52
+
53
+ # Export transactions and generate balance sheet
54
+ balance_sheet_output = export_and_generate_balance_sheet(
55
+ transactions,
56
+ output_file,
57
+ options[:format],
58
+ options[:end],
59
+ options[:verbose]
60
+ )
61
+
62
+ # If balance sheet generation failed, exit
63
+ unless balance_sheet_output
64
+ temp_file.close
205
65
  temp_file.unlink
66
+ return
206
67
  end
68
+
69
+ # Parse the balance sheet output to get account balances
70
+ ledger_balances = {}
71
+ balance_sheet_output.each_line do |line|
72
+ next unless line.strip.start_with?('Assets:')
73
+
74
+ parts = line.strip.split(/\s+/)
75
+ account = parts.first
76
+ amount = parts.last.gsub(/[$,]/, '').to_f
77
+ ledger_balances[account] = amount
78
+ end
79
+
80
+ # Display cross-check between Mercury and ledger balances
81
+ display_balance_cross_check(mercury_balances, ledger_balances, options[:verbose])
82
+
83
+ # Save full report to file if requested
84
+ save_full_report(balance_sheet_output, mercury_balances, ledger_balances, options[:save_report]) if options[:save_report]
85
+
86
+ # Clean up temporary file
87
+ temp_file.close
88
+ temp_file.unlink
89
+ end
90
+
91
+ # Process an existing ledger file
92
+ def process_existing_ledger_file(ledger_file, format, save_path, _verbose)
93
+ puts "Using existing ledger file: #{ledger_file}"
94
+
95
+ # Determine the format based on file extension if not specified
96
+ unless format
97
+ format = File.extname(ledger_file).delete('.').downcase
98
+ puts "Detected format: #{format}"
99
+ end
100
+
101
+ # Generate balance sheet based on format
102
+ balance_sheet_output = process_ledger_format(ledger_file, format)
103
+
104
+ # If balance sheet generation failed, exit
105
+ return unless balance_sheet_output
106
+
107
+ # Save report to file if requested
108
+ return unless save_path
109
+
110
+ File.write(save_path, balance_sheet_output)
111
+ puts "\nReport saved to #{save_path}"
112
+ end
113
+
114
+ # Process ledger format file
115
+ def process_ledger_format(ledger_file, format)
116
+ case format
117
+ when 'ledger'
118
+ generate_ledger_balance_sheet(ledger_file)
119
+ when 'beancount'
120
+ generate_beancount_balance_sheet(ledger_file)
121
+ else
122
+ puts "Unsupported format: #{format}. Please use 'ledger' or 'beancount'."
123
+ nil
124
+ end
125
+ end
126
+
127
+ # Helper method to save a report to a file
128
+ def save_report_to_file(content, file_path)
129
+ File.write(file_path, content)
130
+ puts "\nReport saved to #{file_path}"
207
131
  end
208
132
  end
209
-
133
+
210
134
  desc "incomestatement", "Generate an income statement report"
211
135
  method_option :format, type: :string, default: 'ledger', desc: 'Accounting format to use (ledger or beancount)'
212
136
  method_option :start, type: :string, default: '2020-01-01', desc: 'Start date for transactions (YYYY-MM-DD)'
@@ -217,35 +141,35 @@ module MercuryBanking
217
141
  # Access the parent class to call with_api_client
218
142
  parent_class = self.class.parent_class
219
143
  parent_instance = parent_class.new
220
-
144
+
221
145
  parent_instance.with_api_client do |client|
222
146
  # Get all transactions for the income statement
223
147
  start_date = options[:start]
224
148
  end_date = options[:end]
225
149
  format = options[:format]
226
150
  verbose = options[:verbose]
227
-
151
+
228
152
  date_range = "since #{start_date}"
229
153
  date_range += " until #{end_date}" if end_date
230
-
154
+
231
155
  puts "Fetching transactions for all accounts #{date_range}..."
232
-
156
+
233
157
  transactions = client.get_all_transactions(start_date)
234
-
158
+
235
159
  # Filter by end date if specified
236
160
  if end_date
237
161
  end_date_obj = Date.parse(end_date)
238
162
  transactions = transactions.select do |t|
239
- transaction_date = t["postedAt"] ? Date.parse(t["postedAt"]) : Date.parse(t["createdAt"])
163
+ transaction_date = Date.parse(t["postedAt"] || t["createdAt"])
240
164
  transaction_date <= end_date_obj
241
165
  end
242
166
  end
243
-
167
+
244
168
  # Create a temporary file
245
169
  require 'tempfile'
246
170
  temp_file = Tempfile.new(['mercury_transactions', ".#{format}"])
247
171
  output_file = temp_file.path
248
-
172
+
249
173
  # Export transactions in the specified format
250
174
  case format
251
175
  when 'ledger'
@@ -257,12 +181,13 @@ module MercuryBanking
257
181
  else
258
182
  puts "Unsupported format: #{format}. Please use 'ledger' or 'beancount'."
259
183
  temp_file.unlink
260
- return
184
+ income_statement_output = nil
185
+ break # Use break instead of return to exit the loop
261
186
  end
262
-
263
- # Display the income statement
264
- puts income_statement_output
265
-
187
+
188
+ # Display the income statement if it exists
189
+ puts income_statement_output if income_statement_output
190
+
266
191
  # Save the report to a file if requested
267
192
  if options[:save_report]
268
193
  File.open(options[:save_report], 'w') do |file|
@@ -270,33 +195,160 @@ module MercuryBanking
270
195
  end
271
196
  puts "\nReport saved to #{options[:save_report]}"
272
197
  end
273
-
198
+
274
199
  # Clean up the temporary file
275
200
  temp_file.unlink
276
201
  end
277
202
  end
278
-
203
+
279
204
  # Store reference to parent class
280
205
  class << self
281
206
  attr_accessor :parent_class
282
207
  end
283
208
  end
284
-
209
+
285
210
  # Add financial report commands to the CLI class
286
211
  def self.included(base)
287
212
  base.class_eval do
288
213
  # Register the financials command
289
214
  desc "financials SUBCOMMAND", "Financial reporting commands"
290
215
  subcommand "financials", FinancialsCommand
291
-
216
+
292
217
  # Set the parent class for the subcommand
293
218
  FinancialsCommand.parent_class = base
294
-
219
+
295
220
  # Remove the old methods with underscores if they exist
296
221
  remove_method :financials_balancesheet if method_defined?(:financials_balancesheet)
297
222
  remove_method :financials_incomestatement if method_defined?(:financials_incomestatement)
298
223
  end
299
224
  end
300
225
  end
226
+
227
+ # Module for balance sheet related functionality
228
+ module BalanceSheetHelper
229
+ # Fetch all transactions from all accounts
230
+ def fetch_all_transactions(client, accounts, end_date)
231
+ transactions = []
232
+ accounts.each do |account|
233
+ account_id = account['id']
234
+ account_transactions = client.get_transactions(account_id)
235
+
236
+ # Filter by end date if provided
237
+ if end_date
238
+ account_transactions = account_transactions.select do |t|
239
+ t['postedAt'] && Date.parse(t['postedAt']) <= Date.parse(end_date)
240
+ end
241
+ end
242
+
243
+ transactions.concat(account_transactions)
244
+ end
245
+ transactions
246
+ end
247
+
248
+ # Get current balances for all Mercury accounts
249
+ def get_mercury_balances(accounts)
250
+ mercury_balances = {}
251
+ accounts.each do |account|
252
+ mercury_balances[account['name']] = account['currentBalance']
253
+ end
254
+ mercury_balances
255
+ end
256
+
257
+ # Create a temporary file for exporting transactions
258
+ def create_temp_file(format)
259
+ require 'tempfile'
260
+ temp_file = Tempfile.new(['mercury_transactions', ".#{format}"])
261
+ [temp_file, temp_file.path]
262
+ end
263
+
264
+ # Export transactions and generate balance sheet
265
+ def export_and_generate_balance_sheet(transactions, output_file, format, end_date, verbose)
266
+ case format
267
+ when 'ledger'
268
+ export_to_ledger(transactions, output_file, [], verbose)
269
+ generate_ledger_balance_sheet(output_file, end_date)
270
+ when 'beancount'
271
+ export_to_beancount(transactions, output_file, [], verbose)
272
+ generate_beancount_balance_sheet(output_file, end_date)
273
+ else
274
+ puts "Unsupported format: #{format}. Please use 'ledger' or 'beancount'."
275
+ nil
276
+ end
277
+ end
278
+
279
+ # Display cross-check between Mercury and ledger balances
280
+ def display_balance_cross_check(mercury_balances, ledger_balances, verbose)
281
+ puts "\n=== Balance Sheet Cross-Check ==="
282
+ puts "#{'Mercury Account'.ljust(30)}#{'Mercury Balance'.ljust(15)}#{'Ledger Balance'.ljust(15)}Difference"
283
+ puts "-" * 75
284
+
285
+ total_diff = 0
286
+ mercury_balances.each do |account_name, mercury_balance|
287
+ # Find the corresponding ledger account (might be prefixed with Assets:)
288
+ ledger_account_key = ledger_balances.keys.find { |k| k.include?(account_name) }
289
+
290
+ # Debug information
291
+ if verbose
292
+ puts "Looking for Mercury account '#{account_name}' in ledger accounts:"
293
+ ledger_balances.each_key do |k|
294
+ puts " - #{k} (match: #{k.include?(account_name) ? 'Yes' : 'No'})"
295
+ end
296
+ end
297
+
298
+ ledger_balance = ledger_account_key ? ledger_balances[ledger_account_key] : 0
299
+
300
+ # Calculate difference
301
+ diff = mercury_balance - ledger_balance
302
+ total_diff += diff.abs
303
+
304
+ # Format for display
305
+ mercury_balance_str = format("$%.2f", mercury_balance)
306
+ ledger_balance_str = format("$%.2f", ledger_balance)
307
+ diff_str = format("$%.2f", diff)
308
+
309
+ # Add warning marker for differences
310
+ diff_marker = diff.abs > 0.01 ? " ⚠️" : ""
311
+
312
+ puts account_name.ljust(30) + mercury_balance_str.ljust(15) + ledger_balance_str.ljust(15) + diff_str + diff_marker
313
+ end
314
+
315
+ puts "-" * 75
316
+ puts "Total Discrepancy: #{format('$%.2f', total_diff)}"
317
+
318
+ if total_diff > 0.01
319
+ puts "\n⚠️ Warning: There are discrepancies between Mercury account balances and the ledger balance sheet."
320
+ puts "This could be due to:"
321
+ puts " - Transactions not yet recorded in the ledger"
322
+ puts " - Incorrect categorization of transactions"
323
+ puts " - Timing differences between when transactions were recorded"
324
+ else
325
+ puts "\n✓ Balance sheet matches Mercury account balances."
326
+ end
327
+ end
328
+
329
+ # Save full report to file
330
+ def save_full_report(balance_sheet_output, mercury_balances, ledger_balances, save_path)
331
+ full_report = "#{balance_sheet_output}\n\n=== Balance Sheet Cross-Check ===\n"
332
+ mercury_balances.each do |account_name, mercury_balance|
333
+ ledger_account_key = ledger_balances.keys.find { |k| k.include?(account_name) }
334
+ ledger_balance = ledger_account_key ? ledger_balances[ledger_account_key] : 0
335
+ diff = mercury_balance - ledger_balance
336
+
337
+ # Format the report line with proper line breaks
338
+ report_line = "#{account_name}: "
339
+ report_line += "Mercury $#{format('%.2f', mercury_balance)} "
340
+ report_line += "vs Ledger $#{format('%.2f', ledger_balance)} "
341
+ report_line += "(Diff: $#{format('%.2f', diff)})\n"
342
+
343
+ full_report += report_line
344
+ end
345
+
346
+ File.write(save_path, full_report)
347
+ puts "\nReport saved to #{save_path}"
348
+ end
349
+ end
301
350
  end
302
- end
351
+ end
352
+
353
+ # Include the BalanceSheetHelper module in the FinancialsCommand class
354
+ MercuryBanking::CLI::Financials::FinancialsCommand.include(MercuryBanking::CLI::BalanceSheetHelper)