diff --git a/app/models/account.rb b/app/models/account.rb index a2c144b8e..77e9285c5 100644 --- a/app/models/account.rb +++ b/app/models/account.rb @@ -95,9 +95,18 @@ class Account < ApplicationRecord delegated_type :accountable, types: Accountable::TYPES, dependent: :destroy delegate :subtype, to: :accountable, allow_nil: true - # Writer for subtype that delegates to the accountable - # This allows forms to set subtype directly on the account + # Writer for subtype that delegates to the accountable. + # This allows forms to set subtype directly on the account. + # + # On create the accountable may not be built yet: mass-assignment can apply + # `subtype` before `accountable_attributes` (which is what builds the + # accountable via accepts_nested_attributes_for). With no accountable in place + # `accountable&.subtype = value` is a silent no-op and the chosen subtype is + # dropped. Build the accountable from the delegated type first so the value is + # preserved; the later `accountable_attributes` assignment (update_only) then + # updates this same record instead of building a new one. def subtype=(value) + self.accountable = accountable_class.new if accountable.nil? && accountable_type.present? accountable&.subtype = value end diff --git a/test/models/account_test.rb b/test/models/account_test.rb index 0d7013354..373e1057e 100644 --- a/test/models/account_test.rb +++ b/test/models/account_test.rb @@ -99,6 +99,36 @@ class AccountTest < ActiveSupport::TestCase assert_equal opening_date, opening_anchor.entry.date end + test "subtype set as a top-level account attribute persists on create" do + Account.any_instance.stubs(:sync_later) + + # Mirrors the create flow: the form submits `account[subtype]` as a + # top-level attribute (not nested under accountable_attributes). The + # accountable does not exist yet, so the delegating writer must build it. + account = Account.create_and_sync({ + family: @family, + owner: @admin, + name: "Savings Account", + balance: 100, + currency: "USD", + accountable_type: "Depository", + subtype: "savings" + }) + + assert account.persisted? + assert_equal "savings", account.reload.subtype + assert_equal "savings", account.accountable.subtype + end + + test "subtype assigned before accountable is built is not dropped" do + account = Account.new + account.accountable_type = "Depository" + account.subtype = "checking" + + assert_not_nil account.accountable + assert_equal "checking", account.subtype + end + test "accountable display names expose singular and group contexts" do assert_equal "Investment", Investment.singular_display_name assert_equal "Investments", Investment.display_name