Module: MercuryBanking::CLI::Reports

Included in:
Main
Defined in:
lib/mercury_banking/cli/reports.rb

Overview

Module for report-related commands

Class Method Summary collapse

Class Method Details

.included(base) ⇒ Object

Add report-related commands to the CLI class



8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
# File 'lib/mercury_banking/cli/reports.rb', line 8

def self.included(base)
  base.class_eval do
    # Methods that should not be exposed as commands
    no_commands do
      # The balance_sheet method has been moved to the Financials module
    end

    desc 'statement ACCOUNT_NAME', 'Generate a statement for a specific account and period'
    method_option :start, type: :string, default: '2020-01-01', desc: 'Start date for statement (YYYY-MM-DD)'
    method_option :end, type: :string, desc: 'End date for statement (YYYY-MM-DD)'
    method_option :format, type: :string, default: 'text', enum: %w[text json csv], desc: 'Output format'
    method_option :save, type: :string, desc: 'Save statement to specified file'
    method_option :debug, type: :boolean, default: false, desc: 'Show debug information about balance calculation'
    def statement()
      with_api_client do |client|
        start_date = options[:start]
        end_date = options[:end]
        format = options[:format]
        debug = options[:debug]

        # Find the account by name
        accounts = client.accounts
         = accounts.find { |a| a["name"] ==  }

        unless 
          puts "Error: Account '#{account_name}' not found. Available accounts:"
          accounts.each { |a| puts "- #{a['name']}" }
          return
        end

        # Get transactions for the account
        transactions = client.get_transactions(["id"], start_date)

        # Filter by end date if specified
        if end_date
          end_date_obj = Date.parse(end_date)
          transactions = transactions.select do |t|
            transaction_date = Date.parse(t["postedAt"] || t["createdAt"])
            transaction_date <= end_date_obj
          end
        end

        if transactions.empty?
          puts "No transactions found for the specified period."
          return
        end

        # Sort transactions by date
        transactions = transactions.sort_by do |t|
          date = t["postedAt"] || t["createdAt"]
          Time.parse(date)
        end

        # Calculate opening and closing balances
        current_balance = ["currentBalance"]
        closing_balance = current_balance

        puts "Debug: Current balance from Mercury API: $#{format('%.2f', current_balance)}" if debug

        # Subtract all transactions that occurred after the end date
        if end_date
          end_date_obj = Date.parse(end_date)
          after_end_date_transactions = transactions.select do |t|
            transaction_date = Date.parse(t["postedAt"] || t["createdAt"])
            transaction_date > end_date_obj
          end

          if debug && after_end_date_transactions.any?
            puts "Debug: Transactions after end date (#{end_date}):"
            after_end_date_transactions.each do |t|
              date = t["postedAt"] ? Time.parse(t["postedAt"]).strftime("%Y-%m-%d") : Time.parse(t["createdAt"]).strftime("%Y-%m-%d")
              puts "  #{date}: $#{format('%.2f', t['amount'])}"
            end
          end

          after_end_date_sum = after_end_date_transactions.sum { |t| t["amount"] }
          closing_balance = current_balance - after_end_date_sum

          if debug
            puts "Debug: Sum of transactions after end date: $#{format('%.2f', after_end_date_sum)}"
            puts "Debug: Closing balance at end date: $#{format('%.2f', closing_balance)}"
          end
        elsif debug
          puts "Debug: No end date specified, closing balance equals current balance"
        end

        # Calculate opening balance by subtracting all transactions in the period
        opening_balance = closing_balance

        puts "Debug: Transactions in the statement period:" if debug

        transactions_sum = 0
        transactions.each do |t|
          amount = t["amount"]
          transactions_sum += amount

          if debug
            date = t["postedAt"] ? Time.parse(t["postedAt"]).strftime("%Y-%m-%d") : Time.parse(t["createdAt"]).strftime("%Y-%m-%d")
            puts "  #{date}: $#{format('%.2f', amount)}"
          end

          opening_balance -= amount
        end

        if debug
          puts "Debug: Sum of transactions in period: $#{format('%.2f', transactions_sum)}"
          puts "Debug: Opening balance calculation: $#{format('%.2f',
                                                              closing_balance)} - $#{format('%.2f',
                                                                                            transactions_sum)} = $#{format(
                                                                                              '%.2f', opening_balance
                                                                                            )}"
        end

        # Generate statement based on format
        case format
        when 'text'
          generate_text_statement(, transactions, opening_balance, closing_balance, start_date, end_date,
                                  options[:save], debug)
        when 'json'
          generate_json_statement(, transactions, opening_balance, closing_balance, start_date, end_date,
                                  options[:save])
        when 'csv'
          generate_csv_statement(, transactions, opening_balance, closing_balance, start_date, end_date,
                                 options[:save])
        end
      end
    end

    private

    def generate_text_statement(, transactions, opening_balance, closing_balance, start_date, end_date,
                                save_path, debug = false)
      # Create statement header
      statement = []
      statement << "Mercury Banking Statement"
      statement << "Account: #{account['name']} (#{account['accountNumber']})"
      statement << "Period: #{start_date} to #{end_date || 'present'}"
      statement << "Generated on: #{Time.now.strftime('%Y-%m-%d %H:%M:%S')}"
      statement << ""
      statement << "Opening Balance: $#{format('%.2f', opening_balance)}"
      statement << ""
      statement << "Transactions:"
      statement << ("-" * 80)
      statement << ("#{'Date'.ljust(12)}#{'Description'.ljust(40)}#{'Amount'.ljust(15)}Balance")
      statement << ("-" * 80)

      # Add transactions
      running_balance = opening_balance
      transactions.each do |t|
        date = t["postedAt"] ? Time.parse(t["postedAt"]).strftime("%Y-%m-%d") : Time.parse(t["createdAt"]).strftime("%Y-%m-%d")
        description = t["bankDescription"] || t["externalMemo"] || "Unknown transaction"
        description = description.length > 37 ? "#{description[0..34]}..." : description
        amount = t["amount"]
        running_balance += amount

        statement << (date.ljust(12) + description.ljust(40) + format("$%.2f",
                                                                      amount).rjust(15) + format("$%.2f",
                                                                                                 running_balance).rjust(15))
      end

      statement << ("-" * 80)
      statement << "Closing Balance: $#{format('%.2f', closing_balance)}"

      if debug
        statement << ""
        statement << "Debug Information:"
        statement << "- Opening balance is calculated by taking the closing balance and subtracting all transactions in the period"
        statement << "- Closing balance is the current balance adjusted for any transactions after the end date"
        statement << "- Running balance starts with the opening balance and adds each transaction amount"
      end

      # Output or save
      if save_path
        File.write(save_path, statement.join("\n"))
        puts "Statement saved to #{save_path}"
      else
        puts statement.join("\n")
      end
    end

    def generate_json_statement(, transactions, opening_balance, closing_balance, start_date, end_date,
                                save_path)
      # Format transactions
      formatted_transactions = transactions.map do |t|
        {
          date: t["postedAt"] || t["createdAt"],
          description: t["bankDescription"] || t["externalMemo"] || "Unknown transaction",
          amount: t["amount"],
          status: t["status"]
        }
      end

      # Create statement object
      statement = {
        account: {
          name: ['name'],
          number: ['accountNumber'],
          type: ['kind']
        },
        period: {
          start_date: start_date,
          end_date: end_date
        },
        balances: {
          opening: opening_balance,
          closing: closing_balance
        },
        transactions: formatted_transactions,
        generated_at: Time.now.iso8601
      }

      # Output or save
      json_output = JSON.pretty_generate(statement)

      if save_path
        File.write(save_path, json_output)
        puts "Statement saved to #{save_path}"
      else
        puts json_output
      end
    end

    def generate_csv_statement(, transactions, opening_balance, closing_balance, start_date, end_date,
                               save_path)
      require 'csv'

      # Prepare CSV data
      csv_data = []

      # Add header row
      csv_data << %w[Date Description Amount Balance Status]

      # Add opening balance row
      csv_data << [start_date, "Opening Balance", "", opening_balance, ""]

      # Add transactions
      running_balance = opening_balance
      transactions.each do |t|
        date = t["postedAt"] ? Time.parse(t["postedAt"]).strftime("%Y-%m-%d") : Time.parse(t["createdAt"]).strftime("%Y-%m-%d")
        description = t["bankDescription"] || t["externalMemo"] || "Unknown transaction"
        amount = t["amount"]
        running_balance += amount

        csv_data << [date, description, amount, running_balance, t["status"]]
      end

      # Add closing balance row
      csv_data << [end_date || Time.now.strftime("%Y-%m-%d"), "Closing Balance", "", closing_balance, ""]

      # Output or save
      csv_output = CSV.generate do |csv|
        csv_data.each { |row| csv << row }
      end

      if save_path
        File.write(save_path, csv_output)
        puts "Statement saved to #{save_path}"
      else
        puts csv_output
      end
    end
  end
end