- <%= form.label options[:label] || t(".label"), class: "form-field__label" do %>
- <%= options[:label] || t(".label") %>
- <% if options[:required] %>
-
+ <% if options[:label_tooltip] %>
+
<% end %>
-
-
-
- <%= currency.symbol %>
-
-
- <%= form.number_field amount_method,
- class: "form-field__input",
- inline: true,
- placeholder: "100",
- value: if options[:value]
- sprintf("%.#{currency.default_precision}f", options[:value])
- elsif form.object && form.object.respond_to?(amount_method)
- val = form.object.public_send(amount_method)
- sprintf("%.#{currency.default_precision}f", val) if val.present?
- end,
- min: options[:min] || -99999999999999,
- max: options[:max] || 99999999999999,
- step: currency.step,
- disabled: options[:disabled],
- data: {
- "money-field-target": "amount",
- "auto-submit-form-target": ("auto" if options[:auto_submit])
- }.compact,
- required: options[:required] %>
-
-
- <% unless options[:hide_currency] %>
-
- <%= form.select currency_method,
- Money::Currency.as_options.map(&:iso_code),
- { inline: true, selected: currency.iso_code },
- {
- class: "w-fit pr-5 disabled:text-subdued form-field__input",
- disabled: options[:disable_currency],
- data: {
- "money-field-target": "currency",
- action: "change->money-field#handleCurrencyChange",
- "auto-submit-form-target": ("auto" if options[:auto_submit])
- }.compact
- } %>
-
+
+ <% unless options[:label_tooltip] %>
+ <%= form.label options[:label] || t(".label"), class: "form-field__label" do %>
+ <%= options[:label] || t(".label") %>
+ <% if options[:required] %>
+
*
+ <% end %>
+ <% end %>
<% end %>
+
+
+
+
+ <%= currency.symbol %>
+
+
+ <%= form.number_field amount_method,
+ class: "form-field__input",
+ inline: true,
+ placeholder: "100",
+ value: if options[:value]
+ sprintf("%.#{currency.default_precision}f", options[:value])
+ elsif form.object && form.object.respond_to?(amount_method)
+ val = form.object.public_send(amount_method)
+ sprintf("%.#{currency.default_precision}f", val) if val.present?
+ end,
+ min: options[:min] || -99999999999999,
+ max: options[:max] || 99999999999999,
+ step: currency.step,
+ disabled: options[:disabled],
+ data: {
+ "money-field-target": "amount",
+ "auto-submit-form-target": ("auto" if options[:auto_submit])
+ }.compact,
+ required: options[:required] %>
+
+
+ <% unless options[:hide_currency] %>
+
+ <%= form.select currency_method,
+ Money::Currency.as_options.map(&:iso_code),
+ { inline: true, selected: currency.iso_code },
+ {
+ class: "w-fit pr-5 disabled:text-subdued form-field__input",
+ disabled: options[:disable_currency],
+ data: {
+ "money-field-target": "currency",
+ action: "change->money-field#handleCurrencyChange",
+ "auto-submit-form-target": ("auto" if options[:auto_submit])
+ }.compact
+ } %>
+
+ <% end %>
+
diff --git a/app/views/trades/_form.html.erb b/app/views/trades/_form.html.erb
index ddf9a26b..57e22907 100644
--- a/app/views/trades/_form.html.erb
+++ b/app/views/trades/_form.html.erb
@@ -45,7 +45,7 @@
<% end %>
<% if %w[deposit withdrawal].include?(type) %>
- <%= form.collection_select :transfer_account_id, Current.family.accounts.alphabetically, :id, :name, { prompt: t(".account_prompt"), label: t(".account") } %>
+ <%= form.collection_select :transfer_account_id, Current.family.accounts.visible.alphabetically, :id, :name, { prompt: t(".account_prompt"), label: t(".account") } %>
<% end %>
<% if %w[buy sell].include?(type) %>
diff --git a/app/views/valuations/_form.html.erb b/app/views/valuations/_form.html.erb
index 287d057f..5429f3a7 100644
--- a/app/views/valuations/_form.html.erb
+++ b/app/views/valuations/_form.html.erb
@@ -1,10 +1,10 @@
-<%# locals: (entry:) %>
+<%# locals: (entry:, error_message:) %>
<%= styled_form_with model: entry, url: valuations_path, class: "space-y-4" do |form| %>
<%= form.hidden_field :account_id %>
- <% if entry.errors.any? %>
- <%= render "shared/form_errors", model: entry %>
+ <% if error_message.present? %>
+ <%= render AlertComponent.new(message: error_message, variant: :error) %>
<% end %>
diff --git a/app/views/valuations/_header.html.erb b/app/views/valuations/_header.html.erb
index 1f19967a..ce7f138f 100644
--- a/app/views/valuations/_header.html.erb
+++ b/app/views/valuations/_header.html.erb
@@ -2,7 +2,7 @@
<%= tag.header class: "mb-4 space-y-1", id: dom_id(entry, :header) do %>
- <%= t(".balance") %>
+ <%= entry.name %>
diff --git a/app/views/valuations/_valuation.html.erb b/app/views/valuations/_valuation.html.erb
index 416f1775..176f21be 100644
--- a/app/views/valuations/_valuation.html.erb
+++ b/app/views/valuations/_valuation.html.erb
@@ -25,15 +25,7 @@
-
- <% if balance_trend&.trend %>
- <%= tag.span format_money(balance_trend.trend.value), style: "color: #{balance_trend.trend.color}" %>
- <% else %>
- <%= tag.span "--", class: "text-gray-400" %>
- <% end %>
-
-
-
+
<%= tag.p format_money(entry.amount_money), class: "font-medium text-sm text-primary" %>
diff --git a/app/views/valuations/new.html.erb b/app/views/valuations/new.html.erb
index 191ee5f5..82102f16 100644
--- a/app/views/valuations/new.html.erb
+++ b/app/views/valuations/new.html.erb
@@ -1,6 +1,6 @@
<%= render DialogComponent.new do |dialog| %>
<% dialog.with_header(title: t(".title")) %>
<% dialog.with_body do %>
- <%= render "form", entry: @entry %>
+ <%= render "form", entry: @entry, error_message: @error_message %>
<% end %>
<% end %>
diff --git a/app/views/valuations/show.html.erb b/app/views/valuations/show.html.erb
index bd7d8d9e..6e96dcaa 100644
--- a/app/views/valuations/show.html.erb
+++ b/app/views/valuations/show.html.erb
@@ -6,17 +6,18 @@
<% end %>
<% dialog.with_body do %>
+ <% if @error_message.present? %>
+
+ <%= render AlertComponent.new(message: @error_message, variant: :error) %>
+
+ <% end %>
+
<% dialog.with_section(title: t(".overview"), open: true) do %>
<%= styled_form_with model: entry,
url: entry_path(entry),
class: "space-y-2",
data: { controller: "auto-submit-form" } do |f| %>
- <%= f.text_field :name,
- label: t(".name_label"),
- placeholder: t(".name_placeholder"),
- "data-auto-submit-form-target": "auto" %>
-
<%= f.date_field :date,
label: t(".date_label"),
max: Date.current,
diff --git a/config/brakeman.ignore b/config/brakeman.ignore
index 4d872a49..5ca90443 100644
--- a/config/brakeman.ignore
+++ b/config/brakeman.ignore
@@ -1,5 +1,28 @@
{
"ignored_warnings": [
+ {
+ "warning_type": "Mass Assignment",
+ "warning_code": 105,
+ "fingerprint": "85e2c11853dd6c69b1953a6ec3ad661cd0ce3df55e4e5beff92365b6ed601171",
+ "check_name": "PermitAttributes",
+ "message": "Potentially dangerous key allowed for mass assignment",
+ "file": "app/controllers/api/v1/transactions_controller.rb",
+ "line": 255,
+ "link": "https://brakemanscanner.org/docs/warning_types/mass_assignment/",
+ "code": "params.require(:transaction).permit(:account_id, :date, :amount, :name, :description, :notes, :currency, :category_id, :merchant_id, :nature, :tag_ids => ([]))",
+ "render_path": null,
+ "location": {
+ "type": "method",
+ "class": "Api::V1::TransactionsController",
+ "method": "transaction_params"
+ },
+ "user_input": ":account_id",
+ "confidence": "High",
+ "cwe_id": [
+ 915
+ ],
+ "note": "account_id is properly validated in create action - line 79 ensures account belongs to user's family: family.accounts.find(transaction_params[:account_id])"
+ },
{
"warning_type": "Mass Assignment",
"warning_code": 105,
@@ -26,13 +49,13 @@
{
"warning_type": "Dangerous Eval",
"warning_code": 13,
- "fingerprint": "c193307bb82f931950d3bf2855f82f9a7f50d94c5bd950ee2803cb8a8abe5253",
+ "fingerprint": "c154514a0f86341473e4abf35e77721495b326c7855e4967d284b4942371819c",
"check_name": "Evaluation",
"message": "Dynamic string evaluated as code",
"file": "app/helpers/styled_form_builder.rb",
- "line": 7,
+ "line": 5,
"link": "https://brakemanscanner.org/docs/warning_types/dangerous_eval/",
- "code": "class_eval(\" def #{selector}(method, options = {})\\n merged_options = { class: \\\"form-field__input\\\" }.merge(options)\\n label = build_label(method, options)\\n field = super(method, merged_options)\\n\\n build_styled_field(label, field, merged_options)\\n end\\n\", \"app/helpers/styled_form_builder.rb\", (7 + 1))",
+ "code": "class_eval(\" def #{selector}(method, options = {})\\n form_options = options.slice(:label, :label_tooltip, :inline, :container_class, :required)\\n html_options = options.except(:label, :label_tooltip, :inline, :container_class)\\n\\n build_field(method, form_options, html_options) do |merged_options|\\n super(method, merged_options)\\n end\\n end\\n\", \"app/helpers/styled_form_builder.rb\", (5 + 1))",
"render_path": null,
"location": {
"type": "method",
@@ -45,7 +68,7 @@
913,
95
],
- "note": "This is safe as 'selector' comes from a predefined list of Rails form helpers (StyledFormBuilder.text_field_helpers)."
+ "note": "Uses similar pattern to Rails internal form builder"
},
{
"warning_type": "Dynamic Render Path",
@@ -80,29 +103,6 @@
22
],
"note": ""
- },
- {
- "warning_type": "Mass Assignment",
- "warning_code": 105,
- "fingerprint": "85e2c11853dd6c69b1953a6ec3ad661cd0ce3df55e4e5beff92365b6ed601171",
- "check_name": "PermitAttributes",
- "message": "Potentially dangerous key allowed for mass assignment",
- "file": "app/controllers/api/v1/transactions_controller.rb",
- "line": 255,
- "link": "https://brakemanscanner.org/docs/warning_types/mass_assignment/",
- "code": "params.require(:transaction).permit(:account_id, :date, :amount, :name, :description, :notes, :currency, :category_id, :merchant_id, :nature, :tag_ids => ([]))",
- "render_path": null,
- "location": {
- "type": "method",
- "class": "Api::V1::TransactionsController",
- "method": "transaction_params"
- },
- "user_input": ":account_id",
- "confidence": "High",
- "cwe_id": [
- 915
- ],
- "note": "account_id is properly validated in create action - line 79 ensures account belongs to user's family: family.accounts.find(transaction_params[:account_id])"
}
],
"brakeman_version": "7.0.2"
diff --git a/config/routes.rb b/config/routes.rb
index 43ca807a..f0414287 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -108,14 +108,6 @@ Rails.application.routes.draw do
resources :mappings, only: :update, module: :import
end
- resources :accounts, only: %i[index new], shallow: true do
- member do
- post :sync
- get :chart
- get :sparkline
- end
- end
-
resources :holdings, only: %i[index new show destroy]
resources :trades, only: %i[show new create update destroy]
resources :valuations, only: %i[show new create update destroy]
@@ -155,18 +147,36 @@ Rails.application.routes.draw do
end
end
+ resources :accounts, only: %i[index new], shallow: true do
+ member do
+ post :sync
+ get :chart
+ get :sparkline
+ patch :toggle_active
+ end
+ end
+
# Convenience routes for polymorphic paths
# Example: account_path(Account.new(accountable: Depository.new)) => /depositories/123
direct :account do |model, options|
route_for model.accountable_name, model, options
end
+
direct :edit_account do |model, options|
route_for "edit_#{model.accountable_name}", model, options
end
resources :depositories, except: :index
resources :investments, except: :index
- resources :properties, except: :index
+ resources :properties, except: :index do
+ member do
+ get :balances
+ patch :update_balances
+
+ get :address
+ patch :update_address
+ end
+ end
resources :vehicles, except: :index
resources :credit_cards, except: :index
resources :loans, except: :index
diff --git a/db/migrate/20250701161640_add_account_status.rb b/db/migrate/20250701161640_add_account_status.rb
new file mode 100644
index 00000000..afa4ede3
--- /dev/null
+++ b/db/migrate/20250701161640_add_account_status.rb
@@ -0,0 +1,41 @@
+class AddAccountStatus < ActiveRecord::Migration[7.2]
+ def up
+ add_column :accounts, :status, :string, default: "active"
+ change_column_null :entries, :amount, false
+
+ # Migrate existing data
+ execute <<-SQL
+ UPDATE accounts
+ SET status = CASE
+ WHEN scheduled_for_deletion = true THEN 'pending_deletion'
+ WHEN is_active = true THEN 'active'
+ WHEN is_active = false THEN 'disabled'
+ ELSE 'draft'
+ END
+ SQL
+
+ remove_column :accounts, :is_active
+ remove_column :accounts, :scheduled_for_deletion
+ end
+
+ def down
+ add_column :accounts, :is_active, :boolean, default: true, null: false
+ add_column :accounts, :scheduled_for_deletion, :boolean, default: false
+
+ # Restore the original boolean fields based on status
+ execute <<-SQL
+ UPDATE accounts
+ SET is_active = CASE
+ WHEN status = 'active' THEN true
+ WHEN status IN ('disabled', 'pending_deletion') THEN false
+ ELSE false
+ END,
+ scheduled_for_deletion = CASE
+ WHEN status = 'pending_deletion' THEN true
+ ELSE false
+ END
+ SQL
+
+ remove_column :accounts, :status
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index 8a484295..f1bc9daf 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: 2025_06_23_162207) do
+ActiveRecord::Schema[7.2].define(version: 2025_07_01_161640) do
# These are extensions that must be enabled in order to support this database
enable_extension "pgcrypto"
enable_extension "plpgsql"
@@ -29,13 +29,12 @@ ActiveRecord::Schema[7.2].define(version: 2025_06_23_162207) do
t.uuid "accountable_id"
t.decimal "balance", precision: 19, scale: 4
t.string "currency"
- t.boolean "is_active", default: true, null: false
- t.virtual "classification", type: :string, as: "\nCASE\n WHEN ((accountable_type)::text = ANY (ARRAY[('Loan'::character varying)::text, ('CreditCard'::character varying)::text, ('OtherLiability'::character varying)::text])) THEN 'liability'::text\n ELSE 'asset'::text\nEND", stored: true
+ t.virtual "classification", type: :string, as: "\nCASE\n WHEN ((accountable_type)::text = ANY ((ARRAY['Loan'::character varying, 'CreditCard'::character varying, 'OtherLiability'::character varying])::text[])) THEN 'liability'::text\n ELSE 'asset'::text\nEND", stored: true
t.uuid "import_id"
t.uuid "plaid_account_id"
- t.boolean "scheduled_for_deletion", default: false
t.decimal "cash_balance", precision: 19, scale: 4, default: "0.0"
t.jsonb "locked_attributes", default: {}
+ t.string "status", default: "active"
t.index ["accountable_id", "accountable_type"], name: "index_accounts_on_accountable_id_and_accountable_type"
t.index ["accountable_type"], name: "index_accounts_on_accountable_type"
t.index ["family_id", "accountable_type"], name: "index_accounts_on_family_id_and_accountable_type"
@@ -205,7 +204,7 @@ ActiveRecord::Schema[7.2].define(version: 2025_06_23_162207) do
t.uuid "account_id", null: false
t.string "entryable_type"
t.uuid "entryable_id"
- t.decimal "amount", precision: 19, scale: 4
+ t.decimal "amount", precision: 19, scale: 4, null: false
t.string "currency"
t.date "date"
t.string "name", null: false
@@ -216,12 +215,7 @@ ActiveRecord::Schema[7.2].define(version: 2025_06_23_162207) do
t.boolean "excluded", default: false
t.string "plaid_id"
t.jsonb "locked_attributes", default: {}
- t.index ["account_id", "date"], name: "index_entries_on_account_id_and_date"
t.index ["account_id"], name: "index_entries_on_account_id"
- t.index ["amount"], name: "index_entries_on_amount"
- t.index ["date"], name: "index_entries_on_date"
- t.index ["entryable_id", "entryable_type"], name: "index_entries_on_entryable"
- t.index ["excluded"], name: "index_entries_on_excluded"
t.index ["import_id"], name: "index_entries_on_import_id"
end
@@ -232,7 +226,6 @@ ActiveRecord::Schema[7.2].define(version: 2025_06_23_162207) do
t.date "date", null: false
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
- t.index ["date", "from_currency", "to_currency"], name: "index_exchange_rates_on_date_and_currencies"
t.index ["from_currency", "to_currency", "date"], name: "index_exchange_rates_on_base_converted_date_unique", unique: true
t.index ["from_currency"], name: "index_exchange_rates_on_from_currency"
t.index ["to_currency"], name: "index_exchange_rates_on_to_currency"
@@ -691,7 +684,6 @@ ActiveRecord::Schema[7.2].define(version: 2025_06_23_162207) do
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["tag_id"], name: "index_taggings_on_tag_id"
- t.index ["taggable_id", "taggable_type"], name: "index_taggings_on_taggable_id_and_type"
t.index ["taggable_type", "taggable_id"], name: "index_taggings_on_taggable"
end
diff --git a/test/components/previews/alert_component_preview.rb b/test/components/previews/alert_component_preview.rb
new file mode 100644
index 00000000..34abcb37
--- /dev/null
+++ b/test/components/previews/alert_component_preview.rb
@@ -0,0 +1,7 @@
+class AlertComponentPreview < Lookbook::Preview
+ # @param message text
+ # @param variant select [info, success, warning, error]
+ def default(message: "This is an alert message.", variant: :info)
+ render AlertComponent.new(message: message, variant: variant.to_sym)
+ end
+end
diff --git a/test/controllers/api/v1/accounts_controller_test.rb b/test/controllers/api/v1/accounts_controller_test.rb
index 0af38702..9d95b454 100644
--- a/test/controllers/api/v1/accounts_controller_test.rb
+++ b/test/controllers/api/v1/accounts_controller_test.rb
@@ -81,7 +81,7 @@ end
test "should only return active accounts" do
# Make one account inactive
inactive_account = accounts(:depository)
- inactive_account.update!(is_active: false)
+ inactive_account.disable!
access_token = Doorkeeper::AccessToken.create!(
application: @oauth_app,
diff --git a/test/controllers/properties_controller_test.rb b/test/controllers/properties_controller_test.rb
index 43053373..b5f1305f 100644
--- a/test/controllers/properties_controller_test.rb
+++ b/test/controllers/properties_controller_test.rb
@@ -8,72 +8,169 @@ class PropertiesControllerTest < ActionDispatch::IntegrationTest
@account = accounts(:property)
end
- test "creates with property details" do
- assert_difference -> { Account.count } => 1,
- -> { Property.count } => 1,
- -> { Valuation.count } => 2,
- -> { Entry.count } => 2 do
+ test "creates property in draft status and redirects to balances step" do
+ assert_difference -> { Account.count } => 1 do
post properties_path, params: {
account: {
- name: "Property",
- balance: 500000,
- currency: "USD",
+ name: "New Property",
+ subtype: "house",
accountable_type: "Property",
accountable_attributes: {
- year_built: 2002,
- area_value: 1000,
- area_unit: "sqft",
- address_attributes: {
- line1: "123 Main St",
- line2: "Apt 1",
- locality: "Los Angeles",
- region: "CA", # ISO3166-2 code
- country: "US", # ISO3166-1 Alpha-2 code
- postal_code: "90001"
- }
+ year_built: 1990,
+ area_value: 1200,
+ area_unit: "sqft"
}
}
}
end
created_account = Account.order(:created_at).last
-
- assert created_account.accountable.year_built.present?
- assert created_account.accountable.address.line1.present?
-
- assert_redirected_to created_account
- assert_equal "Property account created", flash[:notice]
- assert_enqueued_with(job: SyncJob)
+ assert created_account.accountable.is_a?(Property)
+ assert_equal "draft", created_account.status
+ assert_equal 0, created_account.balance
+ assert_equal 1990, created_account.accountable.year_built
+ assert_equal 1200, created_account.accountable.area_value
+ assert_equal "sqft", created_account.accountable.area_unit
+ assert_redirected_to balances_property_path(created_account)
end
- test "updates with property details" do
+ test "updates property overview" do
assert_no_difference [ "Account.count", "Property.count" ] do
- patch account_path(@account), params: {
+ patch property_path(@account), params: {
account: {
name: "Updated Property",
- balance: 500000,
- currency: "USD",
- accountable_type: "Property",
- accountable_attributes: {
- id: @account.accountable_id,
- year_built: 2002,
- area_value: 1000,
- area_unit: "sqft",
- address_attributes: {
- line1: "123 Main St",
- line2: "Apt 1",
- locality: "Los Angeles",
- region: "CA", # ISO3166-2 code
- country: "US", # ISO3166-1 Alpha-2 code
- postal_code: "90001"
- }
- }
+ subtype: "condo"
}
}
end
- assert_redirected_to @account
- assert_equal "Property account updated", flash[:notice]
- assert_enqueued_with(job: SyncJob)
+ @account.reload
+ assert_equal "Updated Property", @account.name
+ assert_equal "condo", @account.subtype
+
+ # If account is active, it renders edit view; otherwise redirects to balances
+ if @account.active?
+ assert_response :success
+ else
+ assert_redirected_to balances_property_path(@account)
+ end
+ end
+
+ # Tab view tests
+ test "shows balances tab" do
+ get balances_property_path(@account)
+ assert_response :success
+ end
+
+ test "shows address tab" do
+ get address_property_path(@account)
+ assert_response :success
+ end
+
+ # Tab update tests
+ test "updates balances tab" do
+ original_balance = @account.balance
+
+ # Mock the update_balance method to return a successful result
+ Account::BalanceUpdater::Result.any_instance.stubs(:success?).returns(true)
+ Account::BalanceUpdater::Result.any_instance.stubs(:updated?).returns(true)
+
+ patch update_balances_property_path(@account), params: {
+ account: {
+ balance: 600000,
+ currency: "EUR"
+ }
+ }
+
+ # If account is active, it renders balances view; otherwise redirects to address
+ if @account.reload.active?
+ assert_response :success
+ else
+ assert_redirected_to address_property_path(@account)
+ end
+ end
+
+ test "updates address tab" do
+ patch update_address_property_path(@account), params: {
+ property: {
+ address_attributes: {
+ line1: "456 New Street",
+ locality: "San Francisco",
+ region: "CA",
+ country: "US",
+ postal_code: "94102"
+ }
+ }
+ }
+
+ @account.reload
+ assert_equal "456 New Street", @account.accountable.address.line1
+ assert_equal "San Francisco", @account.accountable.address.locality
+
+ # If account is draft, it activates and redirects; otherwise renders address
+ if @account.draft?
+ assert_redirected_to account_path(@account)
+ else
+ assert_response :success
+ end
+ end
+
+ test "balances update handles validation errors" do
+ # Mock update_balance to return a failure result
+ Account::BalanceUpdater::Result.any_instance.stubs(:success?).returns(false)
+ Account::BalanceUpdater::Result.any_instance.stubs(:error_message).returns("Invalid balance")
+
+ patch update_balances_property_path(@account), params: {
+ account: {
+ balance: 600000,
+ currency: "EUR"
+ }
+ }
+
+ assert_response :unprocessable_entity
+ end
+
+ test "address update handles validation errors" do
+ Property.any_instance.stubs(:update).returns(false)
+
+ patch update_address_property_path(@account), params: {
+ property: {
+ address_attributes: {
+ line1: "123 Test St"
+ }
+ }
+ }
+
+ assert_response :unprocessable_entity
+ end
+
+ test "address update activates draft account" do
+ # Create a draft property account
+ draft_account = Account.create!(
+ family: @user.family,
+ name: "Draft Property",
+ accountable: Property.new,
+ status: "draft",
+ balance: 500000,
+ currency: "USD"
+ )
+
+ assert draft_account.draft?
+
+ patch update_address_property_path(draft_account), params: {
+ property: {
+ address_attributes: {
+ line1: "789 Activate St",
+ locality: "New York",
+ region: "NY",
+ country: "US",
+ postal_code: "10001"
+ }
+ }
+ }
+
+ draft_account.reload
+ assert draft_account.active?
+ assert_redirected_to account_path(draft_account)
end
end
diff --git a/test/controllers/valuations_controller_test.rb b/test/controllers/valuations_controller_test.rb
index 34b2515f..d9ee00b3 100644
--- a/test/controllers/valuations_controller_test.rb
+++ b/test/controllers/valuations_controller_test.rb
@@ -8,35 +8,24 @@ class ValuationsControllerTest < ActionDispatch::IntegrationTest
@entry = entries(:valuation)
end
- test "error when valuation already exists for date" do
- assert_no_difference [ "Entry.count", "Valuation.count" ] do
- post valuations_url(@entry.account), params: {
- entry: {
- account_id: @entry.account_id,
- amount: 19800,
- date: @entry.date,
- currency: "USD"
- }
- }
- end
-
- assert_response :unprocessable_entity
- end
-
test "creates entry with basic attributes" do
+ account = accounts(:investment)
+
assert_difference [ "Entry.count", "Valuation.count" ], 1 do
post valuations_url, params: {
entry: {
- name: "New entry",
- amount: 10000,
+ amount: account.balance + 100,
currency: "USD",
- date: Date.current,
- account_id: @entry.account_id
+ date: Date.current.to_s,
+ account_id: account.id
}
}
end
created_entry = Entry.order(created_at: :desc).first
+ assert_equal "Manual account value update", created_entry.name
+ assert_equal Date.current, created_entry.date
+ assert_equal account.balance + 100, created_entry.amount_money.to_f
assert_enqueued_with job: SyncJob
@@ -47,7 +36,6 @@ class ValuationsControllerTest < ActionDispatch::IntegrationTest
assert_no_difference [ "Entry.count", "Valuation.count" ] do
patch valuation_url(@entry), params: {
entry: {
- name: "Updated entry",
amount: 20000,
currency: "USD",
date: Date.current
diff --git a/test/fixtures/accounts.yml b/test/fixtures/accounts.yml
index ec8668e5..8692522e 100644
--- a/test/fixtures/accounts.yml
+++ b/test/fixtures/accounts.yml
@@ -5,6 +5,7 @@ other_asset:
currency: USD
accountable_type: OtherAsset
accountable: one
+ status: active
other_liability:
family: dylan_family
@@ -13,6 +14,7 @@ other_liability:
currency: USD
accountable_type: OtherLiability
accountable: one
+ status: active
depository:
family: dylan_family
@@ -21,6 +23,7 @@ depository:
currency: USD
accountable_type: Depository
accountable: one
+ status: active
connected:
family: dylan_family
@@ -31,6 +34,7 @@ connected:
accountable_type: Depository
accountable: two
plaid_account: one
+ status: active
credit_card:
family: dylan_family
@@ -39,6 +43,7 @@ credit_card:
currency: USD
accountable_type: CreditCard
accountable: one
+ status: active
investment:
family: dylan_family
@@ -48,6 +53,7 @@ investment:
currency: USD
accountable_type: Investment
accountable: one
+ status: active
loan:
family: dylan_family
@@ -56,6 +62,7 @@ loan:
currency: USD
accountable_type: Loan
accountable: one
+ status: active
property:
family: dylan_family
@@ -64,6 +71,7 @@ property:
currency: USD
accountable_type: Property
accountable: one
+ status: active
vehicle:
family: dylan_family
@@ -72,6 +80,7 @@ vehicle:
currency: USD
accountable_type: Vehicle
accountable: one
+ status: active
crypto:
family: dylan_family
@@ -80,3 +89,4 @@ crypto:
currency: USD
accountable_type: Crypto
accountable: one
+ status: active
diff --git a/test/interfaces/accountable_resource_interface_test.rb b/test/interfaces/accountable_resource_interface_test.rb
index 551995de..ad5f5079 100644
--- a/test/interfaces/accountable_resource_interface_test.rb
+++ b/test/interfaces/accountable_resource_interface_test.rb
@@ -26,66 +26,4 @@ module AccountableResourceInterfaceTest
assert_enqueued_with job: DestroyJob
assert_equal "#{@account.accountable_name.underscore.humanize} account scheduled for deletion", flash[:notice]
end
-
- test "updates basic account balances" do
- assert_no_difference [ "Account.count", "@account.accountable_class.count" ] do
- patch account_url(@account), params: {
- account: {
- name: "Updated name",
- balance: 10000,
- currency: "USD"
- }
- }
- end
-
- assert_redirected_to @account
- assert_equal "#{@account.accountable_name.underscore.humanize} account updated", flash[:notice]
- end
-
- test "creates with basic attributes" do
- assert_difference [ "Account.count", "@account.accountable_class.count" ], 1 do
- post "/#{@account.accountable_name.pluralize}", params: {
- account: {
- accountable_type: @account.accountable_class,
- name: "New accountable",
- balance: 10000,
- currency: "USD",
- subtype: "checking"
- }
- }
- end
-
- assert_redirected_to Account.order(:created_at).last
- assert_equal "#{@account.accountable_name.humanize} account created", flash[:notice]
- end
-
- test "updates account balance by creating new valuation if balance has changed" do
- assert_difference [ "Entry.count", "Valuation.count" ], 1 do
- patch account_url(@account), params: {
- account: {
- balance: 12000
- }
- }
- end
-
- assert_redirected_to @account
- assert_enqueued_with job: SyncJob
- assert_equal "#{@account.accountable_name.humanize} account updated", flash[:notice]
- end
-
- test "updates account balance by editing existing valuation for today" do
- @account.entries.create! date: Date.current, amount: 6000, currency: "USD", name: "Balance update", entryable: Valuation.new
-
- assert_no_difference [ "Entry.count", "Valuation.count" ] do
- patch account_url(@account), params: {
- account: {
- balance: 12000
- }
- }
- end
-
- assert_redirected_to @account
- assert_enqueued_with job: SyncJob
- assert_equal "#{@account.accountable_name.humanize} account updated", flash[:notice]
- end
end
diff --git a/test/models/account/entry_test.rb b/test/models/account/entry_test.rb
index edd55e68..1cc6b478 100644
--- a/test/models/account/entry_test.rb
+++ b/test/models/account/entry_test.rb
@@ -67,21 +67,21 @@ class EntryTest < ActiveSupport::TestCase
assert_equal 0, family.entries.search(params).size
end
- test "active scope only returns entries from active accounts" do
+ test "visible scope only returns entries from visible accounts" do
# Create transactions for all account types
- active_transaction = create_transaction(account: accounts(:depository), name: "Active transaction")
- inactive_transaction = create_transaction(account: accounts(:credit_card), name: "Inactive transaction")
+ visible_transaction = create_transaction(account: accounts(:depository), name: "Visible transaction")
+ invisible_transaction = create_transaction(account: accounts(:credit_card), name: "Invisible transaction")
# Update account statuses
- accounts(:credit_card).update!(is_active: false)
+ accounts(:credit_card).disable!
# Test the scope
- active_entries = Entry.active
+ visible_entries = Entry.visible
# Should include entry from active account
- assert_includes active_entries, active_transaction
+ assert_includes visible_entries, visible_transaction
- # Should not include entry from inactive account
- assert_not_includes active_entries, inactive_transaction
+ # Should not include entry from disabled account
+ assert_not_includes visible_entries, invisible_transaction
end
end
diff --git a/test/models/balance_sheet_test.rb b/test/models/balance_sheet_test.rb
index c9478979..04906afa 100644
--- a/test/models/balance_sheet_test.rb
+++ b/test/models/balance_sheet_test.rb
@@ -39,7 +39,7 @@ class BalanceSheetTest < ActiveSupport::TestCase
create_account(balance: 10000, accountable: Depository.new)
other_liability = create_account(balance: 5000, accountable: OtherLiability.new)
- other_liability.update!(is_active: false)
+ other_liability.disable!
assert_equal 10000 - 1000, BalanceSheet.new(@family).net_worth
assert_equal 10000, BalanceSheet.new(@family).assets.total
diff --git a/test/models/family/auto_transfer_matchable_test.rb b/test/models/family/auto_transfer_matchable_test.rb
index 6f03ad85..6fdad008 100644
--- a/test/models/family/auto_transfer_matchable_test.rb
+++ b/test/models/family/auto_transfer_matchable_test.rb
@@ -89,7 +89,7 @@ class Family::AutoTransferMatchableTest < ActiveSupport::TestCase
end
test "does not consider inactive accounts when matching transfers" do
- @depository.update!(is_active: false)
+ @depository.disable!
outflow = create_transaction(date: Date.current, account: @depository, amount: 500)
inflow = create_transaction(date: Date.current, account: @credit_card, amount: -500)
diff --git a/test/system/accounts_test.rb b/test/system/accounts_test.rb
index c3cf8f45..e910a3ac 100644
--- a/test/system/accounts_test.rb
+++ b/test/system/accounts_test.rb
@@ -23,15 +23,40 @@ class AccountsTest < ApplicationSystemTestCase
end
test "can create property account" do
- assert_account_created "Property" do
- fill_in "Year built", with: 2005
- fill_in "Living area", with: 2250
- fill_in "Street address", with: "123 Main St"
- fill_in "City", with: "San Francisco"
- fill_in "State/Province", with: "CA"
- fill_in "ZIP/Postal code", with: "94101"
- fill_in "Country", with: "US"
- end
+ # Step 1: Select property type and enter basic details
+ click_link "Property"
+
+ account_name = "[system test] Property Account"
+ fill_in "Name*", with: account_name
+ select "Single Family Home", from: "Property type*"
+ fill_in "Year Built (optional)", with: 2005
+ fill_in "Area (optional)", with: 2250
+
+ click_button "Next"
+
+ # Step 2: Enter balance information
+ assert_text "Value"
+ fill_in "account[balance]", with: 500000
+ click_button "Next"
+
+ # Step 3: Enter address information
+ assert_text "Address"
+ fill_in "Address Line 1", with: "123 Main St"
+ fill_in "City", with: "San Francisco"
+ fill_in "State/Region", with: "CA"
+ fill_in "Postal Code", with: "94101"
+ fill_in "Country", with: "US"
+
+ click_button "Save"
+
+ # Verify account was created and is now active
+ assert_text account_name
+
+ created_account = Account.order(:created_at).last
+ assert_equal "active", created_account.status
+ assert_equal 500000, created_account.balance
+ assert_equal "123 Main St", created_account.property.address.line1
+ assert_equal "San Francisco", created_account.property.address.locality
end
test "can create vehicle account" do