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,297 +1,222 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'mercury_banking/cli/base'
2
4
  require 'mercury_banking/formatters/export_formatter'
3
- require 'mercury_banking/reports/balance_sheet'
4
5
 
5
6
  module MercuryBanking
6
7
  module CLI
8
+ # Module for balance check related functionality
9
+ module BalanceSheetHelper
10
+ # Get current balances for all Mercury accounts
11
+ def get_mercury_balances(accounts)
12
+ mercury_balances = {}
13
+ accounts.each do |account|
14
+ # Use the simplified account name for easier matching with ledger accounts
15
+ account_name = account['name'].gsub(/•+\d+/, '').strip
16
+ mercury_balances[account_name] = account['currentBalance'].to_f
17
+ end
18
+
19
+ if options[:verbose]
20
+ puts "\nMercury account balances:"
21
+ mercury_balances.each do |name, balance|
22
+ puts " #{name}: $#{format('%.2f', balance)}"
23
+ end
24
+ end
25
+
26
+ mercury_balances
27
+ end
28
+
29
+ # Display cross-check between Mercury and ledger balances
30
+ def display_balance_cross_check(mercury_balances, ledger_balances, verbose)
31
+ puts "\n=== Balance Sheet Cross-Check ==="
32
+ puts "#{'Mercury Account'.ljust(30)}#{'Mercury Balance'.ljust(15)}#{'Ledger Balance'.ljust(15)}Difference"
33
+ puts "-" * 75
34
+
35
+ total_diff = 0
36
+
37
+ if verbose
38
+ puts "\nMatching accounts:"
39
+ puts "Mercury accounts: #{mercury_balances.keys.join(', ')}"
40
+ puts "Ledger accounts: #{ledger_balances.keys.join(', ')}"
41
+ end
42
+
43
+ mercury_balances.each do |account_name, mercury_balance|
44
+ # Find the corresponding ledger account
45
+ ledger_account_key = nil
46
+ ledger_balance = 0
47
+
48
+ # Try different matching strategies
49
+ if ledger_balances.key?("Assets:Mercury:#{account_name.split.first}")
50
+ # Direct match with first word (e.g., "Checking" or "Savings")
51
+ ledger_account_key = "Assets:Mercury:#{account_name.split.first}"
52
+ ledger_balance = ledger_balances[ledger_account_key]
53
+ elsif account_name.include?("Checking") && ledger_balances.keys.any? { |k| k.include?("Checking") }
54
+ # Match by account type
55
+ ledger_account_key = ledger_balances.keys.find { |k| k.include?("Checking") }
56
+ ledger_balance = ledger_balances[ledger_account_key]
57
+ elsif account_name.include?("Savings") && ledger_balances.keys.any? { |k| k.include?("Savings") }
58
+ # Match by account type
59
+ ledger_account_key = ledger_balances.keys.find { |k| k.include?("Savings") }
60
+ ledger_balance = ledger_balances[ledger_account_key]
61
+ end
62
+
63
+ # Debug information
64
+ if verbose
65
+ puts "\nMatching for Mercury account '#{account_name}':"
66
+ puts " Found match: #{ledger_account_key || 'None'}"
67
+ puts " Mercury balance: $#{format('%.2f', mercury_balance)}"
68
+ puts " Ledger balance: $#{format('%.2f', ledger_balance)}"
69
+ end
70
+
71
+ # Skip zero-balance accounts unless in verbose mode
72
+ if mercury_balance == 0 && ledger_balance == 0
73
+ next unless verbose
74
+ end
75
+
76
+ # Calculate difference
77
+ diff = mercury_balance - ledger_balance
78
+ total_diff += diff.abs
79
+
80
+ # Format for display
81
+ mercury_balance_str = format("$%.2f", mercury_balance)
82
+ ledger_balance_str = format("$%.2f", ledger_balance)
83
+ diff_str = format("$%.2f", diff)
84
+
85
+ # Add warning marker for differences
86
+ diff_marker = diff.abs > 0.01 ? " ⚠️" : ""
87
+
88
+ puts account_name.ljust(30) + mercury_balance_str.ljust(15) + ledger_balance_str.ljust(15) + diff_str + diff_marker
89
+ end
90
+
91
+ puts "-" * 75
92
+ puts "Total Discrepancy: #{format('$%.2f', total_diff)}"
93
+
94
+ if total_diff > 0.01
95
+ puts "\n⚠️ Warning: There are discrepancies between Mercury account balances and the ledger balance sheet."
96
+ puts "This could be due to:"
97
+ puts " - Transactions not yet recorded in the ledger"
98
+ puts " - Incorrect categorization of transactions"
99
+ puts " - Timing differences between when transactions were recorded"
100
+ else
101
+ puts "\n✓ Balance sheet matches Mercury account balances."
102
+ end
103
+ end
104
+ end
105
+
7
106
  # Module for financial report commands
8
107
  module Financials
9
108
  # Define the financials command class
10
109
  class FinancialsCommand < Thor
11
110
  include MercuryBanking::CLI::Base
12
111
  include MercuryBanking::Formatters::ExportFormatter
13
- include MercuryBanking::Reports::BalanceSheet
14
-
15
- desc "balancesheet", "Generate a balance sheet report and cross-check with Mercury account balances"
112
+ include MercuryBanking::CLI::BalanceSheetHelper
113
+
114
+ desc "balancecheck", "Check that transactions match the bank balance shown in Mercury"
16
115
  method_option :format, type: :string, default: 'ledger', desc: 'Accounting format to use (ledger or beancount)'
17
- method_option :start, type: :string, default: '2020-01-01', desc: 'Start date for transactions (YYYY-MM-DD)'
18
- method_option :end, type: :string, desc: 'End date for transactions (YYYY-MM-DD)'
19
- method_option :save_report, type: :string, desc: 'Save report output to specified file'
116
+ method_option :ledger_file, type: :string, desc: 'Ledger file to check against Mercury balances'
20
117
  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'
22
- def balancesheet
23
- # Call the balance_sheet method directly
24
- balance_sheet
25
- end
26
-
27
- # Methods that should not be exposed as commands
28
- no_commands do
29
- # Implementation of the balance sheet functionality
30
- 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
-
118
+ def balancecheck
119
+ with_api_client do |client|
120
+ accounts = client.accounts
121
+
122
+ unless options[:ledger_file] && File.exist?(options[:ledger_file])
123
+ puts "Error: You must provide a valid ledger file to check. Use --ledger-file option."
79
124
  return
80
125
  end
126
+
127
+ # Get balances from the ledger file
128
+ ledger_file = options[:ledger_file]
129
+ format = options[:format] || File.extname(ledger_file).delete('.').downcase
81
130
 
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
205
- temp_file.unlink
131
+ # Get ledger balances
132
+ ledger_balances = get_ledger_balances(ledger_file, format)
133
+
134
+ if ledger_balances.empty?
135
+ puts "No account balances found in the ledger file."
136
+ return
206
137
  end
138
+
139
+ # Get current balances for all Mercury accounts
140
+ mercury_balances = get_mercury_balances(accounts)
141
+
142
+ # Display cross-check between Mercury and ledger balances
143
+ display_balance_cross_check(mercury_balances, ledger_balances, options[:verbose])
207
144
  end
208
145
  end
209
-
210
- desc "incomestatement", "Generate an income statement report"
211
- method_option :format, type: :string, default: 'ledger', desc: 'Accounting format to use (ledger or beancount)'
212
- method_option :start, type: :string, default: '2020-01-01', desc: 'Start date for transactions (YYYY-MM-DD)'
213
- method_option :end, type: :string, desc: 'End date for transactions (YYYY-MM-DD)'
214
- method_option :save_report, type: :string, desc: 'Save report output to specified file'
215
- method_option :verbose, type: :boolean, default: false, desc: 'Show detailed debug information'
216
- def incomestatement
217
- # Access the parent class to call with_api_client
218
- parent_class = self.class.parent_class
219
- parent_instance = parent_class.new
220
-
221
- parent_instance.with_api_client do |client|
222
- # Get all transactions for the income statement
223
- start_date = options[:start]
224
- end_date = options[:end]
225
- format = options[:format]
226
- verbose = options[:verbose]
227
-
228
- date_range = "since #{start_date}"
229
- date_range += " until #{end_date}" if end_date
146
+
147
+ # Methods that should not be exposed as commands
148
+ no_commands do
149
+ # Get balances from ledger file
150
+ def get_ledger_balances(ledger_file, format)
151
+ ledger_balances = {}
230
152
 
231
- puts "Fetching transactions for all accounts #{date_range}..."
153
+ # Get balance sheet output from the ledger file
154
+ balance_sheet_output = case format
155
+ when 'ledger'
156
+ `ledger -f #{ledger_file} balance Assets:Mercury`
157
+ when 'beancount'
158
+ `bean-report #{ledger_file} balances`
159
+ else
160
+ puts "Unsupported format: #{format}. Please use 'ledger' or 'beancount'."
161
+ return {}
162
+ end
232
163
 
233
- transactions = client.get_all_transactions(start_date)
164
+ puts "Raw ledger output:" if options[:verbose]
165
+ puts balance_sheet_output if options[:verbose]
234
166
 
235
- # Filter by end date if specified
236
- if end_date
237
- end_date_obj = Date.parse(end_date)
238
- transactions = transactions.select do |t|
239
- transaction_date = t["postedAt"] ? Date.parse(t["postedAt"]) : Date.parse(t["createdAt"])
240
- transaction_date <= end_date_obj
167
+ # Directly parse the specific format we know ledger outputs
168
+ balance_sheet_output.each_line do |line|
169
+ line = line.strip
170
+ # Skip empty lines
171
+ next if line.empty?
172
+
173
+ # Split the line on whitespace but preserve the account path
174
+ parts = line.split(/\s+/, 2) # Split on first whitespace
175
+ if parts.length == 2
176
+ amount_part = parts[0]
177
+ account_part = parts[1]
178
+
179
+ # Parse amount - remove $ and , characters
180
+ amount = amount_part.gsub(/[$,]/, '').to_f
181
+
182
+ # Store in the hash
183
+ ledger_balances[account_part] = amount
184
+
185
+ puts "Matched: #{account_part} = $#{amount}" if options[:verbose]
241
186
  end
242
187
  end
243
188
 
244
- # Create a temporary file
245
- require 'tempfile'
246
- temp_file = Tempfile.new(['mercury_transactions', ".#{format}"])
247
- output_file = temp_file.path
248
-
249
- # Export transactions in the specified format
250
- case format
251
- when 'ledger'
252
- parent_instance.export_to_ledger(transactions, output_file, [], verbose)
253
- income_statement_output = parent_instance.generate_ledger_reports(output_file, 'income', nil, end_date)
254
- when 'beancount'
255
- parent_instance.export_to_beancount(transactions, output_file, [], verbose)
256
- income_statement_output = parent_instance.generate_beancount_reports(output_file, 'income', nil, end_date)
257
- else
258
- puts "Unsupported format: #{format}. Please use 'ledger' or 'beancount'."
259
- temp_file.unlink
260
- return
261
- end
262
-
263
- # Display the income statement
264
- puts income_statement_output
265
-
266
- # Save the report to a file if requested
267
- if options[:save_report]
268
- File.open(options[:save_report], 'w') do |file|
269
- file.puts income_statement_output
189
+ if options[:verbose]
190
+ puts "Parsed ledger balances:"
191
+ if ledger_balances.empty?
192
+ puts " No account balances found in the ledger file."
193
+ else
194
+ ledger_balances.each do |account, amount|
195
+ puts " #{account}: $#{format('%.2f', amount)}"
196
+ end
270
197
  end
271
- puts "\nReport saved to #{options[:save_report]}"
272
198
  end
273
199
 
274
- # Clean up the temporary file
275
- temp_file.unlink
200
+ ledger_balances
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)
@@ -299,4 +224,4 @@ module MercuryBanking
299
224
  end
300
225
  end
301
226
  end
302
- end
227
+ end