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,306 @@
|
|
1
|
+
module MercuryBanking
|
2
|
+
module Formatters
|
3
|
+
# Formatter for exporting transactions to different formats
|
4
|
+
module ExportFormatter
|
5
|
+
# Export transactions to ledger format
|
6
|
+
def export_to_ledger(transactions, output_file, reconciled_transactions = [], verbose = false)
|
7
|
+
File.open(output_file, 'w') do |file|
|
8
|
+
file.puts "; Mercury Bank Transactions Export"
|
9
|
+
file.puts "; Generated on #{Time.now.strftime('%Y-%m-%d %H:%M:%S')}"
|
10
|
+
file.puts
|
11
|
+
|
12
|
+
# Add account declarations to ensure ledger recognizes them
|
13
|
+
accounts = transactions.map { |t| t["accountName"] || "Mercury Account" }.uniq
|
14
|
+
accounts.each do |account|
|
15
|
+
file.puts "account Assets:#{account}"
|
16
|
+
end
|
17
|
+
file.puts "account Expenses:Unknown"
|
18
|
+
file.puts "account Income:Unknown"
|
19
|
+
file.puts
|
20
|
+
|
21
|
+
# Debug: Show what accounts are being created
|
22
|
+
if verbose
|
23
|
+
puts "Creating ledger file with the following accounts:"
|
24
|
+
accounts.each do |account|
|
25
|
+
puts "- Assets:#{account}"
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
# Filter out failed transactions
|
30
|
+
valid_transactions = transactions.reject { |t| t["status"] == "failed" }
|
31
|
+
|
32
|
+
# Sort transactions chronologically by timestamp
|
33
|
+
sorted_transactions = sort_transactions_chronologically(valid_transactions)
|
34
|
+
|
35
|
+
sorted_transactions.each do |t|
|
36
|
+
# Get the full timestamp for precise ordering
|
37
|
+
timestamp = t["postedAt"] || t["createdAt"]
|
38
|
+
date = Time.parse(timestamp).strftime("%Y/%m/%d")
|
39
|
+
time = Time.parse(timestamp).strftime("%H:%M:%S")
|
40
|
+
description = t["bankDescription"] || t["externalMemo"] || "Unknown transaction"
|
41
|
+
amount = t["amount"].to_f
|
42
|
+
account_name = t["accountName"] || "Mercury Account"
|
43
|
+
transaction_id = t["id"]
|
44
|
+
|
45
|
+
# Check if this transaction is reconciled
|
46
|
+
is_reconciled = reconciled_transactions.include?(transaction_id)
|
47
|
+
|
48
|
+
# Sanitize description for ledger format - replace newlines with spaces and escape semicolons
|
49
|
+
description = description.gsub(/[\r\n]+/, ' ').gsub(/;/, '\\;')
|
50
|
+
|
51
|
+
# Ensure the description doesn't start with characters that could be interpreted as a date
|
52
|
+
# or other ledger syntax elements
|
53
|
+
if description =~ /^\d/ || description =~ /^[v#]/ || description =~ /^\s*\d{4}/ || description =~ /^\s*\d{2}\/\d{2}/
|
54
|
+
description = "- #{description}"
|
55
|
+
end
|
56
|
+
|
57
|
+
# Format the transaction with proper indentation and alignment
|
58
|
+
file.puts "#{date} #{is_reconciled ? '* ' : ''}#{description}"
|
59
|
+
file.puts " ; Transaction ID: #{transaction_id}"
|
60
|
+
file.puts " ; Timestamp: #{timestamp}"
|
61
|
+
file.puts " ; Time: #{time}"
|
62
|
+
file.puts " ; Status: #{t["status"]}"
|
63
|
+
|
64
|
+
# Add reconciliation metadata
|
65
|
+
if is_reconciled
|
66
|
+
file.puts " ; :reconciled: true"
|
67
|
+
end
|
68
|
+
|
69
|
+
# Add counterparty information if available
|
70
|
+
if t["counterpartyName"]
|
71
|
+
# Sanitize counterparty name
|
72
|
+
counterparty = t["counterpartyName"].gsub(/[\r\n]+/, ' ').gsub(/;/, '\\;')
|
73
|
+
file.puts " ; Counterparty: #{counterparty}"
|
74
|
+
end
|
75
|
+
|
76
|
+
# Add additional metadata if available
|
77
|
+
if t["externalMemo"] && t["externalMemo"] != description
|
78
|
+
# Sanitize memo
|
79
|
+
memo = t["externalMemo"].gsub(/[\r\n]+/, ' ').gsub(/;/, '\\;')
|
80
|
+
file.puts " ; Memo: #{memo}"
|
81
|
+
end
|
82
|
+
|
83
|
+
# Add the postings
|
84
|
+
if amount > 0
|
85
|
+
# Credit (money coming in)
|
86
|
+
file.puts " Assets:#{account_name} $#{format('%.2f', amount)}"
|
87
|
+
file.puts " Income:Unknown $#{format('%.2f', -amount)}"
|
88
|
+
else
|
89
|
+
# Debit (money going out)
|
90
|
+
file.puts " Assets:#{account_name} $#{format('%.2f', amount)}"
|
91
|
+
file.puts " Expenses:Unknown $#{format('%.2f', amount.abs)}"
|
92
|
+
end
|
93
|
+
file.puts
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
puts "Verifying ledger file validity..."
|
98
|
+
verify_ledger_file(output_file, verbose)
|
99
|
+
end
|
100
|
+
|
101
|
+
# Sort transactions chronologically by timestamp
|
102
|
+
def sort_transactions_chronologically(transactions)
|
103
|
+
transactions.sort_by do |t|
|
104
|
+
# Use postedAt if available, otherwise fall back to createdAt
|
105
|
+
timestamp = t["postedAt"] || t["createdAt"]
|
106
|
+
Time.parse(timestamp)
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
# Verify that the ledger file is valid
|
111
|
+
def verify_ledger_file(output_file, verbose = false)
|
112
|
+
begin
|
113
|
+
# Debug: Verify the ledger file is valid
|
114
|
+
cmd = "ledger -f #{output_file} balance"
|
115
|
+
output = `#{cmd}`
|
116
|
+
|
117
|
+
if $?.success?
|
118
|
+
if verbose
|
119
|
+
puts "Ledger verification output: #{output}"
|
120
|
+
|
121
|
+
# Show all accounts in the ledger file
|
122
|
+
cmd = "ledger -f #{output_file} accounts"
|
123
|
+
output = `#{cmd}`
|
124
|
+
|
125
|
+
if $?.success?
|
126
|
+
puts "All accounts in the ledger file:"
|
127
|
+
puts output
|
128
|
+
else
|
129
|
+
puts "Warning: Could not list accounts in the ledger file."
|
130
|
+
puts "This may indicate a problem with the file format, but the balance command succeeded."
|
131
|
+
end
|
132
|
+
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
|
+
end
|
143
|
+
rescue => e
|
144
|
+
puts "Error during ledger verification: #{e.message}"
|
145
|
+
puts "This doesn't necessarily mean the file is invalid, but there may be issues with the ledger command."
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
# Export transactions to beancount format
|
150
|
+
def export_to_beancount(transactions, output_file, reconciled_transactions = [], verbose = false)
|
151
|
+
File.open(output_file, 'w') do |file|
|
152
|
+
file.puts "; Mercury Bank Transactions Export"
|
153
|
+
file.puts "; Generated on #{Time.now.strftime('%Y-%m-%d %H:%M:%S')}"
|
154
|
+
file.puts
|
155
|
+
|
156
|
+
# Filter out failed transactions
|
157
|
+
valid_transactions = transactions.reject { |t| t["status"] == "failed" }
|
158
|
+
|
159
|
+
# Get unique account names for debug output
|
160
|
+
if verbose
|
161
|
+
accounts = valid_transactions.map { |t| t["accountName"] || "Mercury" }.map { |name| name.gsub(/[^a-zA-Z0-9]/, '-') }.uniq
|
162
|
+
puts "Creating beancount file with the following accounts:"
|
163
|
+
accounts.each do |account|
|
164
|
+
puts "- Assets:#{account}"
|
165
|
+
end
|
166
|
+
end
|
167
|
+
|
168
|
+
valid_transactions.each do |t|
|
169
|
+
date = t["postedAt"] ? Time.parse(t["postedAt"]).strftime("%Y-%m-%d") : Time.parse(t["createdAt"]).strftime("%Y-%m-%d")
|
170
|
+
description = t["bankDescription"] || t["externalMemo"] || "Unknown transaction"
|
171
|
+
amount = t["amount"].to_f
|
172
|
+
account_name = t["accountName"] || "Mercury"
|
173
|
+
account_name = account_name.gsub(/[^a-zA-Z0-9]/, '-')
|
174
|
+
transaction_id = t["id"]
|
175
|
+
|
176
|
+
# Check if this transaction is reconciled
|
177
|
+
is_reconciled = reconciled_transactions.include?(transaction_id)
|
178
|
+
flag = is_reconciled ? "*" : "!"
|
179
|
+
|
180
|
+
# Sanitize description for beancount format
|
181
|
+
description = description.gsub(/[\r\n]+/, ' ').gsub(/"/, '\\"')
|
182
|
+
|
183
|
+
file.puts "#{date} #{flag} \"#{description}\""
|
184
|
+
file.puts " ; Transaction ID: #{transaction_id}"
|
185
|
+
file.puts " ; Status: #{t["status"]}"
|
186
|
+
|
187
|
+
# Add reconciliation metadata
|
188
|
+
if is_reconciled
|
189
|
+
file.puts " reconciled: \"true\""
|
190
|
+
file.puts " reconciled_at: \"#{Time.now.strftime('%Y-%m-%d')}\""
|
191
|
+
else
|
192
|
+
file.puts " reconciled: \"false\""
|
193
|
+
file.puts " reconciled_at: \"\""
|
194
|
+
end
|
195
|
+
|
196
|
+
if amount < 0
|
197
|
+
file.puts " Expenses:Unknown #{format("%.2f", amount.abs)} USD"
|
198
|
+
file.puts " Assets:#{account_name} #{format("%.2f", amount)} USD"
|
199
|
+
else
|
200
|
+
file.puts " Assets:#{account_name} #{format("%.2f", amount)} USD"
|
201
|
+
file.puts " Income:Unknown #{format("%.2f", -amount)} USD"
|
202
|
+
end
|
203
|
+
file.puts
|
204
|
+
end
|
205
|
+
end
|
206
|
+
end
|
207
|
+
|
208
|
+
# Export transactions to hledger format
|
209
|
+
def export_to_hledger(transactions, output_file, reconciled_transactions = [], verbose = false)
|
210
|
+
File.open(output_file, 'w') do |file|
|
211
|
+
file.puts "; Mercury Bank Transactions Export"
|
212
|
+
file.puts "; Generated on #{Time.now.strftime('%Y-%m-%d %H:%M:%S')}"
|
213
|
+
file.puts
|
214
|
+
|
215
|
+
# Filter out failed transactions
|
216
|
+
valid_transactions = transactions.reject { |t| t["status"] == "failed" }
|
217
|
+
|
218
|
+
# Get unique account names for debug output
|
219
|
+
if verbose
|
220
|
+
accounts = valid_transactions.map { |t| t["accountName"] || "Mercury Account" }.uniq
|
221
|
+
puts "Creating hledger file with the following accounts:"
|
222
|
+
accounts.each do |account|
|
223
|
+
puts "- Assets:#{account}"
|
224
|
+
end
|
225
|
+
end
|
226
|
+
|
227
|
+
valid_transactions.each do |t|
|
228
|
+
date = t["postedAt"] ? Time.parse(t["postedAt"]).strftime("%Y-%m-%d") : Time.parse(t["createdAt"]).strftime("%Y-%m-%d")
|
229
|
+
description = t["bankDescription"] || t["externalMemo"] || "Unknown transaction"
|
230
|
+
amount = t["amount"].to_f
|
231
|
+
account_name = t["accountName"] || "Mercury Account"
|
232
|
+
transaction_id = t["id"]
|
233
|
+
|
234
|
+
# Check if this transaction is reconciled
|
235
|
+
is_reconciled = reconciled_transactions.include?(transaction_id)
|
236
|
+
status_marker = is_reconciled ? " * " : " ! "
|
237
|
+
|
238
|
+
# Sanitize description for hledger format
|
239
|
+
description = description.gsub(/[\r\n]+/, ' ').gsub(/;/, '\\;')
|
240
|
+
|
241
|
+
file.puts "#{date}#{status_marker}#{description}"
|
242
|
+
file.puts " ; Transaction ID: #{transaction_id}"
|
243
|
+
file.puts " ; Status: #{t["status"]}"
|
244
|
+
|
245
|
+
# Add reconciliation metadata
|
246
|
+
if is_reconciled
|
247
|
+
file.puts " reconciled: true"
|
248
|
+
file.puts " reconciled-at: #{Time.now.strftime('%Y-%m-%d')}"
|
249
|
+
else
|
250
|
+
file.puts " reconciled: false"
|
251
|
+
file.puts " reconciled-at:"
|
252
|
+
end
|
253
|
+
|
254
|
+
if amount < 0
|
255
|
+
file.puts " Expenses:Unknown $#{format("%.2f", amount.abs)}"
|
256
|
+
file.puts " Assets:#{account_name} $#{format("%.2f", amount)}"
|
257
|
+
else
|
258
|
+
file.puts " Assets:#{account_name} $#{format("%.2f", amount)}"
|
259
|
+
file.puts " Income:Unknown $#{format("%.2f", -amount)}"
|
260
|
+
end
|
261
|
+
file.puts
|
262
|
+
end
|
263
|
+
end
|
264
|
+
end
|
265
|
+
|
266
|
+
# Export transactions to CSV format
|
267
|
+
def export_to_csv(transactions, output_file, reconciled_transactions = [], verbose = false)
|
268
|
+
require 'csv'
|
269
|
+
|
270
|
+
CSV.open(output_file, 'w') do |csv|
|
271
|
+
# Write header
|
272
|
+
csv << ['Date', 'Description', 'Amount', 'Account', 'Transaction ID', 'Status', 'Reconciled', 'Reconciled At']
|
273
|
+
|
274
|
+
# Filter out failed transactions
|
275
|
+
valid_transactions = transactions.reject { |t| t["status"] == "failed" }
|
276
|
+
|
277
|
+
if verbose
|
278
|
+
puts "Creating CSV file with #{valid_transactions.size} transactions"
|
279
|
+
puts "CSV columns: Date, Description, Amount, Account, Transaction ID, Status, Reconciled, Reconciled At"
|
280
|
+
end
|
281
|
+
|
282
|
+
# Write transactions
|
283
|
+
valid_transactions.each do |t|
|
284
|
+
date = t["postedAt"] ? Time.parse(t["postedAt"]).strftime("%Y-%m-%d") : Time.parse(t["createdAt"]).strftime("%Y-%m-%d")
|
285
|
+
description = t["bankDescription"] || t["externalMemo"] || "Unknown transaction"
|
286
|
+
|
287
|
+
# Replace newlines with spaces for CSV format
|
288
|
+
description = description.gsub(/[\r\n]+/, ' ')
|
289
|
+
|
290
|
+
amount = t["amount"].to_f
|
291
|
+
account_name = t["accountName"] || "Mercury Account"
|
292
|
+
transaction_id = t["id"]
|
293
|
+
status = t["status"]
|
294
|
+
|
295
|
+
# Check if this transaction is reconciled
|
296
|
+
is_reconciled = reconciled_transactions.include?(transaction_id)
|
297
|
+
reconciled = is_reconciled ? "Yes" : "No"
|
298
|
+
reconciled_at = is_reconciled ? Time.now.strftime("%Y-%m-%d") : ""
|
299
|
+
|
300
|
+
csv << [date, description, amount, account_name, transaction_id, status, reconciled, reconciled_at]
|
301
|
+
end
|
302
|
+
end
|
303
|
+
end
|
304
|
+
end
|
305
|
+
end
|
306
|
+
end
|
@@ -0,0 +1,133 @@
|
|
1
|
+
module MercuryBanking
|
2
|
+
module Formatters
|
3
|
+
# Formatter for table output
|
4
|
+
module TableFormatter
|
5
|
+
# Display accounts in a table format
|
6
|
+
def display_accounts_table(accounts)
|
7
|
+
return puts "No accounts found." if accounts.empty?
|
8
|
+
|
9
|
+
rows = accounts.map do |a|
|
10
|
+
[
|
11
|
+
a["name"],
|
12
|
+
a["accountNumber"],
|
13
|
+
a["kind"].capitalize,
|
14
|
+
format("$%.2f", a["currentBalance"]),
|
15
|
+
format("$%.2f", a["availableBalance"]),
|
16
|
+
a["status"].capitalize
|
17
|
+
]
|
18
|
+
end
|
19
|
+
|
20
|
+
table = ::Terminal::Table.new(
|
21
|
+
headings: ['Account Name', 'Account Number', 'Type', 'Current Balance', 'Available Balance', 'Status'],
|
22
|
+
rows: rows
|
23
|
+
)
|
24
|
+
|
25
|
+
puts table
|
26
|
+
end
|
27
|
+
|
28
|
+
# Display recipients in a table format
|
29
|
+
def display_recipients_table(recipients)
|
30
|
+
return puts "No recipients found." if recipients.empty?
|
31
|
+
|
32
|
+
rows = recipients.map do |r|
|
33
|
+
account_info = r["electronicRoutingInfo"] || {}
|
34
|
+
address_info = r["address"] || {}
|
35
|
+
|
36
|
+
[
|
37
|
+
r["id"],
|
38
|
+
r["name"],
|
39
|
+
account_info["routingNumber"] || "N/A",
|
40
|
+
mask_account_number(account_info["accountNumber"]),
|
41
|
+
address_info["city"] || "N/A",
|
42
|
+
r["status"].capitalize
|
43
|
+
]
|
44
|
+
end
|
45
|
+
|
46
|
+
table = ::Terminal::Table.new(
|
47
|
+
headings: ['ID', 'Name', 'Routing #', 'Account #', 'City', 'Status'],
|
48
|
+
rows: rows
|
49
|
+
)
|
50
|
+
|
51
|
+
puts table
|
52
|
+
end
|
53
|
+
|
54
|
+
# Display transactions in a table format
|
55
|
+
def display_transactions_table(transactions, show_account_name = false)
|
56
|
+
return puts "No transactions found." if transactions.empty?
|
57
|
+
|
58
|
+
headings = ['Date', 'Amount', 'Type']
|
59
|
+
headings.insert(0, 'Account') if show_account_name
|
60
|
+
headings.concat(['Description', 'Status'])
|
61
|
+
|
62
|
+
rows = transactions.map do |t|
|
63
|
+
date = t["postedAt"] ? Time.parse(t["postedAt"]).strftime("%Y-%m-%d") : "Pending"
|
64
|
+
amount = format("$%.2f", t["amount"].abs)
|
65
|
+
direction = t["amount"] < 0 ? "DEBIT" : "CREDIT"
|
66
|
+
description = t["bankDescription"] || t["externalMemo"] || "N/A"
|
67
|
+
description = description.length > 50 ? "#{description[0..47]}..." : description
|
68
|
+
status = t["status"].capitalize
|
69
|
+
|
70
|
+
row = [date, amount, direction, description, status]
|
71
|
+
row.insert(0, t["accountName"]) if show_account_name
|
72
|
+
row
|
73
|
+
end
|
74
|
+
|
75
|
+
table = ::Terminal::Table.new(
|
76
|
+
headings: headings,
|
77
|
+
rows: rows
|
78
|
+
)
|
79
|
+
|
80
|
+
puts table
|
81
|
+
end
|
82
|
+
|
83
|
+
# Display a single transaction's details
|
84
|
+
def display_transaction_details(transaction)
|
85
|
+
return puts "Transaction not found." unless transaction
|
86
|
+
|
87
|
+
# Format dates
|
88
|
+
created_at = transaction["createdAt"] ? Time.parse(transaction["createdAt"]).strftime("%Y-%m-%d %H:%M:%S") : "N/A"
|
89
|
+
posted_at = transaction["postedAt"] ? Time.parse(transaction["postedAt"]).strftime("%Y-%m-%d %H:%M:%S") : "Pending"
|
90
|
+
estimated_delivery = transaction["estimatedDeliveryDate"] ? Time.parse(transaction["estimatedDeliveryDate"]).strftime("%Y-%m-%d") : "N/A"
|
91
|
+
|
92
|
+
# Format amount
|
93
|
+
amount = transaction["amount"].to_f
|
94
|
+
amount_str = format("$%.2f", amount.abs)
|
95
|
+
direction = amount < 0 ? "DEBIT" : "CREDIT"
|
96
|
+
|
97
|
+
# Get counterparty info
|
98
|
+
counterparty = transaction["counterpartyId"] ? "ID: #{transaction["counterpartyId"]}" : "N/A"
|
99
|
+
|
100
|
+
# Create a table for the transaction details
|
101
|
+
details = [
|
102
|
+
["Transaction ID", transaction["id"]],
|
103
|
+
["Amount", "#{amount_str} (#{direction})"],
|
104
|
+
["Status", transaction["status"].capitalize],
|
105
|
+
["Created At", created_at],
|
106
|
+
["Posted At", posted_at],
|
107
|
+
["Estimated Delivery", estimated_delivery],
|
108
|
+
["Description", transaction["bankDescription"] || "N/A"],
|
109
|
+
["Note", transaction["note"] || "N/A"],
|
110
|
+
["External Memo", transaction["externalMemo"] || "N/A"],
|
111
|
+
["Counterparty", counterparty]
|
112
|
+
]
|
113
|
+
|
114
|
+
table = ::Terminal::Table.new(
|
115
|
+
title: "Transaction Details",
|
116
|
+
rows: details
|
117
|
+
)
|
118
|
+
|
119
|
+
puts table
|
120
|
+
end
|
121
|
+
|
122
|
+
private
|
123
|
+
|
124
|
+
# Mask account number for security
|
125
|
+
def mask_account_number(account_number)
|
126
|
+
return "N/A" unless account_number
|
127
|
+
|
128
|
+
# Show only the last 4 digits
|
129
|
+
"••••#{account_number[-4..-1]}" if account_number.length >= 4
|
130
|
+
end
|
131
|
+
end
|
132
|
+
end
|
133
|
+
end
|
@@ -0,0 +1,135 @@
|
|
1
|
+
module MercuryBanking
|
2
|
+
class Multi
|
3
|
+
def initialize(keys = [])
|
4
|
+
@banks = {}
|
5
|
+
@apis = {}
|
6
|
+
@keys = keys
|
7
|
+
|
8
|
+
@keys.each do |api_key|
|
9
|
+
next if api_key.empty?
|
10
|
+
mercury = MercuryBanking::API.new(api_key)
|
11
|
+
|
12
|
+
begin
|
13
|
+
accounts = mercury.accounts
|
14
|
+
rescue StandardError => e
|
15
|
+
puts e
|
16
|
+
next
|
17
|
+
end
|
18
|
+
|
19
|
+
if accounts.nil?
|
20
|
+
puts "*" * 80
|
21
|
+
puts "* #{api_key} returned no accounts"
|
22
|
+
puts "*" * 80
|
23
|
+
next
|
24
|
+
end
|
25
|
+
|
26
|
+
checking = find_all_checking_accounts(accounts)
|
27
|
+
|
28
|
+
checking.each do |ac|
|
29
|
+
identifier = ac['name']
|
30
|
+
@banks[identifier] = ac
|
31
|
+
end
|
32
|
+
|
33
|
+
@identifier = accounts.first["name"]
|
34
|
+
count_checking_accounts(accounts)
|
35
|
+
@apis[@identifier] = mercury
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def count_checking_accounts(accounts)
|
40
|
+
checking_count = accounts.count { |a| a["kind"] == "checking" }
|
41
|
+
puts "More than one checking account for #{@identifier}: #{checking_count}" if checking_count > 1
|
42
|
+
end
|
43
|
+
|
44
|
+
def find_all_checking_accounts(accounts)
|
45
|
+
accounts.find_all { |a| a["kind"] == "checking" && a["status"] == "active" }
|
46
|
+
end
|
47
|
+
|
48
|
+
def balances
|
49
|
+
table = Terminal::Table.new rows: @banks.collect { |name, vals|
|
50
|
+
[name, vals["availableBalance"], vals["status"]]
|
51
|
+
}
|
52
|
+
print table.to_s + "\n"
|
53
|
+
end
|
54
|
+
|
55
|
+
def ensure_recipient(from:, to:, email:, address:, city:, region:, postal_code:, country:)
|
56
|
+
raise "Target bank account not found: #{to}" unless @banks[to]
|
57
|
+
mercury_source = @apis[from]
|
58
|
+
target = mercury_source.find_recipient(name: @banks[to]["name"])
|
59
|
+
|
60
|
+
if target.nil?
|
61
|
+
target = add_target_account_to_recipient_list(mercury_source, to, email, address, city, region, postal_code, country)
|
62
|
+
end
|
63
|
+
target
|
64
|
+
end
|
65
|
+
|
66
|
+
def add_target_account_to_recipient_list(mercury_source, to, email, address, city, region, postal_code, country)
|
67
|
+
puts "Adding recipient: #{@banks[to]["name"]}"
|
68
|
+
rec = MercuryBanking::Recipient.new(
|
69
|
+
name: @banks[to]["name"],
|
70
|
+
address: address,
|
71
|
+
email: email,
|
72
|
+
account_number: @banks[to]["accountNumber"],
|
73
|
+
routing_number: @banks[to]["routingNumber"],
|
74
|
+
city: city,
|
75
|
+
region: region,
|
76
|
+
postal_code: postal_code,
|
77
|
+
country: country
|
78
|
+
)
|
79
|
+
mercury_source.post(rec.json, "recipients")
|
80
|
+
mercury_source.find_recipient(name: @banks[to]["name"])
|
81
|
+
end
|
82
|
+
|
83
|
+
def ensure_account(account)
|
84
|
+
found = @banks.keys.grep(/#{account}/i)
|
85
|
+
raise "Account: #{account} not found." if found.empty?
|
86
|
+
raise "Account: #{account} matched multiple #{found.join(",")}." if found.size > 1
|
87
|
+
found.first
|
88
|
+
end
|
89
|
+
|
90
|
+
def transfer(from:, to:, amount:, note:, wait: false, email:, address:, city:, region:, postal_code:, country:)
|
91
|
+
puts "#{from} -> #{to} (#{amount})"
|
92
|
+
from_account = ensure_account(from)
|
93
|
+
to_account = ensure_account(to)
|
94
|
+
|
95
|
+
# Create a recipient with "to" information in "from"'s account:
|
96
|
+
recipient = ensure_recipient(
|
97
|
+
from: from_account,
|
98
|
+
to: to_account,
|
99
|
+
email: email,
|
100
|
+
address: address,
|
101
|
+
city: city,
|
102
|
+
region: region,
|
103
|
+
postal_code: postal_code,
|
104
|
+
country: country
|
105
|
+
)
|
106
|
+
|
107
|
+
puts "From account #{@banks[from_account]["id"]} -> Recipient #{recipient["id"]} (account: #{@banks[to_account]["id"]})"
|
108
|
+
transfer = @apis[from_account].transfer(
|
109
|
+
recipient_id: recipient["id"],
|
110
|
+
amount: amount,
|
111
|
+
account_id: @banks[from_account]["id"],
|
112
|
+
note: note,
|
113
|
+
external: "Ex #{note}"
|
114
|
+
)
|
115
|
+
|
116
|
+
if wait
|
117
|
+
while transfer["status"] != "sent"
|
118
|
+
puts "Waiting on #{transfer["id"]} (#{transfer["status"]}) to complete."
|
119
|
+
sleep 10
|
120
|
+
transfer = @apis[from_account].transaction(@banks[from_account]["id"], transfer["id"])
|
121
|
+
if transfer["status"] == "failed"
|
122
|
+
puts transfer
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
write_log(from_account, to_account, amount, note, transfer)
|
128
|
+
transfer
|
129
|
+
end
|
130
|
+
|
131
|
+
def write_log(from, to, amount, note, transfer)
|
132
|
+
File.write("log", "\"#{from}\", \"#{to}\", \"#{amount}\", \"#{note}\", \"#{transfer["status"]}\"\n")
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module MercuryBanking
|
2
|
+
class Recipient
|
3
|
+
attr_accessor :name
|
4
|
+
|
5
|
+
def initialize(name:, email:, account_number:, routing_number:, address:, city:, region:, postal_code:, country:)
|
6
|
+
@name = name
|
7
|
+
@address = {
|
8
|
+
name: name,
|
9
|
+
address1: address,
|
10
|
+
city: city,
|
11
|
+
region: region,
|
12
|
+
postalCode: postal_code,
|
13
|
+
country: country
|
14
|
+
}
|
15
|
+
@emails = [ email ]
|
16
|
+
@paymentMethod = "electronic"
|
17
|
+
@electronicRoutingInfo = {
|
18
|
+
accountNumber: account_number,
|
19
|
+
routingNumber: routing_number,
|
20
|
+
electronicAccountType: "businessChecking",
|
21
|
+
address: @address
|
22
|
+
}
|
23
|
+
end
|
24
|
+
|
25
|
+
def json
|
26
|
+
{ name: @name, address: @address, emails: @emails, paymentMethod: @paymentMethod, electronicRoutingInfo: @electronicRoutingInfo }
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|