diff --git a/app/controllers/vehicles_controller.rb b/app/controllers/vehicles_controller.rb
new file mode 100644
index 00000000..b2d81ed6
--- /dev/null
+++ b/app/controllers/vehicles_controller.rb
@@ -0,0 +1,42 @@
+class VehiclesController < ApplicationController
+ before_action :set_account, only: :update
+
+ def create
+ account = Current.family
+ .accounts
+ .create_with_optional_start_balance! \
+ attributes: account_params.except(:start_date, :start_balance),
+ start_date: account_params[:start_date],
+ start_balance: account_params[:start_balance]
+
+ account.sync_later
+ redirect_to account, notice: t(".success")
+ end
+
+ def update
+ @account.update!(account_params)
+ @account.sync_later
+ redirect_to @account, notice: t(".success")
+ end
+
+ private
+
+ def set_account
+ @account = Current.family.accounts.find(params[:id])
+ end
+
+ def account_params
+ params.require(:account)
+ .permit(
+ :name, :balance, :start_date, :start_balance, :currency, :accountable_type,
+ accountable_attributes: [
+ :id,
+ :make,
+ :model,
+ :year,
+ :mileage_value,
+ :mileage_unit
+ ]
+ )
+ end
+end
diff --git a/app/helpers/accounts_helper.rb b/app/helpers/accounts_helper.rb
index c26fde98..33d9cfcb 100644
--- a/app/helpers/accounts_helper.rb
+++ b/app/helpers/accounts_helper.rb
@@ -29,6 +29,8 @@ module AccountsHelper
case account.accountable_type
when "Property"
properties_path
+ when "Vehicle"
+ vehicles_path
else
accounts_path
end
@@ -38,6 +40,8 @@ module AccountsHelper
case account.accountable_type
when "Property"
property_path(account)
+ when "Vehicle"
+ vehicle_path(account)
else
account_path(account)
end
@@ -51,7 +55,7 @@ module AccountsHelper
transactions_tab = { key: "transactions", label: t("accounts.show.transactions"), path: account_path(account, tab: "transactions"), route: account_transactions_path(account) }
trades_tab = { key: "trades", label: t("accounts.show.trades"), path: account_path(account, tab: "trades"), route: account_trades_path(account) }
- return [ overview_tab, value_tab ] if account.property?
+ return [ overview_tab, value_tab ] if account.property? || account.vehicle?
return [ holdings_tab, cash_tab, trades_tab ] if account.investment?
[ value_tab, transactions_tab ]
diff --git a/app/helpers/vehicles_helper.rb b/app/helpers/vehicles_helper.rb
new file mode 100644
index 00000000..f8e7abd9
--- /dev/null
+++ b/app/helpers/vehicles_helper.rb
@@ -0,0 +1,2 @@
+module VehiclesHelper
+end
diff --git a/app/models/measurement.rb b/app/models/measurement.rb
index 56b3ec43..ef57086c 100644
--- a/app/models/measurement.rb
+++ b/app/models/measurement.rb
@@ -3,7 +3,7 @@ class Measurement
attr_reader :value, :unit
- VALID_UNITS = %w[sqft sqm]
+ VALID_UNITS = %w[sqft sqm mi km]
validates :unit, inclusion: { in: VALID_UNITS }
validates :value, presence: true
diff --git a/app/models/property.rb b/app/models/property.rb
index d5af127b..a23519ed 100644
--- a/app/models/property.rb
+++ b/app/models/property.rb
@@ -15,10 +15,6 @@ class Property < ApplicationRecord
first_valuation_amount
end
- def equity_value
- account.balance_money
- end
-
def trend
TimeSeries::Trend.new(current: account.balance_money, previous: first_valuation_amount)
end
diff --git a/app/models/vehicle.rb b/app/models/vehicle.rb
index 13d20150..2ab34d7b 100644
--- a/app/models/vehicle.rb
+++ b/app/models/vehicle.rb
@@ -1,3 +1,22 @@
class Vehicle < ApplicationRecord
include Accountable
+
+ attribute :mileage_unit, :string, default: "mi"
+
+ def mileage
+ Measurement.new(mileage_value, mileage_unit) if mileage_value.present?
+ end
+
+ def purchase_price
+ first_valuation_amount
+ end
+
+ def trend
+ TimeSeries::Trend.new(current: account.balance_money, previous: first_valuation_amount)
+ end
+
+ private
+ def first_valuation_amount
+ account.entries.account_valuations.order(:date).first&.amount_money || account.balance_money
+ end
end
diff --git a/app/views/accounts/accountables/_vehicle.html.erb b/app/views/accounts/accountables/_vehicle.html.erb
index e69de29b..e6fe2443 100644
--- a/app/views/accounts/accountables/_vehicle.html.erb
+++ b/app/views/accounts/accountables/_vehicle.html.erb
@@ -0,0 +1,23 @@
+<%# locals: (f:) %>
+
+
+
+
+
+ <%= f.fields_for :accountable do |vehicle_form| %>
+
+
+ <%= vehicle_form.text_field :make, label: t(".make"), placeholder: t(".make_placeholder") %>
+ <%= vehicle_form.text_field :model, label: t(".model"), placeholder: t(".model_placeholder") %>
+
+
+
+ <%= vehicle_form.text_field :year, label: t(".year"), placeholder: t(".year_placeholder") %>
+ <%= vehicle_form.text_field :mileage_value, label: t(".mileage"), placeholder: t(".mileage_placeholder") %>
+ <%= vehicle_form.select :mileage_unit,
+ [["Miles", "mi"], ["Kilometers", "km"]],
+ { label: t(".mileage_unit") } %>
+
+ <% end %>
+
+
diff --git a/app/views/accounts/accountables/vehicle/_overview.html.erb b/app/views/accounts/accountables/vehicle/_overview.html.erb
new file mode 100644
index 00000000..c371a7bb
--- /dev/null
+++ b/app/views/accounts/accountables/vehicle/_overview.html.erb
@@ -0,0 +1,49 @@
+<%# locals: (account:) %>
+
+
+
+
<%= t(".make_model") %>
+
+ <%= [account.vehicle.make, account.vehicle.model].compact.join(" ").presence || t(".unknown") %>
+
+
+
+
+
<%= t(".year") %>
+
+ <%= account.vehicle.year || t(".unknown") %>
+
+
+
+
+
<%= t(".mileage") %>
+
+ <%= account.vehicle.mileage || t(".unknown") %>
+
+
+
+
+
<%= t(".purchase_price") %>
+
+ <%= format_money account.vehicle.purchase_price %>
+
+
+
+
+
<%= t(".current_price") %>
+
+ <%= format_money account.balance_money %>
+
+
+
+
+
<%= t(".trend") %>
+
+
+ <%= account.vehicle.trend.value %>
+
+
+
(<%= account.vehicle.trend.percent %>%)
+
+
+
diff --git a/config/locales/views/accounts/en.yml b/config/locales/views/accounts/en.yml
index eb4bdba1..7dcc5a9a 100644
--- a/config/locales/views/accounts/en.yml
+++ b/config/locales/views/accounts/en.yml
@@ -19,6 +19,24 @@ en:
postal_code: Postal code (optional)
state: State
year_built: Year built (optional)
+ vehicle:
+ make: Make
+ make_placeholder: Toyota
+ mileage: Mileage
+ mileage_placeholder: '15000'
+ mileage_unit: Unit
+ model: Model
+ model_placeholder: Camry
+ overview:
+ current_price: Current Price
+ make_model: Make & Model
+ mileage: Mileage
+ purchase_price: Purchase Price
+ trend: Trend
+ unknown: Unknown
+ year: Year
+ year: Year
+ year_placeholder: '2023'
create:
success: New account created successfully
destroy:
diff --git a/config/locales/views/vehicles/en.yml b/config/locales/views/vehicles/en.yml
new file mode 100644
index 00000000..73e0b8e5
--- /dev/null
+++ b/config/locales/views/vehicles/en.yml
@@ -0,0 +1,7 @@
+---
+en:
+ vehicles:
+ create:
+ success: Vehicle created successfully
+ update:
+ success: Vehicle updated successfully
diff --git a/config/routes.rb b/config/routes.rb
index 25b92740..4ffc2e66 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -90,6 +90,7 @@ Rails.application.routes.draw do
end
resources :properties, only: %i[ create update ]
+ resources :vehicles, only: %i[ create update ]
resources :transactions, only: %i[ index new create ] do
collection do
diff --git a/db/migrate/20240823125526_add_details_to_vehicle.rb b/db/migrate/20240823125526_add_details_to_vehicle.rb
new file mode 100644
index 00000000..6ab6dd6f
--- /dev/null
+++ b/db/migrate/20240823125526_add_details_to_vehicle.rb
@@ -0,0 +1,9 @@
+class AddDetailsToVehicle < ActiveRecord::Migration[7.2]
+ def change
+ add_column :vehicles, :year, :integer
+ add_column :vehicles, :mileage_value, :integer
+ add_column :vehicles, :mileage_unit, :string
+ add_column :vehicles, :make, :string
+ add_column :vehicles, :model, :string
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index cb2fe1ff..7833d7ad 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.2].define(version: 2024_08_22_180845) do
+ActiveRecord::Schema[7.2].define(version: 2024_08_23_125526) do
# These are extensions that must be enabled in order to support this database
enable_extension "pgcrypto"
enable_extension "plpgsql"
@@ -441,6 +441,11 @@ ActiveRecord::Schema[7.2].define(version: 2024_08_22_180845) do
create_table "vehicles", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
+ t.integer "year"
+ t.integer "mileage_value"
+ t.string "mileage_unit"
+ t.string "make"
+ t.string "model"
end
add_foreign_key "account_balances", "accounts", on_delete: :cascade
diff --git a/test/controllers/vehicles_controller_test.rb b/test/controllers/vehicles_controller_test.rb
new file mode 100644
index 00000000..2aecb42f
--- /dev/null
+++ b/test/controllers/vehicles_controller_test.rb
@@ -0,0 +1,71 @@
+require "test_helper"
+
+class VehiclesControllerTest < ActionDispatch::IntegrationTest
+ setup do
+ sign_in @user = users(:family_admin)
+ @account = accounts(:vehicle)
+ end
+
+ test "creates vehicle" do
+ assert_difference -> { Account.count } => 1,
+ -> { Vehicle.count } => 1,
+ -> { Account::Valuation.count } => 2,
+ -> { Account::Entry.count } => 2 do
+ post vehicles_path, params: {
+ account: {
+ name: "Vehicle",
+ balance: 30000,
+ currency: "USD",
+ accountable_type: "Vehicle",
+ start_date: 1.year.ago.to_date,
+ start_balance: 35000,
+ accountable_attributes: {
+ make: "Toyota",
+ model: "Camry",
+ year: 2020,
+ mileage_value: 15000,
+ mileage_unit: "mi"
+ }
+ }
+ }
+ end
+
+ created_account = Account.order(:created_at).last
+
+ assert_equal "Toyota", created_account.vehicle.make
+ assert_equal "Camry", created_account.vehicle.model
+ assert_equal 2020, created_account.vehicle.year
+ assert_equal 15000, created_account.vehicle.mileage_value
+ assert_equal "mi", created_account.vehicle.mileage_unit
+
+ assert_redirected_to account_path(created_account)
+ assert_equal "Vehicle created successfully", flash[:notice]
+ assert_enqueued_with(job: AccountSyncJob)
+ end
+
+ test "updates vehicle" do
+ assert_no_difference [ "Account.count", "Vehicle.count", "Account::Valuation.count", "Account::Entry.count" ] do
+ patch vehicle_path(@account), params: {
+ account: {
+ name: "Updated Vehicle",
+ balance: 28000,
+ currency: "USD",
+ accountable_type: "Vehicle",
+ accountable_attributes: {
+ id: @account.accountable_id,
+ make: "Honda",
+ model: "Accord",
+ year: 2021,
+ mileage_value: 20000,
+ mileage_unit: "mi",
+ purchase_price: 32000
+ }
+ }
+ }
+ end
+
+ assert_redirected_to account_path(@account)
+ assert_equal "Vehicle updated successfully", flash[:notice]
+ assert_enqueued_with(job: AccountSyncJob)
+ end
+end
diff --git a/test/system/accounts_test.rb b/test/system/accounts_test.rb
index 3873e784..57f4268e 100644
--- a/test/system/accounts_test.rb
+++ b/test/system/accounts_test.rb
@@ -34,7 +34,12 @@ class AccountsTest < ApplicationSystemTestCase
end
test "can create vehicle account" do
- assert_account_created("Vehicle")
+ assert_account_created "Vehicle" do
+ fill_in "Make", with: "Toyota"
+ fill_in "Model", with: "Camry"
+ fill_in "Year", with: "2020"
+ fill_in "Mileage", with: "30000"
+ end
end
test "can create other asset account" do