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
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module MercuryBanking
|
2
4
|
module Formatters
|
3
5
|
# Formatter for table output
|
@@ -5,7 +7,7 @@ module MercuryBanking
|
|
5
7
|
# Display accounts in a table format
|
6
8
|
def display_accounts_table(accounts)
|
7
9
|
return puts "No accounts found." if accounts.empty?
|
8
|
-
|
10
|
+
|
9
11
|
rows = accounts.map do |a|
|
10
12
|
[
|
11
13
|
a["name"],
|
@@ -16,23 +18,23 @@ module MercuryBanking
|
|
16
18
|
a["status"].capitalize
|
17
19
|
]
|
18
20
|
end
|
19
|
-
|
21
|
+
|
20
22
|
table = ::Terminal::Table.new(
|
21
23
|
headings: ['Account Name', 'Account Number', 'Type', 'Current Balance', 'Available Balance', 'Status'],
|
22
24
|
rows: rows
|
23
25
|
)
|
24
|
-
|
26
|
+
|
25
27
|
puts table
|
26
28
|
end
|
27
29
|
|
28
30
|
# Display recipients in a table format
|
29
31
|
def display_recipients_table(recipients)
|
30
32
|
return puts "No recipients found." if recipients.empty?
|
31
|
-
|
33
|
+
|
32
34
|
rows = recipients.map do |r|
|
33
35
|
account_info = r["electronicRoutingInfo"] || {}
|
34
36
|
address_info = r["address"] || {}
|
35
|
-
|
37
|
+
|
36
38
|
[
|
37
39
|
r["id"],
|
38
40
|
r["name"],
|
@@ -42,61 +44,61 @@ module MercuryBanking
|
|
42
44
|
r["status"].capitalize
|
43
45
|
]
|
44
46
|
end
|
45
|
-
|
47
|
+
|
46
48
|
table = ::Terminal::Table.new(
|
47
49
|
headings: ['ID', 'Name', 'Routing #', 'Account #', 'City', 'Status'],
|
48
50
|
rows: rows
|
49
51
|
)
|
50
|
-
|
52
|
+
|
51
53
|
puts table
|
52
54
|
end
|
53
|
-
|
55
|
+
|
54
56
|
# Display transactions in a table format
|
55
57
|
def display_transactions_table(transactions, show_account_name = false)
|
56
58
|
return puts "No transactions found." if transactions.empty?
|
57
|
-
|
58
|
-
headings = [
|
59
|
+
|
60
|
+
headings = %w[Date Amount Type]
|
59
61
|
headings.insert(0, 'Account') if show_account_name
|
60
|
-
headings.
|
61
|
-
|
62
|
+
headings.push("Description", "Status")
|
63
|
+
|
62
64
|
rows = transactions.map do |t|
|
63
65
|
date = t["postedAt"] ? Time.parse(t["postedAt"]).strftime("%Y-%m-%d") : "Pending"
|
64
66
|
amount = format("$%.2f", t["amount"].abs)
|
65
|
-
direction = t["amount"]
|
67
|
+
direction = (t["amount"]).negative? ? "DEBIT" : "CREDIT"
|
66
68
|
description = t["bankDescription"] || t["externalMemo"] || "N/A"
|
67
69
|
description = description.length > 50 ? "#{description[0..47]}..." : description
|
68
70
|
status = t["status"].capitalize
|
69
|
-
|
71
|
+
|
70
72
|
row = [date, amount, direction, description, status]
|
71
73
|
row.insert(0, t["accountName"]) if show_account_name
|
72
74
|
row
|
73
75
|
end
|
74
|
-
|
76
|
+
|
75
77
|
table = ::Terminal::Table.new(
|
76
78
|
headings: headings,
|
77
79
|
rows: rows
|
78
80
|
)
|
79
|
-
|
81
|
+
|
80
82
|
puts table
|
81
83
|
end
|
82
|
-
|
84
|
+
|
83
85
|
# Display a single transaction's details
|
84
86
|
def display_transaction_details(transaction)
|
85
87
|
return puts "Transaction not found." unless transaction
|
86
|
-
|
88
|
+
|
87
89
|
# Format dates
|
88
90
|
created_at = transaction["createdAt"] ? Time.parse(transaction["createdAt"]).strftime("%Y-%m-%d %H:%M:%S") : "N/A"
|
89
91
|
posted_at = transaction["postedAt"] ? Time.parse(transaction["postedAt"]).strftime("%Y-%m-%d %H:%M:%S") : "Pending"
|
90
92
|
estimated_delivery = transaction["estimatedDeliveryDate"] ? Time.parse(transaction["estimatedDeliveryDate"]).strftime("%Y-%m-%d") : "N/A"
|
91
|
-
|
93
|
+
|
92
94
|
# Format amount
|
93
95
|
amount = transaction["amount"].to_f
|
94
96
|
amount_str = format("$%.2f", amount.abs)
|
95
|
-
direction = amount
|
96
|
-
|
97
|
+
direction = amount.negative? ? "DEBIT" : "CREDIT"
|
98
|
+
|
97
99
|
# Get counterparty info
|
98
|
-
counterparty = transaction["counterpartyId"] ? "ID: #{transaction[
|
99
|
-
|
100
|
+
counterparty = transaction["counterpartyId"] ? "ID: #{transaction['counterpartyId']}" : "N/A"
|
101
|
+
|
100
102
|
# Create a table for the transaction details
|
101
103
|
details = [
|
102
104
|
["Transaction ID", transaction["id"]],
|
@@ -110,24 +112,24 @@ module MercuryBanking
|
|
110
112
|
["External Memo", transaction["externalMemo"] || "N/A"],
|
111
113
|
["Counterparty", counterparty]
|
112
114
|
]
|
113
|
-
|
115
|
+
|
114
116
|
table = ::Terminal::Table.new(
|
115
117
|
title: "Transaction Details",
|
116
118
|
rows: details
|
117
119
|
)
|
118
|
-
|
120
|
+
|
119
121
|
puts table
|
120
122
|
end
|
121
|
-
|
123
|
+
|
122
124
|
private
|
123
|
-
|
125
|
+
|
124
126
|
# Mask account number for security
|
125
127
|
def mask_account_number(account_number)
|
126
128
|
return "N/A" unless account_number
|
127
|
-
|
129
|
+
|
128
130
|
# Show only the last 4 digits
|
129
|
-
"••••#{account_number[-4
|
131
|
+
"••••#{account_number[-4..]}" if account_number.length >= 4
|
130
132
|
end
|
131
133
|
end
|
132
134
|
end
|
133
|
-
end
|
135
|
+
end
|
@@ -1,35 +1,40 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module MercuryBanking
|
4
|
+
# Multi-account management class
|
5
|
+
# Handles operations across multiple Mercury accounts
|
2
6
|
class Multi
|
3
7
|
def initialize(keys = [])
|
4
8
|
@banks = {}
|
5
9
|
@apis = {}
|
6
10
|
@keys = keys
|
7
|
-
|
11
|
+
|
8
12
|
@keys.each do |api_key|
|
9
13
|
next if api_key.empty?
|
14
|
+
|
10
15
|
mercury = MercuryBanking::API.new(api_key)
|
11
|
-
|
16
|
+
|
12
17
|
begin
|
13
18
|
accounts = mercury.accounts
|
14
19
|
rescue StandardError => e
|
15
20
|
puts e
|
16
21
|
next
|
17
22
|
end
|
18
|
-
|
23
|
+
|
19
24
|
if accounts.nil?
|
20
25
|
puts "*" * 80
|
21
26
|
puts "* #{api_key} returned no accounts"
|
22
27
|
puts "*" * 80
|
23
28
|
next
|
24
29
|
end
|
25
|
-
|
30
|
+
|
26
31
|
checking = find_all_checking_accounts(accounts)
|
27
32
|
|
28
33
|
checking.each do |ac|
|
29
34
|
identifier = ac['name']
|
30
35
|
@banks[identifier] = ac
|
31
36
|
end
|
32
|
-
|
37
|
+
|
33
38
|
@identifier = accounts.first["name"]
|
34
39
|
count_checking_accounts(accounts)
|
35
40
|
@apis[@identifier] = mercury
|
@@ -49,27 +54,29 @@ module MercuryBanking
|
|
49
54
|
table = Terminal::Table.new rows: @banks.collect { |name, vals|
|
50
55
|
[name, vals["availableBalance"], vals["status"]]
|
51
56
|
}
|
52
|
-
print table
|
57
|
+
print "#{table}\n"
|
53
58
|
end
|
54
59
|
|
55
60
|
def ensure_recipient(from:, to:, email:, address:, city:, region:, postal_code:, country:)
|
56
61
|
raise "Target bank account not found: #{to}" unless @banks[to]
|
62
|
+
|
57
63
|
mercury_source = @apis[from]
|
58
64
|
target = mercury_source.find_recipient(name: @banks[to]["name"])
|
59
|
-
|
65
|
+
|
60
66
|
if target.nil?
|
61
|
-
target = add_target_account_to_recipient_list(mercury_source, to, email, address, city, region, postal_code,
|
67
|
+
target = add_target_account_to_recipient_list(mercury_source, to, email, address, city, region, postal_code,
|
68
|
+
country)
|
62
69
|
end
|
63
70
|
target
|
64
71
|
end
|
65
72
|
|
66
73
|
def add_target_account_to_recipient_list(mercury_source, to, email, address, city, region, postal_code, country)
|
67
|
-
puts "Adding recipient: #{@banks[to][
|
74
|
+
puts "Adding recipient: #{@banks[to]['name']}"
|
68
75
|
rec = MercuryBanking::Recipient.new(
|
69
|
-
name: @banks[to]["name"],
|
70
|
-
address: address,
|
71
|
-
email: email,
|
72
|
-
account_number: @banks[to]["accountNumber"],
|
76
|
+
name: @banks[to]["name"],
|
77
|
+
address: address,
|
78
|
+
email: email,
|
79
|
+
account_number: @banks[to]["accountNumber"],
|
73
80
|
routing_number: @banks[to]["routingNumber"],
|
74
81
|
city: city,
|
75
82
|
region: region,
|
@@ -83,53 +90,52 @@ module MercuryBanking
|
|
83
90
|
def ensure_account(account)
|
84
91
|
found = @banks.keys.grep(/#{account}/i)
|
85
92
|
raise "Account: #{account} not found." if found.empty?
|
86
|
-
raise "Account: #{account} matched multiple #{found.join(
|
93
|
+
raise "Account: #{account} matched multiple #{found.join(',')}." if found.size > 1
|
94
|
+
|
87
95
|
found.first
|
88
96
|
end
|
89
97
|
|
90
|
-
def transfer(from:, to:, amount:, note:,
|
98
|
+
def transfer(from:, to:, amount:, note:, email:, address:, city:, region:, postal_code:, country:, wait: false)
|
91
99
|
puts "#{from} -> #{to} (#{amount})"
|
92
100
|
from_account = ensure_account(from)
|
93
101
|
to_account = ensure_account(to)
|
94
|
-
|
102
|
+
|
95
103
|
# Create a recipient with "to" information in "from"'s account:
|
96
104
|
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,
|
105
|
+
from: from_account,
|
106
|
+
to: to_account,
|
107
|
+
email: email,
|
108
|
+
address: address,
|
109
|
+
city: city,
|
110
|
+
region: region,
|
111
|
+
postal_code: postal_code,
|
104
112
|
country: country
|
105
113
|
)
|
106
|
-
|
107
|
-
puts "From account #{@banks[from_account][
|
114
|
+
|
115
|
+
puts "From account #{@banks[from_account]['id']} -> Recipient #{recipient['id']} (account: #{@banks[to_account]['id']})"
|
108
116
|
transfer = @apis[from_account].transfer(
|
109
|
-
recipient_id: recipient["id"],
|
110
|
-
amount: amount,
|
111
|
-
account_id: @banks[from_account]["id"],
|
112
|
-
note: note,
|
117
|
+
recipient_id: recipient["id"],
|
118
|
+
amount: amount,
|
119
|
+
account_id: @banks[from_account]["id"],
|
120
|
+
note: note,
|
113
121
|
external: "Ex #{note}"
|
114
122
|
)
|
115
|
-
|
123
|
+
|
116
124
|
if wait
|
117
125
|
while transfer["status"] != "sent"
|
118
|
-
puts "Waiting on #{transfer[
|
126
|
+
puts "Waiting on #{transfer['id']} (#{transfer['status']}) to complete."
|
119
127
|
sleep 10
|
120
128
|
transfer = @apis[from_account].transaction(@banks[from_account]["id"], transfer["id"])
|
121
|
-
if transfer["status"] == "failed"
|
122
|
-
puts transfer
|
123
|
-
end
|
129
|
+
puts transfer if transfer["status"] == "failed"
|
124
130
|
end
|
125
131
|
end
|
126
|
-
|
132
|
+
|
127
133
|
write_log(from_account, to_account, amount, note, transfer)
|
128
134
|
transfer
|
129
135
|
end
|
130
136
|
|
131
137
|
def write_log(from, to, amount, note, transfer)
|
132
|
-
File.write("log", "\"#{from}\", \"#{to}\", \"#{amount}\", \"#{note}\", \"#{transfer[
|
138
|
+
File.write("log", "\"#{from}\", \"#{to}\", \"#{amount}\", \"#{note}\", \"#{transfer['status']}\"\n")
|
133
139
|
end
|
134
140
|
end
|
135
|
-
end
|
141
|
+
end
|
@@ -1,6 +1,9 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module MercuryBanking
|
4
|
+
# Recipient class for Mercury Banking API
|
2
5
|
class Recipient
|
3
|
-
attr_accessor :name
|
6
|
+
attr_accessor :name, :address, :city, :region, :postal_code, :country, :email
|
4
7
|
|
5
8
|
def initialize(name:, email:, account_number:, routing_number:, address:, city:, region:, postal_code:, country:)
|
6
9
|
@name = name
|
@@ -12,18 +15,23 @@ module MercuryBanking
|
|
12
15
|
postalCode: postal_code,
|
13
16
|
country: country
|
14
17
|
}
|
15
|
-
@emails = [
|
16
|
-
@
|
17
|
-
@
|
18
|
-
accountNumber: account_number,
|
19
|
-
routingNumber: routing_number,
|
18
|
+
@emails = [email]
|
19
|
+
@payment_method = "electronic"
|
20
|
+
@electronic_routing_info = {
|
21
|
+
accountNumber: account_number,
|
22
|
+
routingNumber: routing_number,
|
20
23
|
electronicAccountType: "businessChecking",
|
21
|
-
address: @address
|
24
|
+
address: @address
|
22
25
|
}
|
23
26
|
end
|
24
27
|
|
25
28
|
def json
|
26
|
-
{ name: @name, address: @address, emails: @emails, paymentMethod: @
|
29
|
+
{ name: @name, address: @address, emails: @emails, paymentMethod: @payment_method,
|
30
|
+
electronicRoutingInfo: @electronic_routing_info }
|
31
|
+
end
|
32
|
+
|
33
|
+
def to_json(*_args)
|
34
|
+
json.to_json
|
27
35
|
end
|
28
36
|
end
|
29
|
-
end
|
37
|
+
end
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'json'
|
2
4
|
require 'fileutils'
|
3
5
|
|
@@ -5,100 +7,97 @@ module MercuryBanking
|
|
5
7
|
# Class to manage reconciliation status for transactions
|
6
8
|
class Reconciliation
|
7
9
|
RECONCILIATION_DIR = File.join(Dir.home, '.mercury-banking', 'reconciliation')
|
8
|
-
|
10
|
+
|
9
11
|
def initialize
|
10
12
|
# Ensure reconciliation directory exists
|
11
13
|
FileUtils.mkdir_p(RECONCILIATION_DIR)
|
12
14
|
end
|
13
|
-
|
15
|
+
|
14
16
|
# Get the reconciliation file path for an account
|
15
17
|
def reconciliation_file(account_id)
|
16
18
|
File.join(RECONCILIATION_DIR, "#{account_id}.json")
|
17
19
|
end
|
18
|
-
|
20
|
+
|
19
21
|
# Load reconciled transactions for an account
|
20
22
|
def load_reconciled_transactions(account_id)
|
21
23
|
file_path = reconciliation_file(account_id)
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
else
|
35
|
-
return data
|
36
|
-
end
|
37
|
-
rescue JSON::ParserError
|
38
|
-
# If the file is corrupted, return an empty hash
|
39
|
-
return {}
|
24
|
+
|
25
|
+
return {} unless File.exist?(file_path)
|
26
|
+
|
27
|
+
begin
|
28
|
+
data = JSON.parse(File.read(file_path))
|
29
|
+
# Handle both old format (array of transaction IDs) and new format (hash with dates)
|
30
|
+
return data unless data.is_a?(Array)
|
31
|
+
|
32
|
+
# Convert old format to new format
|
33
|
+
new_data = {}
|
34
|
+
data.each do |transaction_id|
|
35
|
+
new_data[transaction_id] = nil
|
40
36
|
end
|
41
|
-
|
42
|
-
|
43
|
-
return
|
37
|
+
new_data
|
38
|
+
rescue JSON::ParserError
|
39
|
+
# If the file is corrupted, return an empty hash
|
40
|
+
{}
|
44
41
|
end
|
42
|
+
|
43
|
+
# If the file doesn't exist, return an empty hash
|
45
44
|
end
|
46
|
-
|
45
|
+
|
47
46
|
# Save reconciled transactions for an account
|
48
47
|
def save_reconciled_transactions(account_id, transactions)
|
49
48
|
file_path = reconciliation_file(account_id)
|
50
49
|
File.write(file_path, JSON.pretty_generate(transactions))
|
51
50
|
end
|
52
|
-
|
51
|
+
|
53
52
|
# Mark a transaction as reconciled
|
54
53
|
def mark_reconciled(account_id, transaction_id)
|
55
54
|
transactions = load_reconciled_transactions(account_id)
|
56
|
-
|
55
|
+
|
57
56
|
# Add the transaction ID if it's not already in the list
|
58
|
-
if transactions.key?(transaction_id)
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
57
|
+
return false if transactions.key?(transaction_id)
|
58
|
+
|
59
|
+
# Already reconciled
|
60
|
+
|
61
|
+
transactions[transaction_id] = Time.now.strftime('%Y-%m-%d')
|
62
|
+
save_reconciled_transactions(account_id, transactions)
|
63
|
+
true # Successfully reconciled
|
65
64
|
end
|
66
|
-
|
65
|
+
|
67
66
|
# Mark a transaction as unreconciled
|
68
67
|
def mark_unreconciled(account_id, transaction_id)
|
69
68
|
transactions = load_reconciled_transactions(account_id)
|
70
|
-
|
69
|
+
|
71
70
|
# Remove the transaction ID if it's in the list
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
71
|
+
return false unless transactions.key?(transaction_id)
|
72
|
+
|
73
|
+
transactions.delete(transaction_id)
|
74
|
+
save_reconciled_transactions(account_id, transactions)
|
75
|
+
true # Successfully unreconciled
|
76
|
+
|
77
|
+
# Not reconciled to begin with
|
79
78
|
end
|
80
|
-
|
79
|
+
|
81
80
|
# Check if a transaction is reconciled
|
82
81
|
def reconciled?(account_id, transaction_id)
|
83
82
|
transactions = load_reconciled_transactions(account_id)
|
84
83
|
transactions.key?(transaction_id)
|
85
84
|
end
|
86
|
-
|
85
|
+
|
87
86
|
# Get all reconciled transaction IDs for an account
|
88
87
|
def get_reconciled_transactions(account_id)
|
89
88
|
load_reconciled_transactions(account_id).keys
|
90
89
|
end
|
91
|
-
|
90
|
+
|
92
91
|
# Get the reconciliation date for a transaction
|
93
92
|
def get_reconciliation_date(account_id, transaction_id)
|
94
93
|
transactions = load_reconciled_transactions(account_id)
|
95
94
|
transactions[transaction_id]
|
96
95
|
end
|
97
|
-
|
96
|
+
|
98
97
|
# Get reconciliation status for all transactions
|
99
98
|
def get_reconciliation_status(account_id, transactions)
|
100
99
|
reconciled_data = load_reconciled_transactions(account_id)
|
101
|
-
|
100
|
+
|
102
101
|
transactions.map do |t|
|
103
102
|
{
|
104
103
|
transaction_id: t["id"],
|
@@ -110,23 +109,23 @@ module MercuryBanking
|
|
110
109
|
}
|
111
110
|
end
|
112
111
|
end
|
113
|
-
|
112
|
+
|
114
113
|
# Get reconciliation summary
|
115
114
|
def get_reconciliation_summary(account_id, transactions)
|
116
115
|
reconciled_data = load_reconciled_transactions(account_id)
|
117
|
-
|
116
|
+
|
118
117
|
total_transactions = transactions.size
|
119
118
|
reconciled_count = transactions.count { |t| reconciled_data.key?(t["id"]) }
|
120
119
|
unreconciled_count = total_transactions - reconciled_count
|
121
|
-
|
120
|
+
|
122
121
|
reconciled_amount = transactions
|
123
|
-
|
124
|
-
|
125
|
-
|
122
|
+
.select { |t| reconciled_data.key?(t["id"]) }
|
123
|
+
.sum { |t| t["amount"].to_f }
|
124
|
+
|
126
125
|
unreconciled_amount = transactions
|
127
|
-
|
128
|
-
|
129
|
-
|
126
|
+
.reject { |t| reconciled_data.key?(t["id"]) }
|
127
|
+
.sum { |t| t["amount"].to_f }
|
128
|
+
|
130
129
|
{
|
131
130
|
total_transactions: total_transactions,
|
132
131
|
reconciled_count: reconciled_count,
|
@@ -136,4 +135,4 @@ module MercuryBanking
|
|
136
135
|
}
|
137
136
|
end
|
138
137
|
end
|
139
|
-
end
|
138
|
+
end
|