mirror of
https://github.com/we-promise/sure.git
synced 2026-05-29 15:34:58 +00:00
feat(retirement): PR1 scaffold + preview-gated /retirement page
Lays the foundation for Retirement v2 as a preview feature stacked on Goals v2. Math, lens UI, pension sources and bucket all defer to later PRs; this PR ships only the data-model spine and a placeholder landing. - STI on goals: add `type` (default "Goal") + `user_id` columns; partial index for `Goal::Retirement` rows; check constraint requiring an owner on retirement rows. Existing goals backfill to `type='Goal'`; base `Goal#editable_by?` stays family-scoped. - `Goal::Retirement` subclass with single-user owner and `editable_by?` narrowed to owner-only. Parent depository-only linked-account validations no-op'd; PR2 introduces `RetirementBucketEntry`. - `families.retirement_disabled` killswitch (default false) + `Family#retirement_enabled?(user)` helper as tier 2 of the gate. Tier 1 is the existing `PreviewGateable` flow. - `RetirementController#show`: `require_preview_features!` then `ensure_module_enabled!` then a placeholder body. Unknown to users without preview features; 404 when the family killswitch is on (the feature behaves as if it does not exist). - Sidebar: new `sun`-icon entry after Goals, hidden unless the user has preview features AND the family has retirement enabled, so the killswitch hides the nav rather than leaving a link that 404s. - Locales: EN copy for nav, breadcrumb, page header, placeholder body, and the new `owner.must_belong_to_family` validation message under the goal model. DE deferred to PR4. - Tests: STI roundtrip, owner presence + family-membership validations, `editable_by?` on both Goal and Goal::Retirement, gate matrix on the controller, nav-item visibility under both preview and family flags, base-row STI backfill. Stack ahead: PR2 ships the data plane (PensionSource, statements, adjustments, bucket entries); PR3 wires the `Retirement::Fire::*` forecast engine + WHAT-IF Turbo Stream slider loop; PR4 lands the single combined-page UI per Claude's 2026-05-29 design (glide chart with hover-tooltip income breakdown, no separate stacked-area chart).
This commit is contained in:
26
app/controllers/retirement_controller.rb
Normal file
26
app/controllers/retirement_controller.rb
Normal file
@@ -0,0 +1,26 @@
|
||||
class RetirementController < ApplicationController
|
||||
include PreviewGateable
|
||||
|
||||
before_action :require_preview_features!
|
||||
before_action :ensure_module_enabled!
|
||||
before_action :load_goal_retirement
|
||||
|
||||
def show
|
||||
@breadcrumbs = [
|
||||
[ t("breadcrumbs.home"), root_path ],
|
||||
[ t("breadcrumbs.retirement"), nil ]
|
||||
]
|
||||
end
|
||||
|
||||
private
|
||||
def ensure_module_enabled!
|
||||
return if Current.family.retirement_enabled?(Current.user)
|
||||
raise ActionController::RoutingError, "Not Found"
|
||||
end
|
||||
|
||||
def load_goal_retirement
|
||||
@goal = Current.family.goals
|
||||
.where(type: "Goal::Retirement", user_id: Current.user.id)
|
||||
.first
|
||||
end
|
||||
end
|
||||
@@ -405,6 +405,16 @@ class Family < ApplicationRecord
|
||||
Rails.application.config.app_mode.self_hosted?
|
||||
end
|
||||
|
||||
# Tier 2 of the Retirement preview gate. Tier 1 is
|
||||
# User#preview_features_enabled? (read via PreviewGateable). The
|
||||
# retirement_disabled column defaults to false; admins flip it to hide
|
||||
# the feature household-wide without revoking individual preview
|
||||
# access. Per-user param futureproofs the v1.1 non-owner viewer split.
|
||||
def retirement_enabled?(user)
|
||||
return false if user.nil?
|
||||
!retirement_disabled?
|
||||
end
|
||||
|
||||
private
|
||||
def normalize_enabled_currencies!
|
||||
if enabled_currencies.blank?
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
class Goal < ApplicationRecord
|
||||
include AASM, Monetizable
|
||||
|
||||
self.inheritance_column = :type
|
||||
|
||||
COLORS = Category::COLORS
|
||||
ICONS = Category.icon_codes
|
||||
|
||||
@@ -399,6 +401,13 @@ class Goal < ApplicationRecord
|
||||
Money.new(delta, currency)
|
||||
end
|
||||
|
||||
# Family-scoped default for base Goal records. Subclasses like
|
||||
# Goal::Retirement narrow this to owner-only by overriding.
|
||||
def editable_by?(user)
|
||||
return false if user.nil?
|
||||
family_id == user.family_id
|
||||
end
|
||||
|
||||
private
|
||||
# Cleared after every AASM transition. The state column drives the
|
||||
# display_status / projection_summary memos; without this the same
|
||||
|
||||
26
app/models/goal/retirement.rb
Normal file
26
app/models/goal/retirement.rb
Normal file
@@ -0,0 +1,26 @@
|
||||
class Goal::Retirement < Goal
|
||||
belongs_to :owner, class_name: "User", foreign_key: :user_id
|
||||
|
||||
validates :owner, presence: true
|
||||
validate :owner_belongs_to_family
|
||||
|
||||
def editable_by?(user)
|
||||
return false if user.nil?
|
||||
user_id == user.id
|
||||
end
|
||||
|
||||
private
|
||||
# Retirement uses RetirementBucketEntry (PR2) for asset selection, not
|
||||
# the goal_accounts depository join. The parent validations operate on
|
||||
# goal_accounts, so they would always fail for retirement subtypes.
|
||||
def must_have_at_least_one_linked_account
|
||||
end
|
||||
|
||||
def linked_accounts_must_be_depository
|
||||
end
|
||||
|
||||
def owner_belongs_to_family
|
||||
return if owner.nil? || family_id.nil?
|
||||
errors.add(:owner, :must_belong_to_family) unless owner.family_id == family_id
|
||||
end
|
||||
end
|
||||
@@ -12,6 +12,7 @@ else
|
||||
{ name: t(".nav.reports"), path: reports_path, icon: "chart-bar", icon_custom: false, active: page_active?(reports_path) },
|
||||
{ name: t(".nav.budgets"), path: budgets_path, icon: "map", icon_custom: false, active: page_active?(budgets_path) },
|
||||
preview_gated_nav_item({ name: t(".nav.goals"), path: goals_path, icon: "piggy-bank", icon_custom: false, active: page_active?(goals_path) }),
|
||||
(preview_gated_nav_item({ name: t(".nav.retirement"), path: retirement_path, icon: "sun", icon_custom: false, active: page_active?(retirement_path) }) if Current.family.retirement_enabled?(Current.user)),
|
||||
{ name: t(".nav.assistant"), path: chats_path, icon: "icon-assistant", icon_custom: true, active: page_active?(chats_path), mobile_only: true }
|
||||
].compact
|
||||
end %>
|
||||
|
||||
26
app/views/retirement/show.html.erb
Normal file
26
app/views/retirement/show.html.erb
Normal file
@@ -0,0 +1,26 @@
|
||||
<% content_for :breadcrumbs do %>
|
||||
<%= render "layouts/shared/breadcrumbs", breadcrumbs: @breadcrumbs %>
|
||||
<% end %>
|
||||
|
||||
<div class="space-y-6">
|
||||
<header class="space-y-2">
|
||||
<h1 class="text-primary text-2xl font-semibold">
|
||||
<%= t("retirement.show.title") %>
|
||||
</h1>
|
||||
<p class="text-secondary text-sm">
|
||||
<%= t("retirement.show.subtitle") %>
|
||||
</p>
|
||||
</header>
|
||||
|
||||
<section class="bg-container shadow-border-xs rounded-xl p-8 text-center">
|
||||
<div class="mx-auto mb-4 w-12 h-12 flex items-center justify-center rounded-full bg-surface-inset">
|
||||
<%= icon("sun", size: "lg") %>
|
||||
</div>
|
||||
<p class="text-primary text-base font-medium mb-2">
|
||||
<%= t("retirement.show.placeholder_heading") %>
|
||||
</p>
|
||||
<p class="text-secondary text-sm max-w-prose mx-auto">
|
||||
<%= t("retirement.show.placeholder_body") %>
|
||||
</p>
|
||||
</section>
|
||||
</div>
|
||||
@@ -65,6 +65,7 @@ en:
|
||||
recurring_transactions: Recurring
|
||||
registrations: Sign up
|
||||
reports: Reports
|
||||
retirement: Retirement
|
||||
rules: Rules
|
||||
securities: Security
|
||||
security: Security
|
||||
|
||||
@@ -11,6 +11,7 @@ en:
|
||||
notes: Notes
|
||||
state: State
|
||||
linked_accounts: Linked accounts
|
||||
owner: Owner
|
||||
errors:
|
||||
models:
|
||||
goal:
|
||||
@@ -23,3 +24,5 @@ en:
|
||||
must_belong_to_family: Linked accounts must belong to the same family as the goal.
|
||||
currency:
|
||||
locked_after_linked: Can't change the currency after the goal is linked to accounts.
|
||||
owner:
|
||||
must_belong_to_family: must belong to the same family as the goal.
|
||||
|
||||
@@ -10,6 +10,7 @@ en:
|
||||
home: Home
|
||||
reports: Reports
|
||||
goals: Goals
|
||||
retirement: Retirement
|
||||
transactions: Transactions
|
||||
auth:
|
||||
existing_account: Already have an account?
|
||||
|
||||
8
config/locales/views/retirement/en.yml
Normal file
8
config/locales/views/retirement/en.yml
Normal file
@@ -0,0 +1,8 @@
|
||||
---
|
||||
en:
|
||||
retirement:
|
||||
show:
|
||||
title: Retirement
|
||||
subtitle: Plan when you can stop working — and what you'll need to get there.
|
||||
placeholder_heading: Retirement preview — coming soon
|
||||
placeholder_body: We're building a unified retirement planning surface. You'll be able to set a target spending number, log pension statements, and see your projected freedom date here.
|
||||
@@ -317,6 +317,8 @@ Rails.application.routes.draw do
|
||||
end
|
||||
end
|
||||
|
||||
resource :retirement, only: %i[show], controller: "retirement"
|
||||
|
||||
resources :family_merchants, only: %i[index new create edit update destroy] do
|
||||
collection do
|
||||
get :merge
|
||||
|
||||
29
db/migrate/20260529120000_add_sti_and_owner_to_goals.rb
Normal file
29
db/migrate/20260529120000_add_sti_and_owner_to_goals.rb
Normal file
@@ -0,0 +1,29 @@
|
||||
class AddStiAndOwnerToGoals < ActiveRecord::Migration[7.2]
|
||||
def up
|
||||
add_column :goals, :type, :string, default: "Goal"
|
||||
add_column :goals, :user_id, :uuid
|
||||
|
||||
execute "UPDATE goals SET type = 'Goal' WHERE type IS NULL"
|
||||
change_column_null :goals, :type, false
|
||||
|
||||
add_foreign_key :goals, :users, column: :user_id, on_delete: :restrict
|
||||
add_index :goals, [ :user_id, :type ],
|
||||
where: "type = 'Goal::Retirement'",
|
||||
name: "index_goals_on_user_and_type_retirement"
|
||||
add_index :goals, [ :family_id, :type, :state ],
|
||||
name: "index_goals_on_family_type_state"
|
||||
|
||||
add_check_constraint :goals,
|
||||
"type <> 'Goal::Retirement' OR user_id IS NOT NULL",
|
||||
name: "chk_goals_retirement_requires_owner"
|
||||
end
|
||||
|
||||
def down
|
||||
remove_check_constraint :goals, name: "chk_goals_retirement_requires_owner"
|
||||
remove_index :goals, name: "index_goals_on_family_type_state"
|
||||
remove_index :goals, name: "index_goals_on_user_and_type_retirement"
|
||||
remove_foreign_key :goals, column: :user_id
|
||||
remove_column :goals, :user_id
|
||||
remove_column :goals, :type
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,5 @@
|
||||
class AddRetirementDisabledToFamilies < ActiveRecord::Migration[7.2]
|
||||
def change
|
||||
add_column :families, :retirement_disabled, :boolean, default: false, null: false
|
||||
end
|
||||
end
|
||||
31
db/schema.rb
generated
31
db/schema.rb
generated
@@ -10,7 +10,7 @@
|
||||
#
|
||||
# It's strongly recommended that you check this file into your version control system.
|
||||
|
||||
ActiveRecord::Schema[7.2].define(version: 2026_05_19_100000) do
|
||||
ActiveRecord::Schema[7.2].define(version: 2026_05_29_120001) do
|
||||
# These are extensions that must be enabled in order to support this database
|
||||
enable_extension "pgcrypto"
|
||||
enable_extension "plpgsql"
|
||||
@@ -53,7 +53,6 @@ ActiveRecord::Schema[7.2].define(version: 2026_05_19_100000) do
|
||||
t.string "content_type", limit: 100, null: false
|
||||
t.bigint "byte_size", null: false
|
||||
t.string "checksum", limit: 64, null: false
|
||||
t.string "content_sha256"
|
||||
t.string "source", default: "manual_upload", null: false
|
||||
t.string "upload_status", default: "stored", null: false
|
||||
t.string "institution_name_hint", limit: 200
|
||||
@@ -70,6 +69,7 @@ ActiveRecord::Schema[7.2].define(version: 2026_05_19_100000) do
|
||||
t.jsonb "sanitized_parser_output", default: {}, null: false
|
||||
t.datetime "created_at", null: false
|
||||
t.datetime "updated_at", null: false
|
||||
t.string "content_sha256"
|
||||
t.index ["account_id", "period_start_on", "period_end_on"], name: "index_account_statements_on_account_period"
|
||||
t.index ["account_id"], name: "index_account_statements_on_account_id"
|
||||
t.index ["family_id", "checksum"], name: "index_account_statements_on_family_checksum"
|
||||
@@ -254,9 +254,9 @@ ActiveRecord::Schema[7.2].define(version: 2026_05_19_100000) do
|
||||
t.string "institution_domain"
|
||||
t.string "institution_url"
|
||||
t.string "institution_color"
|
||||
t.string "status", default: "good"
|
||||
t.boolean "scheduled_for_deletion", default: false
|
||||
t.boolean "pending_account_setup", default: false
|
||||
t.string "status", default: "good", null: false
|
||||
t.boolean "scheduled_for_deletion", default: false, null: false
|
||||
t.boolean "pending_account_setup", default: false, null: false
|
||||
t.datetime "sync_start_date"
|
||||
t.jsonb "raw_payload"
|
||||
t.text "api_key"
|
||||
@@ -500,7 +500,7 @@ ActiveRecord::Schema[7.2].define(version: 2026_05_19_100000) do
|
||||
t.index ["provider_key"], name: "index_debug_log_entries_on_provider_key"
|
||||
t.index ["source"], name: "index_debug_log_entries_on_source"
|
||||
t.index ["user_id"], name: "index_debug_log_entries_on_user_id"
|
||||
t.check_constraint "level::text = ANY (ARRAY['debug'::character varying, 'info'::character varying, 'warn'::character varying, 'error'::character varying]::text[])", name: "chk_debug_log_entries_level"
|
||||
t.check_constraint "level::text = ANY (ARRAY['debug'::character varying::text, 'info'::character varying::text, 'warn'::character varying::text, 'error'::character varying::text])", name: "chk_debug_log_entries_level"
|
||||
end
|
||||
|
||||
create_table "depositories", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
|
||||
@@ -719,6 +719,7 @@ ActiveRecord::Schema[7.2].define(version: 2026_05_19_100000) do
|
||||
t.string "default_account_sharing", default: "shared", null: false
|
||||
t.string "enabled_currencies", array: true
|
||||
t.datetime "last_sync_all_attempted_at"
|
||||
t.boolean "retirement_disabled", default: false, null: false
|
||||
t.check_constraint "default_account_sharing::text = ANY (ARRAY['shared'::character varying::text, 'private'::character varying::text])", name: "chk_families_default_account_sharing"
|
||||
t.check_constraint "month_start_day >= 1 AND month_start_day <= 28", name: "month_start_day_range"
|
||||
end
|
||||
@@ -798,11 +799,16 @@ ActiveRecord::Schema[7.2].define(version: 2026_05_19_100000) do
|
||||
t.datetime "created_at", null: false
|
||||
t.datetime "updated_at", null: false
|
||||
t.string "icon"
|
||||
t.string "type", default: "Goal", null: false
|
||||
t.uuid "user_id"
|
||||
t.index ["family_id", "state"], name: "index_goals_on_family_id_and_state"
|
||||
t.index ["family_id", "type", "state"], name: "index_goals_on_family_type_state"
|
||||
t.index ["family_id"], name: "index_goals_on_family_id"
|
||||
t.index ["user_id", "type"], name: "index_goals_on_user_and_type_retirement", where: "((type)::text = 'Goal::Retirement'::text)"
|
||||
t.check_constraint "char_length(name::text) <= 255", name: "chk_savings_goals_name_length"
|
||||
t.check_constraint "state::text = ANY (ARRAY['active'::character varying::text, 'paused'::character varying::text, 'completed'::character varying::text, 'archived'::character varying::text])", name: "chk_savings_goals_state_enum"
|
||||
t.check_constraint "state::text = ANY (ARRAY['active'::character varying, 'paused'::character varying, 'completed'::character varying, 'archived'::character varying]::text[])", name: "chk_savings_goals_state_enum"
|
||||
t.check_constraint "target_amount > 0::numeric", name: "chk_savings_goals_target_amount_positive"
|
||||
t.check_constraint "type::text <> 'Goal::Retirement'::text OR user_id IS NOT NULL", name: "chk_goals_retirement_requires_owner"
|
||||
end
|
||||
|
||||
create_table "holdings", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
|
||||
@@ -838,9 +844,9 @@ ActiveRecord::Schema[7.2].define(version: 2026_05_19_100000) do
|
||||
t.decimal "current_balance", precision: 19, scale: 4
|
||||
t.decimal "cash_balance", precision: 19, scale: 4
|
||||
t.jsonb "institution_metadata"
|
||||
t.jsonb "raw_holdings_payload", default: [], null: false
|
||||
t.jsonb "raw_activities_payload", default: {}, null: false
|
||||
t.jsonb "raw_cash_report_payload", default: [], null: false
|
||||
t.jsonb "raw_holdings_payload", default: []
|
||||
t.jsonb "raw_activities_payload", default: {}
|
||||
t.jsonb "raw_cash_report_payload", default: []
|
||||
t.date "report_date"
|
||||
t.datetime "last_holdings_sync"
|
||||
t.datetime "last_activities_sync"
|
||||
@@ -854,8 +860,8 @@ ActiveRecord::Schema[7.2].define(version: 2026_05_19_100000) do
|
||||
create_table "ibkr_items", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
|
||||
t.uuid "family_id", null: false
|
||||
t.string "name"
|
||||
t.string "status", default: "good", null: false
|
||||
t.boolean "scheduled_for_deletion", default: false, null: false
|
||||
t.string "status", default: "good"
|
||||
t.boolean "scheduled_for_deletion", default: false
|
||||
t.boolean "pending_account_setup", default: false, null: false
|
||||
t.jsonb "raw_payload"
|
||||
t.string "query_id"
|
||||
@@ -1977,6 +1983,7 @@ ActiveRecord::Schema[7.2].define(version: 2026_05_19_100000) do
|
||||
add_foreign_key "goal_pledges", "goals", on_delete: :cascade
|
||||
add_foreign_key "goal_pledges", "transactions", column: "matched_transaction_id", on_delete: :nullify
|
||||
add_foreign_key "goals", "families", on_delete: :cascade
|
||||
add_foreign_key "goals", "users", on_delete: :restrict
|
||||
add_foreign_key "holdings", "account_providers"
|
||||
add_foreign_key "holdings", "accounts", on_delete: :cascade
|
||||
add_foreign_key "holdings", "securities"
|
||||
|
||||
58
test/controllers/retirement_controller_test.rb
Normal file
58
test/controllers/retirement_controller_test.rb
Normal file
@@ -0,0 +1,58 @@
|
||||
require "test_helper"
|
||||
|
||||
class RetirementControllerTest < ActionDispatch::IntegrationTest
|
||||
setup do
|
||||
@user = users(:family_admin)
|
||||
@family = @user.family
|
||||
@user.update!(preferences: (@user.preferences || {}).merge("preview_features_enabled" => true))
|
||||
@family.update!(retirement_disabled: false)
|
||||
sign_in @user
|
||||
ensure_tailwind_build
|
||||
end
|
||||
|
||||
test "redirects when preview features disabled" do
|
||||
@user.update!(preferences: (@user.preferences || {}).merge("preview_features_enabled" => false))
|
||||
|
||||
get retirement_url
|
||||
|
||||
assert_redirected_to root_path
|
||||
assert_match(/preview/i, flash[:alert])
|
||||
end
|
||||
|
||||
test "404 when family retirement_disabled is true" do
|
||||
@family.update!(retirement_disabled: true)
|
||||
|
||||
get retirement_url
|
||||
|
||||
assert_response :not_found
|
||||
end
|
||||
|
||||
test "200 when preview features and family flag both allow" do
|
||||
get retirement_url
|
||||
|
||||
assert_response :success
|
||||
assert_match(/Retirement/i, response.body)
|
||||
end
|
||||
|
||||
test "nav item rendered when preview enabled" do
|
||||
get root_url
|
||||
|
||||
assert_select "a[href=?]", retirement_path
|
||||
end
|
||||
|
||||
test "nav item hidden when preview disabled" do
|
||||
@user.update!(preferences: (@user.preferences || {}).merge("preview_features_enabled" => false))
|
||||
|
||||
get root_url
|
||||
|
||||
assert_select "a[href=?]", retirement_path, count: 0
|
||||
end
|
||||
|
||||
test "nav item hidden when family retirement disabled" do
|
||||
@family.update!(retirement_disabled: true)
|
||||
|
||||
get root_url
|
||||
|
||||
assert_select "a[href=?]", retirement_path, count: 0
|
||||
end
|
||||
end
|
||||
76
test/models/goal/retirement_test.rb
Normal file
76
test/models/goal/retirement_test.rb
Normal file
@@ -0,0 +1,76 @@
|
||||
require "test_helper"
|
||||
|
||||
class Goal::RetirementTest < ActiveSupport::TestCase
|
||||
setup do
|
||||
@family = families(:dylan_family)
|
||||
@owner = users(:family_admin)
|
||||
@sibling = users(:family_member)
|
||||
@stranger = users(:empty)
|
||||
end
|
||||
|
||||
test "STI: Goal.find returns Goal::Retirement instance" do
|
||||
retirement = Goal::Retirement.create!(
|
||||
family: @family,
|
||||
owner: @owner,
|
||||
name: "Retirement",
|
||||
target_amount: 1_000_000,
|
||||
currency: "USD"
|
||||
)
|
||||
|
||||
fetched = Goal.find(retirement.id)
|
||||
assert_instance_of Goal::Retirement, fetched
|
||||
assert_equal "Goal::Retirement", fetched.type
|
||||
end
|
||||
|
||||
test "requires owner" do
|
||||
retirement = Goal::Retirement.new(
|
||||
family: @family,
|
||||
name: "Retirement",
|
||||
target_amount: 1_000_000,
|
||||
currency: "USD"
|
||||
)
|
||||
|
||||
assert_not retirement.valid?
|
||||
assert_includes retirement.errors[:owner], "can't be blank"
|
||||
end
|
||||
|
||||
test "owner must belong to family" do
|
||||
retirement = Goal::Retirement.new(
|
||||
family: @family,
|
||||
owner: @stranger,
|
||||
name: "Retirement",
|
||||
target_amount: 1_000_000,
|
||||
currency: "USD"
|
||||
)
|
||||
|
||||
assert_not retirement.valid?
|
||||
assert_match(/belong to the same family/i, retirement.errors[:owner].join(" "))
|
||||
end
|
||||
|
||||
test "editable_by? owner true, sibling false, stranger false, nil false" do
|
||||
retirement = Goal::Retirement.create!(
|
||||
family: @family,
|
||||
owner: @owner,
|
||||
name: "Retirement",
|
||||
target_amount: 1_000_000,
|
||||
currency: "USD"
|
||||
)
|
||||
|
||||
assert retirement.editable_by?(@owner)
|
||||
assert_not retirement.editable_by?(@sibling)
|
||||
assert_not retirement.editable_by?(@stranger)
|
||||
assert_not retirement.editable_by?(nil)
|
||||
end
|
||||
|
||||
test "no linked_accounts required (parent validation bypassed)" do
|
||||
retirement = Goal::Retirement.new(
|
||||
family: @family,
|
||||
owner: @owner,
|
||||
name: "Retirement",
|
||||
target_amount: 1_000_000,
|
||||
currency: "USD"
|
||||
)
|
||||
|
||||
assert retirement.valid?, retirement.errors.full_messages.to_sentence
|
||||
end
|
||||
end
|
||||
@@ -248,4 +248,17 @@ class GoalTest < ActiveSupport::TestCase
|
||||
reloaded = Goal.find(@goal.id)
|
||||
assert_equal "goals.show.pledge_just_saved", reloaded.pledge_action_label_key
|
||||
end
|
||||
|
||||
test "STI: no Goal rows have NULL type after migration" do
|
||||
assert_equal 0, Goal.where(type: nil).count
|
||||
end
|
||||
|
||||
test "editable_by? base Goal: same family user true, foreign family false, nil false" do
|
||||
same_family_user = users(:family_admin)
|
||||
foreign_user = users(:empty)
|
||||
|
||||
assert @goal.editable_by?(same_family_user)
|
||||
assert_not @goal.editable_by?(foreign_user)
|
||||
assert_not @goal.editable_by?(nil)
|
||||
end
|
||||
end
|
||||
|
||||
Reference in New Issue
Block a user