diff --git a/app/models/financial_report.rb b/app/models/financial_report.rb index 2802d08..56849c8 100644 --- a/app/models/financial_report.rb +++ b/app/models/financial_report.rb @@ -20,43 +20,106 @@ def next_year end def checking_data - AccountData.new(FinancialAccount.wf_checking, @year) + accounts = [ + FinancialAccount.usb_checking, + FinancialAccount.wf_checking + ] + AccountData.new(accounts, @year) end def savings_data - AccountData.new(FinancialAccount.wf_savings, @year) + accounts = [ + FinancialAccount.wf_savings + ] + AccountData.new(accounts, @year) end class AccountData - def initialize(account, year) - @account = account + def initialize(accounts, year) + @accounts = accounts @year = year + grouped_statements end def starting_amounts - statements.map(&:starting_amount_cents) + grouped_statements.map do |statements| + amounts = statements.map(&:starting_amount_cents).compact + next if amounts.empty? + + amounts.sum + end + end + + def income_amounts + grouped_transactions.map do |transactions| + amounts = transactions.map do |transaction| + next unless transaction.amount_cents&.positive? + + transaction.amount_cents + end.compact + next if amounts.empty? + + amounts.sum + end + end + + def expenses_amounts + grouped_transactions.map do |transactions| + amounts = transactions.map do |transaction| + next unless transaction.amount_cents&.negative? + + transaction.amount_cents + end.compact + next if amounts.empty? + + amounts.sum + end end def ending_amounts - statements.map(&:ending_amount_cents) + grouped_statements.map do |statements| + amounts = statements.map(&:ending_amount_cents).compact + next if amounts.empty? + + amounts.sum + end end def net_amounts - statements.map(&:net_amount_cents) + grouped_statements.map do |statements| + amounts = statements.map(&:net_amount_cents).compact + next if amounts.empty? + + amounts.sum + end end private - def compute_statements - account_statements = @account.financial_statements.for_year(@year) + def compute_grouped_statements + account_statements = @accounts.map { |account| account.financial_statements.for_year(@year) }.flatten + FinancialReport.months_for_year(@year).map do |date| + statements_for_period = account_statements.select { |statement| statement.period_start_on == date } + statements_for_period << NullStatement.instance if statements_for_period.empty? + statements_for_period + end + end + + def grouped_statements + @grouped_statements ||= compute_grouped_statements + end + + def compute_grouped_transactions + account_transactions = @accounts.map { |account| account.financial_transactions.for_year(@year) }.flatten FinancialReport.months_for_year(@year).map do |date| - statement_for_period = account_statements.find { |statement| statement.period_start_on == date } - statement_for_period || NullStatement.instance + transactions_for_period = account_transactions.select { |transaction| transaction.posted_on.month == date.month } + transactions_for_period << NullTransaction.instance if transactions_for_period.empty? + transactions_for_period end end - def statements - @statements ||= compute_statements + def grouped_transactions + @grouped_transactions ||= compute_grouped_transactions end end end diff --git a/app/models/financial_transaction.rb b/app/models/financial_transaction.rb index eb18edf..d95ff6a 100644 --- a/app/models/financial_transaction.rb +++ b/app/models/financial_transaction.rb @@ -2,4 +2,6 @@ class FinancialTransaction < ApplicationRecord belongs_to :financial_account validates_presence_of :posted_on, :amount_cents, :description + + scope :for_year, ->(year) { where("extract(year from posted_on) = ?", year) } end diff --git a/app/models/null_transaction.rb b/app/models/null_transaction.rb new file mode 100644 index 0000000..968a086 --- /dev/null +++ b/app/models/null_transaction.rb @@ -0,0 +1,6 @@ +class NullTransaction + include Singleton + + def amount_cents + end +end diff --git a/app/views/financial_reports/show.html.haml b/app/views/financial_reports/show.html.haml index b524e63..7016bd1 100644 --- a/app/views/financial_reports/show.html.haml +++ b/app/views/financial_reports/show.html.haml @@ -29,6 +29,12 @@ %tr %td.text-left Starting = render partial: "amount_cents", collection: checking_data.starting_amounts + %tr + %td.text-left Income + = render partial: "amount_cents", collection: checking_data.income_amounts + %tr + %td.text-left Expenses + = render partial: "amount_cents", collection: checking_data.expenses_amounts %tr %td.text-left Ending = render partial: "amount_cents", collection: checking_data.ending_amounts diff --git a/spec/system/financial_reports/admin_views_financial_report_spec.rb b/spec/system/financial_reports/admin_views_financial_report_spec.rb index b66781d..ab4e76d 100644 --- a/spec/system/financial_reports/admin_views_financial_report_spec.rb +++ b/spec/system/financial_reports/admin_views_financial_report_spec.rb @@ -14,8 +14,8 @@ scenario "with no statements for that year" do visit "/financial_reports/#{last_year}" - header = page.find("header nav h1") - expect(header.text).to eq last_year.year.to_s + header_h1 = page.find("header nav h1") + expect(header_h1.text).to eq last_year.year.to_s prev_link, next_link = page.all("header nav a").to_a @@ -25,22 +25,59 @@ expect(next_link.text).to eq "next" expect(next_link["href"]).to end_with((last_year.year + 1).to_s) - page.all("table").to_a.each do |table| - month_row, starting_row, ending_row, net_row = table.all("tr").to_a + checking_table, savings_table = page.all("table").to_a + + within checking_table do + checking_trs = checking_table.all("tr").to_a + + month_tr = checking_trs[0] + starting_tr = checking_trs[1] + income_tr = checking_trs[2] + expenses_tr = checking_trs[3] + ending_tr = checking_trs[4] + net_tr = checking_trs[5] + + expect(month_tr.all("th").map(&:text)).to eq %w[ + Month Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec + ] + + expect(starting_tr.all("td").map(&:text)).to eq %w[ + Starting - - - - - - - - - - - - + ] + + expect(income_tr.all("td").map(&:text)).to eq %w[ + Income - - - - - - - - - - - - + ] + + expect(expenses_tr.all("td").map(&:text)).to eq %w[ + Expenses - - - - - - - - - - - - + ] + + expect(ending_tr.all("td").map(&:text)).to eq %w[ + Ending - - - - - - - - - - - - + ] + + expect(net_tr.all("td").map(&:text)).to eq %w[ + Net - - - - - - - - - - - - + ] + end - expect(month_row.all("th").map(&:text)).to eq %w[ + within savings_table do + month_tr, starting_tr, ending_tr, net_tr = savings_table.all("tr").to_a + + expect(month_tr.all("th").map(&:text)).to eq %w[ Month Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec ] - expect(starting_row.all("td").map(&:text)).to eq %w[ + expect(starting_tr.all("td").map(&:text)).to eq %w[ Starting - - - - - - - - - - - - ] - expect(ending_row.all("td").map(&:text)).to eq %w[ + expect(ending_tr.all("td").map(&:text)).to eq %w[ Ending - - - - - - - - - - - - ] - expect(net_row.all("td").map(&:text)).to eq %w[ + expect(net_tr.all("td").map(&:text)).to eq %w[ Net - - - - - - - - - - - - ] end @@ -58,11 +95,23 @@ visit "/financial_reports/#{last_year}" checking_table = page.all("table").to_a.first - _month_tr, starting_tr, ending_tr, net_tr = checking_table.all("tr").to_a + checking_trs = checking_table.all("tr").to_a + + starting_tr = checking_trs[1] + income_tr = checking_trs[2] + expenses_tr = checking_trs[3] + ending_tr = checking_trs[4] + net_tr = checking_trs[5] may_starting_td = starting_tr.all("td").to_a[5] expect(may_starting_td.text).to eq "$1,034.89" + may_income_td = income_tr.all("td").to_a[5] + expect(may_income_td.text).to eq "-" + + may_expenses_td = expenses_tr.all("td").to_a[5] + expect(may_expenses_td.text).to eq "-" + may_ending_td = ending_tr.all("td").to_a[5] expect(may_ending_td.text).to eq "$99.00" @@ -94,7 +143,11 @@ checking_table, savings_table = page.all("table").to_a within checking_table do - _month_tr, starting_tr, ending_tr, net_tr = checking_table.all("tr").to_a + checking_trs = checking_table.all("tr").to_a + + starting_tr = checking_trs[1] + ending_tr = checking_trs[4] + net_tr = checking_trs[5] _starting_label, *starting_tds = starting_tr.all("td").to_a expect(starting_tds.count).to eq 12 @@ -125,4 +178,90 @@ expect(net_tds.map(&:text).uniq).to eq ["$935.89"] end end + + scenario "with two statements on two accounts for the same period" do + FactoryBot.create( + :financial_statement, + financial_account: FinancialAccount.usb_checking, + period_start_on: Date.parse("#{last_year.year}-05-01"), + starting_amount_cents: 100_00, + ending_amount_cents: 50_00 + ) + + FactoryBot.create( + :financial_statement, + financial_account: FinancialAccount.wf_checking, + period_start_on: Date.parse("#{last_year.year}-05-01"), + starting_amount_cents: 70_00, + ending_amount_cents: 20_00 + ) + + visit "/financial_reports/#{last_year}" + + checking_table = page.all("table").to_a.first + checking_trs = checking_table.all("tr").to_a + + starting_tr = checking_trs[1] + ending_tr = checking_trs[4] + net_tr = checking_trs[5] + + may_starting_td = starting_tr.all("td").to_a[5] + expect(may_starting_td.text).to eq "$170.00" + + may_ending_td = ending_tr.all("td").to_a[5] + expect(may_ending_td.text).to eq "$70.00" + + may_net_td = net_tr.all("td").to_a[5] + expect(may_net_td.text).to eq "($100.00)" + end + + scenario "with one statement in the middle of that year" do + FactoryBot.create( + :financial_statement, + financial_account: FinancialAccount.wf_checking, + period_start_on: Date.parse("#{last_year.year}-05-01"), + starting_amount_cents: 1_034_89, + ending_amount_cents: 99_00 + ) + + FactoryBot.create( + :financial_transaction, + financial_account: FinancialAccount.wf_checking, + posted_on: Date.parse("#{last_year.year}-05-01"), + amount_cents: 900_00 + ) + + FactoryBot.create( + :financial_transaction, + financial_account: FinancialAccount.wf_checking, + posted_on: Date.parse("#{last_year.year}-05-01"), + amount_cents: -1_835_89 + ) + + visit "/financial_reports/#{last_year}" + + checking_table = page.all("table").to_a.first + checking_trs = checking_table.all("tr").to_a + + starting_tr = checking_trs[1] + income_tr = checking_trs[2] + expenses_tr = checking_trs[3] + ending_tr = checking_trs[4] + net_tr = checking_trs[5] + + may_starting_td = starting_tr.all("td").to_a[5] + expect(may_starting_td.text).to eq "$1,034.89" + + may_income_td = income_tr.all("td").to_a[5] + expect(may_income_td.text).to eq "$900.00" + + may_expenses_td = expenses_tr.all("td").to_a[5] + expect(may_expenses_td.text).to eq "($1,835.89)" + + may_ending_td = ending_tr.all("td").to_a[5] + expect(may_ending_td.text).to eq "$99.00" + + may_net_td = net_tr.all("td").to_a[5] + expect(may_net_td.text).to eq "($935.89)" + end end