diff --git a/app/controllers/expenses_controller.rb b/app/controllers/expenses_controller.rb index 774b87f..3d38733 100644 --- a/app/controllers/expenses_controller.rb +++ b/app/controllers/expenses_controller.rb @@ -8,6 +8,10 @@ class ExpensesController < ApplicationController @expenses = Expense.all end + def summary + render json: Expenses::TotalQuery.new.call + end + # GET /expenses/1 or /expenses/1.json def show end diff --git a/app/queries/expenses/total_query.rb b/app/queries/expenses/total_query.rb new file mode 100644 index 0000000..5ed4ca9 --- /dev/null +++ b/app/queries/expenses/total_query.rb @@ -0,0 +1,48 @@ +# Copyright (C) 2024 Manuel Bustillo + +module Expenses + class TotalQuery + def call + ActiveRecord::Base.connection.execute(query).first + end + + private + + def query + <<~SQL + WITH guest_count AS (#{guest_count_per_status}), + expense_summary AS (#{expense_summary}) + SELECT expense_summary.fixed, + expense_summary.fixed_count, + expense_summary.variable, + expense_summary.variable_count, + expense_summary.total_count, + guest_count.confirmed as confirmed_guests, + guest_count.projected as projected_guests, + expense_summary.fixed + expense_summary.variable * guest_count.confirmed as total, + expense_summary.fixed + expense_summary.variable * guest_count.projected as max_projected, + (expense_summary.fixed + expense_summary.variable * guest_count.confirmed) / guest_count.confirmed as per_person + FROM guest_count, expense_summary; + SQL + end + + def expense_summary + <<~SQL + SELECT coalesce(sum(amount) filter (where pricing_type = 'fixed'), 0) as fixed, + coalesce(count(amount) filter (where pricing_type = 'fixed'), 0) as fixed_count, + coalesce(sum(amount) filter (where pricing_type = 'per_person'), 0) as variable, + coalesce(count(amount) filter (where pricing_type = 'per_person'), 0) as variable_count, + count(*) as total_count + FROM expenses + SQL + end + + def guest_count_per_status + <<~SQL + SELECT COALESCE(count(*) filter(where status = #{Guest.statuses["confirmed"]}), 0) as confirmed, + COALESCE(count(*) filter(where status IN (#{Guest.statuses.values_at("confirmed", "invited", "tentative").join(",")})), 0) as projected + FROM guests + SQL + end + end +end \ No newline at end of file diff --git a/config/routes.rb b/config/routes.rb index d96c9c9..a7b4d0c 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -6,7 +6,9 @@ Rails.application.routes.draw do post :import, on: :collection post :bulk_update, on: :collection end - resources :expenses + resources :expenses do + get :summary, on: :collection + end resources :tables_arrangements, only: [:index, :show] get 'up' => 'rails/health#show', as: :rails_health_check diff --git a/spec/factories/expense.rb b/spec/factories/expense.rb new file mode 100644 index 0000000..f9bf6c3 --- /dev/null +++ b/spec/factories/expense.rb @@ -0,0 +1,16 @@ +FactoryBot.define do + factory :expense do + sequence(:name) { |i| "Expense #{i}" } + pricing_type { "fixed" } + amount { 100 } + end + + trait :fixed do + pricing_type { "fixed" } + end + + trait :per_person do + pricing_type { "per_person" } + end + end + \ No newline at end of file diff --git a/spec/queries/expenses/total_query_spec.rb b/spec/queries/expenses/total_query_spec.rb new file mode 100644 index 0000000..9e32716 --- /dev/null +++ b/spec/queries/expenses/total_query_spec.rb @@ -0,0 +1,92 @@ +require 'rails_helper' + +module Expenses + RSpec.describe TotalQuery do + describe '#call' do + let(:response) { described_class.new.call } + + before do + create_list(:guest, 2, status: :confirmed) + create_list(:guest, 3, status: :considered) + create_list(:guest, 4, status: :invited) + create_list(:guest, 5, status: :tentative) + create_list(:guest, 6, status: :declined) + end + + context "when there is no expense" do + it "returns zero in all values", :aggregate_failures do + expect(response["fixed"]).to be_zero + expect(response["fixed_count"]).to be_zero + expect(response["variable"]).to be_zero + expect(response["variable_count"]).to be_zero + expect(response["total"]).to be_zero + expect(response["total_count"]).to be_zero + expect(response["max_projected"]).to be_zero + expect(response["per_person"]).to be_zero + expect(response["confirmed_guests"]).to eq(2) + expect(response["projected_guests"]).to eq(2 + 4 + 5) + end + end + + context "when there are only fixed expenses" do + before do + create(:expense, :fixed, amount: 100) + create(:expense, :fixed, amount: 200) + end + + it "returns the sum of fixed expenses", :aggregate_failures do + expect(response["fixed"]).to eq(300) + expect(response["fixed_count"]).to eq(2) + expect(response["variable"]).to be_zero + expect(response["variable_count"]).to be_zero + expect(response["total"]).to eq(300) + expect(response["total_count"]).to eq(2) + expect(response["max_projected"]).to eq(300) + expect(response["per_person"]).to eq(150) + expect(response["confirmed_guests"]).to eq(2) + expect(response["projected_guests"]).to eq(2 + 4 + 5) + end + end + + context "when there are only variable expenses" do + before do + create(:expense, :per_person, amount: 100) + create(:expense, :per_person, amount: 200) + end + + it "returns zero in the values and nonzero in the count", :aggregate_failures do + expect(response["fixed"]).to be_zero + expect(response["fixed_count"]).to be_zero + expect(response["variable"]).to eq(300) + expect(response["variable_count"]).to eq(2) + expect(response["total"]).to eq(2*300) + expect(response["total_count"]).to eq(2) + expect(response["max_projected"]).to eq(11*300) + expect(response["confirmed_guests"]).to eq(2) + expect(response["projected_guests"]).to eq(2 + 4 + 5) + end + end + + context "when there are both fixed and variable expenses" do + before do + create(:expense, :fixed, amount: 100) + create(:expense, :fixed, amount: 200) + create(:expense, :per_person, amount: 50) + end + + it "returns the sum of fixed and variable expenses", :aggregate_failures do + expect(response["fixed"]).to eq(300) + expect(response["fixed_count"]).to eq(2) + expect(response["variable"]).to eq(50) + expect(response["variable_count"]).to eq(1) + expect(response["total"]).to eq(100 + 200 + 50 * 2) + expect(response["total_count"]).to eq(3) + expect(response["max_projected"]).to eq(100 + 200 + 11*50) + expect(response["per_person"]).to eq(200) + expect(response["confirmed_guests"]).to eq(2) + expect(response["projected_guests"]).to eq(2 + 4 + 5) + end + end + end + end +end