Define an endpoint with a global summary of expenses and attendance

This commit is contained in:
Manuel Bustillo 2024-12-11 09:00:45 +01:00
parent 98c1c0d18c
commit 28f1daf1dc
5 changed files with 143 additions and 78 deletions

View File

@ -0,0 +1,27 @@
class SummaryController < ApplicationController
def index
expense_summary = Expenses::TotalQuery.new(wedding: ActsAsTenant.current_tenant).call
render json: {
expenses: {
projected: {
total: expense_summary['total_projected'],
guests: expense_summary['projected_guests']
},
confirmed: {
total: expense_summary['total_confirmed'],
guests: expense_summary['confirmed_guests']
},
status: {
paid: 0
}
},
guests: {
total: 200,
confirmed: 100,
declined: 50,
tentative: 25,
invited: 25
}
}
end
end

View File

@ -2,8 +2,15 @@
module Expenses module Expenses
class TotalQuery class TotalQuery
private attr_reader :wedding
def initialize(wedding:)
@wedding = wedding
end
def call def call
ActiveRecord::Base.connection.execute(query).first ActiveRecord::Base.connection.execute(
ActiveRecord::Base.sanitize_sql_array([query, { wedding_id: wedding.id }])
).first
end end
private private
@ -12,16 +19,10 @@ module Expenses
<<~SQL <<~SQL
WITH guest_count AS (#{guest_count_per_status}), WITH guest_count AS (#{guest_count_per_status}),
expense_summary AS (#{expense_summary}) expense_summary AS (#{expense_summary})
SELECT expense_summary.fixed, SELECT guest_count.confirmed as confirmed_guests,
expense_summary.fixed_count, guest_count.projected as projected_guests,
expense_summary.variable, expense_summary.fixed + expense_summary.variable * guest_count.confirmed as total_confirmed,
expense_summary.variable_count, expense_summary.fixed + expense_summary.variable * guest_count.projected as total_projected
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; FROM guest_count, expense_summary;
SQL SQL
end end
@ -29,20 +30,19 @@ module Expenses
def expense_summary def expense_summary
<<~SQL <<~SQL
SELECT coalesce(sum(amount) filter (where pricing_type = 'fixed'), 0) as fixed, 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(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 FROM expenses
WHERE wedding_id = :wedding_id
SQL SQL
end end
def guest_count_per_status def guest_count_per_status
<<~SQL <<~SQL
SELECT COALESCE(count(*) filter(where status = #{Guest.statuses["confirmed"]}), 0) as confirmed, 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 COALESCE(count(*) filter(where status IN (#{Guest.statuses.values_at('confirmed', 'invited', 'tentative').join(',')})), 0) as projected
FROM guests FROM guests
WHERE wedding_id = :wedding_id
SQL SQL
end end
end end
end end

View File

@ -31,6 +31,7 @@ Rails.application.routes.draw do
get :summary, on: :collection get :summary, on: :collection
end end
resources :tables_arrangements, only: %i[index show] resources :tables_arrangements, only: %i[index show]
resources :summary, only: :index
root to: redirect("/%{slug}") root to: redirect("/%{slug}")
end end

View File

@ -5,88 +5,66 @@ require 'rails_helper'
module Expenses module Expenses
RSpec.describe TotalQuery do RSpec.describe TotalQuery do
describe '#call' do describe '#call' do
let(:response) { described_class.new.call } let(:wedding) { create(:wedding) }
let(:response) { described_class.new(wedding:).call }
before do before do
create_list(:guest, 2, status: :confirmed) create_list(:guest, 2, wedding:, status: :confirmed)
create_list(:guest, 3, status: :considered) create_list(:guest, 3, wedding:, status: :considered)
create_list(:guest, 4, status: :invited) create_list(:guest, 4, wedding:, status: :invited)
create_list(:guest, 5, status: :tentative) create_list(:guest, 5, wedding:, status: :tentative)
create_list(:guest, 6, status: :declined) create_list(:guest, 6, wedding:, status: :declined)
end end
context "when there is no expense" do context 'when there is no expense' do
it "returns zero in all values", :aggregate_failures do it 'returns zero in all values', :aggregate_failures do
expect(response["fixed"]).to be_zero expect(response['total_confirmed']).to be_zero
expect(response["fixed_count"]).to be_zero expect(response['total_projected']).to be_zero
expect(response["variable"]).to be_zero expect(response['confirmed_guests']).to eq(2)
expect(response["variable_count"]).to be_zero expect(response['projected_guests']).to eq(2 + 4 + 5)
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
end end
context "when there are only fixed expenses" do context 'when there are only fixed expenses' do
before do before do
create(:expense, :fixed, amount: 100) create(:expense, :fixed, wedding:, amount: 100)
create(:expense, :fixed, amount: 200) create(:expense, :fixed, wedding:, amount: 200)
end end
it "returns the sum of fixed expenses", :aggregate_failures do it 'returns the sum of fixed expenses', :aggregate_failures do
expect(response["fixed"]).to eq(300) expect(response['total_confirmed']).to eq(300)
expect(response["fixed_count"]).to eq(2) expect(response['total_projected']).to eq(300)
expect(response["variable"]).to be_zero expect(response['confirmed_guests']).to eq(2)
expect(response["variable_count"]).to be_zero expect(response['projected_guests']).to eq(2 + 4 + 5)
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
end end
context "when there are only variable expenses" do context 'when there are only variable expenses' do
before do before do
create(:expense, :per_person, amount: 100) create(:expense, :per_person, wedding:, amount: 100)
create(:expense, :per_person, amount: 200) create(:expense, :per_person, wedding:, amount: 200)
end end
it "returns zero in the values and nonzero in the count", :aggregate_failures do it 'returns zero in the values and nonzero in the count', :aggregate_failures do
expect(response["fixed"]).to be_zero expect(response['total_confirmed']).to eq(2 * 300)
expect(response["fixed_count"]).to be_zero expect(response['total_projected']).to eq(11 * 300)
expect(response["variable"]).to eq(300) expect(response['confirmed_guests']).to eq(2)
expect(response["variable_count"]).to eq(2) expect(response['projected_guests']).to eq(2 + 4 + 5)
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
end end
context "when there are both fixed and variable expenses" do context 'when there are both fixed and variable expenses' do
before do before do
create(:expense, :fixed, amount: 100) create(:expense, :fixed, wedding:, amount: 100)
create(:expense, :fixed, amount: 200) create(:expense, :fixed, wedding:, amount: 200)
create(:expense, :per_person, amount: 50) create(:expense, :per_person, wedding:, amount: 50)
end end
it "returns the sum of fixed and variable expenses", :aggregate_failures do it 'returns the sum of fixed and variable expenses', :aggregate_failures do
expect(response["fixed"]).to eq(300) expect(response['total_confirmed']).to eq(100 + 200 + 50 * 2)
expect(response["fixed_count"]).to eq(2) expect(response['total_projected']).to eq(100 + 200 + 11 * 50)
expect(response["variable"]).to eq(50) expect(response['confirmed_guests']).to eq(2)
expect(response["variable_count"]).to eq(1) expect(response['projected_guests']).to eq(2 + 4 + 5)
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 end

View File

@ -0,0 +1,59 @@
require 'swagger_helper'
RSpec.describe 'summary', type: :request do
path '/{slug}/summary' do
get('list summaries') do
tags 'Summary'
produces 'application/json'
consumes 'application/json'
parameter Swagger::Schema::SLUG
response(200, 'successful') do
schema type: :object,
required: %i[expenses guests],
properties: {
expenses: {
type: :object,
required: %i[projected confirmed status],
properties: {
projected: {
type: :object,
required: %i[total guests],
properties: {
total: { type: :number },
guests: { type: :number }
}
},
confirmed: {
type: :object,
required: %i[total guests],
properties: {
total: { type: :number },
guests: { type: :number }
}
},
status: {
type: :object,
required: [:paid],
properties: {
paid: { type: :number }
}
}
}
},
guests: {
type: :object,
required: %i[total confirmed declined tentative invited],
properties: {
total: { type: :number },
confirmed: { type: :number },
declined: { type: :number },
tentative: { type: :number },
invited: { type: :number }
}
}
}
xit
end
end
end
end