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