From 38b210610b8c88a466023beedcc2d9dd3ac2fa3e Mon Sep 17 00:00:00 2001 From: Benjamin Randolph <104036158+neb417@users.noreply.github.com> Date: Sat, 18 Nov 2023 15:09:33 -0700 Subject: [PATCH] Savings investing rates (#19) * Add route, controller, and form for saving types * Migrate savings rate * Migrate savings rate * Add savings/investing and render on dashboard * lint * Update factory and test suite * Lint --- app/controllers/concerns/dashboard_builder.rb | 6 + app/controllers/concerns/save_income.rb | 39 ++++++ app/controllers/incomes_controller.rb | 10 +- app/controllers/savings_rates_controller.rb | 74 ++++++++++ app/helpers/savings_rates_helper.rb | 2 + app/models/savings_rate.rb | 31 +++++ app/services/savings_calculator.rb | 52 +++++++ app/views/budget/_hourly_budget.html.erb | 11 +- app/views/budget/_salary_budget.html.erb | 11 +- app/views/dashboard/index.html.erb | 50 ++++++- .../fixed_expenses/update.turbo_stream.erb | 30 +++- app/views/incomes/update.turbo_stream.erb | 29 +++- app/views/savings_rates/_form.html.erb | 27 ++++ .../savings_rates/_savings_rate.html.erb | 17 +++ .../savings_rates/_savings_rate.json.jbuilder | 2 + app/views/savings_rates/edit.html.erb | 8 ++ app/views/savings_rates/index.html.erb | 14 ++ app/views/savings_rates/index.json.jbuilder | 1 + app/views/savings_rates/new.html.erb | 7 + app/views/savings_rates/show.html.erb | 15 ++ app/views/savings_rates/show.json.jbuilder | 1 + .../savings_rates/update.turbo_stream.erb | 27 ++++ app/views/shared/_budget.html.erb | 33 +++-- config/routes.rb | 1 + .../20231117231745_create_savings_rates.rb | 10 ++ db/schema.rb | 9 +- spec/factories/savings_rates.rb | 16 +++ spec/features/dashboard/dashboard_spec.rb | 2 + spec/helpers/savings_rates_helper_spec.rb | 15 ++ spec/models/savings_rate_spec.rb | 15 ++ spec/requests/savings_rates_spec.rb | 130 ++++++++++++++++++ spec/routing/savings_rates_routing_spec.rb | 37 +++++ .../edit.html.tailwindcss_spec.rb | 19 +++ .../index.html.tailwindcss_spec.rb | 23 ++++ .../new.html.tailwindcss_spec.rb | 20 +++ .../show.html.tailwindcss_spec.rb | 16 +++ 36 files changed, 778 insertions(+), 32 deletions(-) create mode 100644 app/controllers/concerns/save_income.rb create mode 100644 app/controllers/savings_rates_controller.rb create mode 100644 app/helpers/savings_rates_helper.rb create mode 100644 app/models/savings_rate.rb create mode 100644 app/services/savings_calculator.rb create mode 100644 app/views/savings_rates/_form.html.erb create mode 100644 app/views/savings_rates/_savings_rate.html.erb create mode 100644 app/views/savings_rates/_savings_rate.json.jbuilder create mode 100644 app/views/savings_rates/edit.html.erb create mode 100644 app/views/savings_rates/index.html.erb create mode 100644 app/views/savings_rates/index.json.jbuilder create mode 100644 app/views/savings_rates/new.html.erb create mode 100644 app/views/savings_rates/show.html.erb create mode 100644 app/views/savings_rates/show.json.jbuilder create mode 100644 app/views/savings_rates/update.turbo_stream.erb create mode 100644 db/migrate/20231117231745_create_savings_rates.rb create mode 100644 spec/factories/savings_rates.rb create mode 100644 spec/helpers/savings_rates_helper_spec.rb create mode 100644 spec/models/savings_rate_spec.rb create mode 100644 spec/requests/savings_rates_spec.rb create mode 100644 spec/routing/savings_rates_routing_spec.rb create mode 100644 spec/views/savings_rates/edit.html.tailwindcss_spec.rb create mode 100644 spec/views/savings_rates/index.html.tailwindcss_spec.rb create mode 100644 spec/views/savings_rates/new.html.tailwindcss_spec.rb create mode 100644 spec/views/savings_rates/show.html.tailwindcss_spec.rb diff --git a/app/controllers/concerns/dashboard_builder.rb b/app/controllers/concerns/dashboard_builder.rb index b387e7f..ae6f97a 100644 --- a/app/controllers/concerns/dashboard_builder.rb +++ b/app/controllers/concerns/dashboard_builder.rb @@ -1,13 +1,19 @@ # frozen_string_literal: true module DashboardBuilder + extend ActiveSupport::Concern include TaxedIncome include TotalCost + include SaveIncome def build_dashboard_variables! @incomes = Income.order_by_type @fixed_expenses = FixedExpense.get_ordered + @savings_rate = SavingsRate.savings + @investing_rate = SavingsRate.investing build_taxed_income_vars! + build_savings_vars! build_total_cost_vars! + Rails.logger.debug "\n *** Building Vars!!\n " end end diff --git a/app/controllers/concerns/save_income.rb b/app/controllers/concerns/save_income.rb new file mode 100644 index 0000000..4e0ffb7 --- /dev/null +++ b/app/controllers/concerns/save_income.rb @@ -0,0 +1,39 @@ +# frozen_string_literal: true + +module SaveIncome + extend ActiveSupport::Concern + include TaxedIncome + + def build_savings_vars! + @hourly_saving = hourly_saving + @hourly_invest = hourly_investing + @salary_saving = salary_saving + @salary_invest = salary_investing + end + + def salary_investing + SavingsCalculator.new(tax_on_salary, set_invest_rate) + end + + def salary_saving + SavingsCalculator.new(tax_on_salary, set_save_rate) + end + + def hourly_investing + SavingsCalculator.new(tax_on_hourly, set_invest_rate) + end + + def hourly_saving + SavingsCalculator.new(tax_on_hourly, set_save_rate) + end + + private + + def set_save_rate + SavingsRate.savings.rate + end + + def set_invest_rate + SavingsRate.investing.rate + end +end diff --git a/app/controllers/incomes_controller.rb b/app/controllers/incomes_controller.rb index aab5e66..8e4501c 100644 --- a/app/controllers/incomes_controller.rb +++ b/app/controllers/incomes_controller.rb @@ -1,6 +1,7 @@ class IncomesController < ApplicationController include DashboardBuilder include TotalCost + include SaveIncome before_action :set_income, only: %i[show edit update destroy] @@ -42,7 +43,6 @@ def update respond_to do |format| if @income.update_from_dashboard(params: params) build_dashboard_variables! - format.html { redirect_to root_path, notice: "Income was successfully updated." } format.turbo_stream else format.html { render :edit, status: :unprocessable_entity } @@ -93,13 +93,17 @@ def income_params params.require(:income).permit(:income_type, :rate, :hours, :weekly_income) end - def build_locals(income) + def build_locals(taxed_income) + income = taxed_income.income build_total_cost_vars! + build_savings_vars! { total_annual_cost: @total_annual_cost, total_monthly_cost: @total_monthly_cost, total_bi_weekly_cost: @total_bi_weekly_cost, - income: income + income: taxed_income, + investing_amount: income.is_hourly? ? @hourly_invest : @salary_invest, + savings_amount: income.is_hourly? ? @hourly_saving : @salary_saving } end end diff --git a/app/controllers/savings_rates_controller.rb b/app/controllers/savings_rates_controller.rb new file mode 100644 index 0000000..be14377 --- /dev/null +++ b/app/controllers/savings_rates_controller.rb @@ -0,0 +1,74 @@ +class SavingsRatesController < ApplicationController + include DashboardBuilder + + before_action :set_savings_rate, only: %i[show edit update destroy] + + # GET /savings_rates or /savings_rates.json + def index + @savings_rates = SavingsRate.all + end + + # GET /savings_rates/1 or /savings_rates/1.json + def show + end + + # GET /savings_rates/new + def new + @savings_rate = SavingsRate.new + end + + # GET /savings_rates/1/edit + def edit + end + + # POST /savings_rates or /savings_rates.json + def create + @savings_rate = SavingsRate.new(savings_rate_params) + + respond_to do |format| + if @savings_rate.save + format.html { redirect_to savings_rate_url(@savings_rate), notice: "Savings rate was successfully created." } + format.json { render :show, status: :created, location: @savings_rate } + else + format.html { render :new, status: :unprocessable_entity } + format.json { render json: @savings_rate.errors, status: :unprocessable_entity } + end + end + end + + # PATCH/PUT /savings_rates/1 or /savings_rates/1.json + def update + respond_to do |format| + if @savings_rate.update_from_dashboard(params: savings_rate_params) + # calculate new savings amounts for both hourly and salary and new guilt free + build_dashboard_variables! + format.turbo_stream + else + format.html { render :edit, status: :unprocessable_entity } + format.json { render json: @savings_rate.errors, status: :unprocessable_entity } + end + end + end + + # DELETE /savings_rates/1 or /savings_rates/1.json + def destroy + @savings_rate.destroy + + respond_to do |format| + format.html { redirect_to savings_rates_url, notice: "Savings rate was successfully destroyed." } + format.json { head :no_content } + end + end + + private + + # Use callbacks to share common setup or constraints between actions. + def set_savings_rate + @savings_rate = SavingsRate.find(params[:id]) + end + + # Only allow a list of trusted parameters through. + def savings_rate_params + params.require(:savings_rate).permit(:name, :rate) + end +end diff --git a/app/helpers/savings_rates_helper.rb b/app/helpers/savings_rates_helper.rb new file mode 100644 index 0000000..7635e12 --- /dev/null +++ b/app/helpers/savings_rates_helper.rb @@ -0,0 +1,2 @@ +module SavingsRatesHelper +end diff --git a/app/models/savings_rate.rb b/app/models/savings_rate.rb new file mode 100644 index 0000000..08c932a --- /dev/null +++ b/app/models/savings_rate.rb @@ -0,0 +1,31 @@ +# == Schema Information +# +# Table name: savings_rates +# +# id :bigint not null, primary key +# name :string +# rate :float +# created_at :datetime not null +# updated_at :datetime not null +# +class SavingsRate < ApplicationRecord + SAVING_TYPES = %w[savings investing] + validates :name, presence: true, inclusion: SAVING_TYPES + + def self.savings + find_by(name: "savings") + end + + def self.investing + find_by(name: "investing") + end + + def display_rate + (rate * 100) + end + + def update_from_dashboard(params:) + rate = params[:rate].to_f / 100 + update(rate: rate) + end +end diff --git a/app/services/savings_calculator.rb b/app/services/savings_calculator.rb new file mode 100644 index 0000000..7cfe974 --- /dev/null +++ b/app/services/savings_calculator.rb @@ -0,0 +1,52 @@ +# frozen_string_literal: true + +class SavingsCalculator + attr_reader :saving_amount, + :annual_saving, + :biannual_saving, + :quarterly_saving, + :monthly_saving, + :bi_weekly_saving, + :weekly_saving, + :daily_saving + + def initialize(income_type, saving_rate) + @annual_income = income_type.annual_income + @saving_rate = saving_rate + @annual_saving = calculate_savings + @bi_weekly_saving = calculate_bi_weekly_saving + @daily_saving = calculate_daily_saving + @weekly_saving = calculate_weekly_saving + @monthly_saving = calculate_monthly_saving + @quarterly_saving = calculate_quarterly_saving + @biannual_saving = calculate_biannual_saving + end + + def calculate_savings + @annual_income * @saving_rate + end + + def calculate_daily_saving + @annual_saving / 365 + end + + def calculate_weekly_saving + @annual_saving / 52 + end + + def calculate_bi_weekly_saving + @annual_saving / 26 + end + + def calculate_monthly_saving + @annual_saving / 12 + end + + def calculate_quarterly_saving + @annual_saving / 4 + end + + def calculate_biannual_saving + @annual_saving / 2 + end +end diff --git a/app/views/budget/_hourly_budget.html.erb b/app/views/budget/_hourly_budget.html.erb index 9d9007d..bd5b031 100644 --- a/app/views/budget/_hourly_budget.html.erb +++ b/app/views/budget/_hourly_budget.html.erb @@ -1,7 +1,16 @@ <%= turbo_frame_tag "hourly_budget" do %>
+ Hourly <%= render partial: "budget/budget_headings" %> - <%= render partial: "shared/budget", locals: { total_annual_cost: total_annual_cost, total_monthly_cost: total_monthly_cost, total_bi_weekly_cost: total_bi_weekly_cost, income: income } %> + <%= render partial: "shared/budget", + locals: { + total_annual_cost: total_annual_cost, + total_monthly_cost: total_monthly_cost, + total_bi_weekly_cost: total_bi_weekly_cost, + income: income, + investing_amount: investing_amount, + savings_amount: savings_amount + } %>
<% end %> \ No newline at end of file diff --git a/app/views/budget/_salary_budget.html.erb b/app/views/budget/_salary_budget.html.erb index 939cc68..d6f13e0 100644 --- a/app/views/budget/_salary_budget.html.erb +++ b/app/views/budget/_salary_budget.html.erb @@ -2,6 +2,15 @@
Salary <%= render partial: "budget/budget_headings" %> - <%= render partial: "shared/budget", locals: { total_annual_cost: total_annual_cost, total_monthly_cost: total_monthly_cost, total_bi_weekly_cost: total_bi_weekly_cost, income: income } %> + <% Rails.logger.debug "*** Salary_budget #{savings_amount.daily_saving}" %> + <%= render partial: "shared/budget", + locals: { + total_annual_cost: total_annual_cost, + total_monthly_cost: total_monthly_cost, + total_bi_weekly_cost: total_bi_weekly_cost, + income: income, + investing_amount: investing_amount, + savings_amount: savings_amount + } %>
<% end %> diff --git a/app/views/dashboard/index.html.erb b/app/views/dashboard/index.html.erb index cf1aac1..2e6eccf 100644 --- a/app/views/dashboard/index.html.erb +++ b/app/views/dashboard/index.html.erb @@ -7,12 +7,45 @@
-

Final Budget

-
- <%= render partial: "components/income_switch" %> +
+

Final Budget

+
Savings Rate %
+
Investing Rate %
+
+
+
+ <%= render partial: "components/income_switch" %> +
+ +
+ <%= form_with model: @savings_rate, local: true do |form| %> +
+ <%= form.number_field :rate, in: 0.00..50.00, step: 0.25, value: @savings_rate.display_rate, class: "block shadow rounded-md border border-gray-200 outline-none px-3 py-2 mt-2 w-1/4 h-7" %> + <%= form.submit "Save", class:"btn btn-primary ml-4 mt-2 py-0.5 my-auto"%> +
+ <% end %> +
+ +
+ <%= form_with model: @investing_rate, local: true do |form| %> +
+ <%= form.number_field :rate, in: 0.00..50.00, step: 0.25, value: @investing_rate.display_rate, class: "block shadow rounded-md border border-gray-200 outline-none px-3 py-2 mt-2 w-1/4 h-7" %> + <%= form.submit "Invest", class:"btn btn-primary ml-4 mt-2 py-0.5 my-auto"%> +
+ <% end %> +
+
- <%= render partial: "budget/salary_budget", locals: { total_annual_cost: @total_annual_cost, total_monthly_cost: @total_monthly_cost, total_bi_weekly_cost: @total_bi_weekly_cost, income: @salary_taxed } %> + <%= render partial: "budget/salary_budget", + locals: { + total_annual_cost: @total_annual_cost, + total_monthly_cost: @total_monthly_cost, + total_bi_weekly_cost: @total_bi_weekly_cost, income: @salary_taxed, + investing_amount: @salary_invest, + savings_amount: @salary_saving + } + %>
@@ -32,7 +65,8 @@
<%= turbo_frame_tag "taxed_incomes" do %> - <%= render partial: "shared/taxed_incomes", locals: { salary_taxed: @salary_taxed, hourly_taxed: @hourly_taxed} %> + <%= render partial: "shared/taxed_incomes", + locals: { salary_taxed: @salary_taxed, hourly_taxed: @hourly_taxed} %> <% end %>
@@ -50,7 +84,11 @@ <% end %> <%= turbo_frame_tag "total_costs" do %> - <%= render partial: "shared/total_costs", locals: { total_annual_cost: @total_annual_cost, total_monthly_cost: @total_monthly_cost, total_bi_weekly_cost: @total_bi_weekly_cost } %> + <%= render partial: "shared/total_costs", + locals: { + total_annual_cost: @total_annual_cost, + total_monthly_cost: @total_monthly_cost, + total_bi_weekly_cost: @total_bi_weekly_cost } %> <% end %> diff --git a/app/views/fixed_expenses/update.turbo_stream.erb b/app/views/fixed_expenses/update.turbo_stream.erb index b431b7a..770766e 100644 --- a/app/views/fixed_expenses/update.turbo_stream.erb +++ b/app/views/fixed_expenses/update.turbo_stream.erb @@ -1,13 +1,37 @@ <%= turbo_stream.update @fixed_expense %> <%= turbo_stream.replace "total_costs" do %> - <%= render partial: "shared/total_costs", locals: { total_annual_cost: @total_annual_cost, total_monthly_cost: @total_monthly_cost, total_bi_weekly_cost: @total_bi_weekly_cost } %> + <%= render partial: "shared/total_costs", + locals: { + total_annual_cost: @total_annual_cost, + total_monthly_cost: @total_monthly_cost, + total_bi_weekly_cost: @total_bi_weekly_cost + } + %> <% end %> <%= turbo_stream.replace "salary_budget" do %> - <%= render partial: "budget/salary_budget", locals: {total_annual_cost: @total_annual_cost, total_monthly_cost: @total_monthly_cost, total_bi_weekly_cost: @total_bi_weekly_cost, income: @salary_taxed} %> + <%= render partial: "budget/salary_budget", + locals: { + total_annual_cost: @total_annual_cost, + total_monthly_cost: @total_monthly_cost, + total_bi_weekly_cost: @total_bi_weekly_cost, + income: @salary_taxed, + investing_amount: @salary_invest, + savings_amount: @salary_saving + } + %> <% end %> <%= turbo_stream.replace "hourly_budget" do %> - <%= render partial: "budget/hourly_budget", locals: {total_annual_cost: @total_annual_cost, total_monthly_cost: @total_monthly_cost, total_bi_weekly_cost: @total_bi_weekly_cost, income: @hourly_taxed} %> + <%= render partial: "budget/hourly_budget", + locals: { + total_annual_cost: @total_annual_cost, + total_monthly_cost: @total_monthly_cost, + total_bi_weekly_cost: @total_bi_weekly_cost, + income: @hourly_taxed, + investing_amount: @hourly_invest, + savings_amount: @hourly_saving + } + %> <% end %> diff --git a/app/views/incomes/update.turbo_stream.erb b/app/views/incomes/update.turbo_stream.erb index 435496e..b8aa8ff 100644 --- a/app/views/incomes/update.turbo_stream.erb +++ b/app/views/incomes/update.turbo_stream.erb @@ -1,13 +1,36 @@ <%= turbo_stream.update @income %> <%= turbo_stream.replace "taxed_incomes" do %> - <%= render partial: "shared/taxed_incomes", locals: { salary_taxed: @salary_taxed, hourly_taxed: @hourly_taxed } %> + <%= render partial: "shared/taxed_incomes", + locals: { + salary_taxed: @salary_taxed, + hourly_taxed: @hourly_taxed + } + %> <% end %> <%= turbo_stream.replace "salary_budget" do %> - <%= render partial: "budget/salary_budget", locals: {total_annual_cost: @total_annual_cost, total_monthly_cost: @total_monthly_cost, total_bi_weekly_cost: @total_bi_weekly_cost, income: @salary_taxed} %> + <%= render partial: "budget/salary_budget", + locals: { + total_annual_cost: @total_annual_cost, + total_monthly_cost: @total_monthly_cost, + total_bi_weekly_cost: @total_bi_weekly_cost, + income: @salary_taxed, + investing_amount: @salary_invest, + savings_amount: @salary_saving + } + %> <% end %> <%= turbo_stream.replace "hourly_budget" do %> - <%= render partial: "budget/hourly_budget", locals: {total_annual_cost: @total_annual_cost, total_monthly_cost: @total_monthly_cost, total_bi_weekly_cost: @total_bi_weekly_cost, income: @hourly_taxed} %> + <%= render partial: "budget/hourly_budget", + locals: { + total_annual_cost: @total_annual_cost, + total_monthly_cost: @total_monthly_cost, + total_bi_weekly_cost: @total_bi_weekly_cost, + income: @hourly_taxed, + investing_amount: @hourly_invest, + savings_amount: @hourly_saving + } + %> <% end %> \ No newline at end of file diff --git a/app/views/savings_rates/_form.html.erb b/app/views/savings_rates/_form.html.erb new file mode 100644 index 0000000..f1cc3f2 --- /dev/null +++ b/app/views/savings_rates/_form.html.erb @@ -0,0 +1,27 @@ +<%= form_with(model: savings_rate, class: "contents") do |form| %> + <% if savings_rate.errors.any? %> +
+

<%= pluralize(savings_rate.errors.count, "error") %> prohibited this savings_rate from being saved:

+ + +
+ <% end %> + +
+ <%= form.label :name %> + <%= form.text_field :name, class: "block shadow rounded-md border border-gray-200 outline-none px-3 py-2 mt-2 w-full" %> +
+ +
+ <%= form.label :rate %> + <%= form.text_field :rate, class: "block shadow rounded-md border border-gray-200 outline-none px-3 py-2 mt-2 w-full" %> +
+ +
+ <%= form.submit class: "rounded-lg py-3 px-5 bg-blue-600 text-white inline-block font-medium cursor-pointer" %> +
+<% end %> diff --git a/app/views/savings_rates/_savings_rate.html.erb b/app/views/savings_rates/_savings_rate.html.erb new file mode 100644 index 0000000..129ad18 --- /dev/null +++ b/app/views/savings_rates/_savings_rate.html.erb @@ -0,0 +1,17 @@ +
+

+ Name: + <%= savings_rate.name %> +

+ +

+ Rate: + <%= savings_rate.rate %> +

+ + <% if action_name != "show" %> + <%= link_to "Show this savings rate", savings_rate, class: "rounded-lg py-3 px-5 bg-gray-100 inline-block font-medium" %> + <%= link_to 'Edit this savings rate', edit_savings_rate_path(savings_rate), class: "rounded-lg py-3 ml-2 px-5 bg-gray-100 inline-block font-medium" %> +
+ <% end %> +
diff --git a/app/views/savings_rates/_savings_rate.json.jbuilder b/app/views/savings_rates/_savings_rate.json.jbuilder new file mode 100644 index 0000000..0d63a68 --- /dev/null +++ b/app/views/savings_rates/_savings_rate.json.jbuilder @@ -0,0 +1,2 @@ +json.extract! savings_rate, :id, :name, :rate, :created_at, :updated_at +json.url savings_rate_url(savings_rate, format: :json) diff --git a/app/views/savings_rates/edit.html.erb b/app/views/savings_rates/edit.html.erb new file mode 100644 index 0000000..51e011c --- /dev/null +++ b/app/views/savings_rates/edit.html.erb @@ -0,0 +1,8 @@ +
+

Editing savings rate

+ + <%= render "form", savings_rate: @savings_rate %> + + <%= link_to "Show this savings rate", @savings_rate, class: "ml-2 rounded-lg py-3 px-5 bg-gray-100 inline-block font-medium" %> + <%= link_to "Back to savings rates", savings_rates_path, class: "ml-2 rounded-lg py-3 px-5 bg-gray-100 inline-block font-medium" %> +
diff --git a/app/views/savings_rates/index.html.erb b/app/views/savings_rates/index.html.erb new file mode 100644 index 0000000..66414e1 --- /dev/null +++ b/app/views/savings_rates/index.html.erb @@ -0,0 +1,14 @@ +
+ <% if notice.present? %> +

<%= notice %>

+ <% end %> + +
+

Savings rates

+ <%= link_to 'New savings rate', new_savings_rate_path, class: "rounded-lg py-3 px-5 bg-blue-600 text-white block font-medium" %> +
+ +
+ <%= render @savings_rates %> +
+
diff --git a/app/views/savings_rates/index.json.jbuilder b/app/views/savings_rates/index.json.jbuilder new file mode 100644 index 0000000..b3f558b --- /dev/null +++ b/app/views/savings_rates/index.json.jbuilder @@ -0,0 +1 @@ +json.array! @savings_rates, partial: "savings_rates/savings_rate", as: :savings_rate diff --git a/app/views/savings_rates/new.html.erb b/app/views/savings_rates/new.html.erb new file mode 100644 index 0000000..0402756 --- /dev/null +++ b/app/views/savings_rates/new.html.erb @@ -0,0 +1,7 @@ +
+

New savings rate

+ + <%= render "form", savings_rate: @savings_rate %> + + <%= link_to 'Back to savings rates', savings_rates_path, class: "ml-2 rounded-lg py-3 px-5 bg-gray-100 inline-block font-medium" %> +
diff --git a/app/views/savings_rates/show.html.erb b/app/views/savings_rates/show.html.erb new file mode 100644 index 0000000..b26171e --- /dev/null +++ b/app/views/savings_rates/show.html.erb @@ -0,0 +1,15 @@ +
+
+ <% if notice.present? %> +

<%= notice %>

+ <% end %> + + <%= render @savings_rate %> + + <%= link_to 'Edit this savings_rate', edit_savings_rate_path(@savings_rate), class: "mt-2 rounded-lg py-3 px-5 bg-gray-100 inline-block font-medium" %> +
+ <%= button_to 'Destroy this savings_rate', savings_rate_path(@savings_rate), method: :delete, class: "mt-2 rounded-lg py-3 px-5 bg-gray-100 font-medium" %> +
+ <%= link_to 'Back to savings_rates', savings_rates_path, class: "ml-2 rounded-lg py-3 px-5 bg-gray-100 inline-block font-medium" %> +
+
diff --git a/app/views/savings_rates/show.json.jbuilder b/app/views/savings_rates/show.json.jbuilder new file mode 100644 index 0000000..1ce72f9 --- /dev/null +++ b/app/views/savings_rates/show.json.jbuilder @@ -0,0 +1 @@ +json.partial! "savings_rates/savings_rate", savings_rate: @savings_rate diff --git a/app/views/savings_rates/update.turbo_stream.erb b/app/views/savings_rates/update.turbo_stream.erb new file mode 100644 index 0000000..b4779e5 --- /dev/null +++ b/app/views/savings_rates/update.turbo_stream.erb @@ -0,0 +1,27 @@ +<%= turbo_stream.update @savings_rate %> + +<%= turbo_stream.replace "salary_budget" do %> + <%= render partial: "budget/salary_budget", + locals: { + total_annual_cost: @total_annual_cost, + total_monthly_cost: @total_monthly_cost, + total_bi_weekly_cost: @total_bi_weekly_cost, + income: @salary_taxed, + investing_amount: @salary_invest, + savings_amount: @salary_saving + } + %> +<% end %> + +<%= turbo_stream.replace "hourly_budget" do %> + <%= render partial: "budget/hourly_budget", + locals: { + total_annual_cost: @total_annual_cost, + total_monthly_cost: @total_monthly_cost, + total_bi_weekly_cost: @total_bi_weekly_cost, + income: @hourly_taxed, + investing_amount: @hourly_invest, + savings_amount: @hourly_saving + } + %> +<% end %> \ No newline at end of file diff --git a/app/views/shared/_budget.html.erb b/app/views/shared/_budget.html.erb index 7a1eb2a..e42b434 100644 --- a/app/views/shared/_budget.html.erb +++ b/app/views/shared/_budget.html.erb @@ -1,8 +1,13 @@
Daily
+ <%= render partial: "shared/total", locals: { total: total_annual_cost / 365 } %> -
0
-
0
+
+ <%= humanized_money_with_symbol(investing_amount.daily_saving) %> +
+
+ <%= humanized_money_with_symbol(savings_amount.daily_saving) %> +
<%= humanized_money_with_symbol((income.daily_income) - total_annual_cost / 365) %>
@@ -11,8 +16,8 @@
Weekly
<%= render partial: "shared/total", locals: { total: total_annual_cost / 52 } %> -
0
-
0
+
<%= humanized_money_with_symbol(investing_amount.weekly_saving) %>
+
<%= humanized_money_with_symbol(savings_amount.weekly_saving) %>
<%= humanized_money_with_symbol((income.weekly_income) - total_annual_cost / 52) %>
@@ -21,8 +26,8 @@
Bi-Weekly
<%= render partial: "shared/total", locals: { total: total_bi_weekly_cost } %> -
0
-
0
+
<%= humanized_money_with_symbol(investing_amount.bi_weekly_saving) %>
+
<%= humanized_money_with_symbol(savings_amount.bi_weekly_saving) %>
<%= humanized_money_with_symbol((income.bi_weekly_net_income) - total_bi_weekly_cost) %>
@@ -31,8 +36,8 @@
Monthly
<%= render partial: "shared/total", locals: { total: total_monthly_cost } %> -
0
-
0
+
<%= humanized_money_with_symbol(investing_amount.monthly_saving) %>
+
<%= humanized_money_with_symbol(savings_amount.monthly_saving) %>
<%= humanized_money_with_symbol((income.monthly_income) - total_monthly_cost) %>
@@ -41,8 +46,8 @@
Quarterly
<%= render partial: "shared/total", locals: { total: total_annual_cost / 4 } %> -
0
-
0
+
<%= humanized_money_with_symbol(investing_amount.quarterly_saving) %>
+
<%= humanized_money_with_symbol(savings_amount.quarterly_saving) %>
<%= humanized_money_with_symbol((income.quarterly_income) - total_annual_cost / 4) %>
@@ -51,8 +56,8 @@
Biannual
<%= render partial: "shared/total", locals: { total: total_annual_cost / 2 } %> -
0
-
0
+
<%= humanized_money_with_symbol(investing_amount.biannual_saving) %>
+
<%= humanized_money_with_symbol(savings_amount.biannual_saving) %>
<%= humanized_money_with_symbol((income.biannual_income) - total_annual_cost / 2) %>
@@ -62,8 +67,8 @@
Annually
<%= render partial: "shared/total", locals: { total: total_annual_cost } %> -
0
-
0
+
<%= humanized_money_with_symbol(investing_amount.annual_saving) %>
+
<%= humanized_money_with_symbol(savings_amount.annual_saving) %>
<%= humanized_money_with_symbol(income.total_net_income - total_annual_cost ) %>
diff --git a/config/routes.rb b/config/routes.rb index d07e0e3..0fca9f2 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -1,4 +1,5 @@ Rails.application.routes.draw do + resources :savings_rates resources :federal_tax_brackets resources :fixed_expenses root "dashboard#index" diff --git a/db/migrate/20231117231745_create_savings_rates.rb b/db/migrate/20231117231745_create_savings_rates.rb new file mode 100644 index 0000000..f26953d --- /dev/null +++ b/db/migrate/20231117231745_create_savings_rates.rb @@ -0,0 +1,10 @@ +class CreateSavingsRates < ActiveRecord::Migration[7.0] + def change + create_table :savings_rates do |t| + t.string :name + t.float :rate + + t.timestamps + end + end +end diff --git a/db/schema.rb b/db/schema.rb index 4c2a184..98d2dd2 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[7.0].define(version: 2023_06_11_195916) do +ActiveRecord::Schema[7.0].define(version: 2023_11_17_231745) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -50,4 +50,11 @@ t.datetime "updated_at", null: false end + create_table "savings_rates", force: :cascade do |t| + t.string "name" + t.float "rate" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + end + end diff --git a/spec/factories/savings_rates.rb b/spec/factories/savings_rates.rb new file mode 100644 index 0000000..b1b36d7 --- /dev/null +++ b/spec/factories/savings_rates.rb @@ -0,0 +1,16 @@ +# == Schema Information +# +# Table name: savings_rates +# +# id :bigint not null, primary key +# name :string +# rate :float +# created_at :datetime not null +# updated_at :datetime not null +# +FactoryBot.define do + factory :savings_rate do + name { "savings" } + rate { 1.5 } + end +end diff --git a/spec/features/dashboard/dashboard_spec.rb b/spec/features/dashboard/dashboard_spec.rb index 6c1fdf1..7283ba2 100644 --- a/spec/features/dashboard/dashboard_spec.rb +++ b/spec/features/dashboard/dashboard_spec.rb @@ -7,6 +7,8 @@ let!(:fed2) { create(:federal_tax_bracket, :tier_2) } let!(:fed3) { create(:federal_tax_bracket, :tier_3) } let!(:fixed_expenses) { create_list(:fixed_expense, 2) } + let!(:saving_rate) { create(:savings_rate) } + let!(:investing_rate) { create(:savings_rate, name: "investing", rate: 0.02) } describe "GET /index" do it "routes to root path" do diff --git a/spec/helpers/savings_rates_helper_spec.rb b/spec/helpers/savings_rates_helper_spec.rb new file mode 100644 index 0000000..2f2bf94 --- /dev/null +++ b/spec/helpers/savings_rates_helper_spec.rb @@ -0,0 +1,15 @@ +require "rails_helper" + +# Specs in this file have access to a helper object that includes +# the SavingsRatesHelper. For example: +# +# describe SavingsRatesHelper do +# describe "string concat" do +# it "concats two strings with spaces" do +# expect(helper.concat_strings("this","that")).to eq("this that") +# end +# end +# end +RSpec.describe SavingsRatesHelper, type: :helper do + pending "add some examples to (or delete) #{__FILE__}" +end diff --git a/spec/models/savings_rate_spec.rb b/spec/models/savings_rate_spec.rb new file mode 100644 index 0000000..7928deb --- /dev/null +++ b/spec/models/savings_rate_spec.rb @@ -0,0 +1,15 @@ +# == Schema Information +# +# Table name: savings_rates +# +# id :bigint not null, primary key +# name :string +# rate :float +# created_at :datetime not null +# updated_at :datetime not null +# +require "rails_helper" + +RSpec.describe SavingsRate, type: :model do + pending "add some examples to (or delete) #{__FILE__}" +end diff --git a/spec/requests/savings_rates_spec.rb b/spec/requests/savings_rates_spec.rb new file mode 100644 index 0000000..442165b --- /dev/null +++ b/spec/requests/savings_rates_spec.rb @@ -0,0 +1,130 @@ +require "rails_helper" + +# This spec was generated by rspec-rails when you ran the scaffold generator. +# It demonstrates how one might use RSpec to test the controller code that +# was generated by Rails when you ran the scaffold generator. +# +# It assumes that the implementation code is generated by the rails scaffold +# generator. If you are using any extension libraries to generate different +# controller code, this generated spec may or may not pass. +# +# It only uses APIs available in rails and/or rspec-rails. There are a number +# of tools you can use to make these specs even more expressive, but we're +# sticking to rails and rspec-rails APIs to keep things simple and stable. + +RSpec.describe "/savings_rates", type: :request do + # This should return the minimal set of attributes required to create a valid + # SavingsRate. As you add validations to SavingsRate, be sure to + # adjust the attributes here as well. + let(:valid_attributes) { + skip("Add a hash of attributes valid for your model") + } + + let(:invalid_attributes) { + skip("Add a hash of attributes invalid for your model") + } + + describe "GET /index" do + it "renders a successful response" do + SavingsRate.create! valid_attributes + get savings_rates_url + expect(response).to be_successful + end + end + + describe "GET /show" do + it "renders a successful response" do + savings_rate = SavingsRate.create! valid_attributes + get savings_rate_url(savings_rate) + expect(response).to be_successful + end + end + + describe "GET /new" do + it "renders a successful response" do + get new_savings_rate_url + expect(response).to be_successful + end + end + + describe "GET /edit" do + it "renders a successful response" do + savings_rate = SavingsRate.create! valid_attributes + get edit_savings_rate_url(savings_rate) + expect(response).to be_successful + end + end + + describe "POST /create" do + context "with valid parameters" do + it "creates a new SavingsRate" do + expect { + post savings_rates_url, params: {savings_rate: valid_attributes} + }.to change(SavingsRate, :count).by(1) + end + + it "redirects to the created savings_rate" do + post savings_rates_url, params: {savings_rate: valid_attributes} + expect(response).to redirect_to(savings_rate_url(SavingsRate.last)) + end + end + + context "with invalid parameters" do + it "does not create a new SavingsRate" do + expect { + post savings_rates_url, params: {savings_rate: invalid_attributes} + }.to change(SavingsRate, :count).by(0) + end + + it "renders a response with 422 status (i.e. to display the 'new' template)" do + post savings_rates_url, params: {savings_rate: invalid_attributes} + expect(response).to have_http_status(:unprocessable_entity) + end + end + end + + describe "PATCH /update" do + context "with valid parameters" do + let(:new_attributes) { + skip("Add a hash of attributes valid for your model") + } + + it "updates the requested savings_rate" do + savings_rate = SavingsRate.create! valid_attributes + patch savings_rate_url(savings_rate), params: {savings_rate: new_attributes} + savings_rate.reload + skip("Add assertions for updated state") + end + + it "redirects to the savings_rate" do + savings_rate = SavingsRate.create! valid_attributes + patch savings_rate_url(savings_rate), params: {savings_rate: new_attributes} + savings_rate.reload + expect(response).to redirect_to(savings_rate_url(savings_rate)) + end + end + + context "with invalid parameters" do + it "renders a response with 422 status (i.e. to display the 'edit' template)" do + savings_rate = SavingsRate.create! valid_attributes + patch savings_rate_url(savings_rate), params: {savings_rate: invalid_attributes} + expect(response).to have_http_status(:unprocessable_entity) + end + end + end + + describe "DELETE /destroy" do + it "destroys the requested savings_rate" do + savings_rate = SavingsRate.create! valid_attributes + expect { + delete savings_rate_url(savings_rate) + }.to change(SavingsRate, :count).by(-1) + end + + it "redirects to the savings_rates list" do + savings_rate = SavingsRate.create! valid_attributes + delete savings_rate_url(savings_rate) + expect(response).to redirect_to(savings_rates_url) + end + end +end diff --git a/spec/routing/savings_rates_routing_spec.rb b/spec/routing/savings_rates_routing_spec.rb new file mode 100644 index 0000000..d30e805 --- /dev/null +++ b/spec/routing/savings_rates_routing_spec.rb @@ -0,0 +1,37 @@ +require "rails_helper" + +RSpec.describe SavingsRatesController, type: :routing do + describe "routing" do + it "routes to #index" do + expect(get: "/savings_rates").to route_to("savings_rates#index") + end + + it "routes to #new" do + expect(get: "/savings_rates/new").to route_to("savings_rates#new") + end + + it "routes to #show" do + expect(get: "/savings_rates/1").to route_to("savings_rates#show", id: "1") + end + + it "routes to #edit" do + expect(get: "/savings_rates/1/edit").to route_to("savings_rates#edit", id: "1") + end + + it "routes to #create" do + expect(post: "/savings_rates").to route_to("savings_rates#create") + end + + it "routes to #update via PUT" do + expect(put: "/savings_rates/1").to route_to("savings_rates#update", id: "1") + end + + it "routes to #update via PATCH" do + expect(patch: "/savings_rates/1").to route_to("savings_rates#update", id: "1") + end + + it "routes to #destroy" do + expect(delete: "/savings_rates/1").to route_to("savings_rates#destroy", id: "1") + end + end +end diff --git a/spec/views/savings_rates/edit.html.tailwindcss_spec.rb b/spec/views/savings_rates/edit.html.tailwindcss_spec.rb new file mode 100644 index 0000000..430e9ff --- /dev/null +++ b/spec/views/savings_rates/edit.html.tailwindcss_spec.rb @@ -0,0 +1,19 @@ +require "rails_helper" + +RSpec.describe "savings_rates/edit", type: :view do + let(:savings_rate) { create(:savings_rate) } + + before(:each) do + assign(:savings_rate, savings_rate) + end + + it "renders the edit savings_rate form" do + render + + assert_select "form[action=?][method=?]", savings_rate_path(savings_rate), "post" do + assert_select "input[name=?]", "savings_rate[name]" + + assert_select "input[name=?]", "savings_rate[rate]" + end + end +end diff --git a/spec/views/savings_rates/index.html.tailwindcss_spec.rb b/spec/views/savings_rates/index.html.tailwindcss_spec.rb new file mode 100644 index 0000000..873e010 --- /dev/null +++ b/spec/views/savings_rates/index.html.tailwindcss_spec.rb @@ -0,0 +1,23 @@ +require "rails_helper" + +RSpec.describe "savings_rates/index", type: :view do + before(:each) do + assign(:savings_rates, [ + SavingsRate.create!( + name: "savings", + rate: 2.5 + ), + SavingsRate.create!( + name: "investing", + rate: 2.5 + ) + ]) + end + + it "renders a list of savings_rates" do + render + cell_selector = (Rails::VERSION::STRING >= "7") ? "div>p" : "tr>td" + assert_select cell_selector, text: Regexp.new("Name".to_s), count: 2 + assert_select cell_selector, text: Regexp.new(2.5.to_s), count: 2 + end +end diff --git a/spec/views/savings_rates/new.html.tailwindcss_spec.rb b/spec/views/savings_rates/new.html.tailwindcss_spec.rb new file mode 100644 index 0000000..16f6ed9 --- /dev/null +++ b/spec/views/savings_rates/new.html.tailwindcss_spec.rb @@ -0,0 +1,20 @@ +require "rails_helper" + +RSpec.describe "savings_rates/new", type: :view do + before(:each) do + assign(:savings_rate, SavingsRate.new( + name: "investing", + rate: 1.5 + )) + end + + it "renders new savings_rate form" do + render + + assert_select "form[action=?][method=?]", savings_rates_path, "post" do + assert_select "input[name=?]", "savings_rate[name]" + + assert_select "input[name=?]", "savings_rate[rate]" + end + end +end diff --git a/spec/views/savings_rates/show.html.tailwindcss_spec.rb b/spec/views/savings_rates/show.html.tailwindcss_spec.rb new file mode 100644 index 0000000..cf3ae3b --- /dev/null +++ b/spec/views/savings_rates/show.html.tailwindcss_spec.rb @@ -0,0 +1,16 @@ +require "rails_helper" + +RSpec.describe "savings_rates/show", type: :view do + before(:each) do + assign(:savings_rate, SavingsRate.create!( + name: "savings", + rate: 2.5 + )) + end + + it "renders attributes in

" do + render + expect(rendered).to match(/Name/) + expect(rendered).to match(/2.5/) + end +end