Files
sure/app/controllers/holdings_controller.rb
soky srm 560c9fbff3 Family sharing (#1272)
* Initial account sharing changes

* Update schema.rb

* Update schema.rb

* Change sharing UI to modal

* UX fixes and sharing controls

* Scope include in finances better

* Update totals.rb

* Update totals.rb

* Scope reports to finance account scope

* Update impersonation_sessions_controller_test.rb

* Review fixes

* Update schema.rb

* Update show.html.erb

* FIX db validation

* Refine edit permissions

* Review items

* Review

* Review

* Add application level helper

* Critical review

* Address remaining review items

* Fix modals

* more scoping

* linter

* small UI fix

* Fix: Sync broadcasts push unscoped balance sheet to all users

* Update sync_complete_event.rb

 The fix removes the sidebar broadcasts (which rendered unscoped account groups using family.balance_sheet without user context)
  along with the now-unused sidebar_targets, account_group, and family_balance_sheet private methods.

  The sidebar will still update correctly — when the sync completes, Family::SyncCompleteEvent#broadcast fires family.broadcast_refresh, which triggers a
  morph-based page refresh for each user with their own authenticated session, rendering properly scoped sidebar content.
2026-03-25 10:50:23 +01:00

163 lines
5.5 KiB
Ruby

class HoldingsController < ApplicationController
include StreamExtensions
before_action :set_holding, only: %i[show update destroy unlock_cost_basis remap_security reset_security sync_prices]
before_action :require_holding_write_permission!, only: %i[update destroy unlock_cost_basis remap_security reset_security]
def index
@account = accessible_accounts.find(params[:account_id])
end
def show
@last_price_updated = @holding.security.prices.maximum(:updated_at)
end
def update
total_cost_basis = holding_params[:cost_basis].to_d
if total_cost_basis >= 0 && @holding.qty.positive?
# Convert total cost basis to per-share cost (the cost_basis field stores per-share)
# Zero is valid for gifted/inherited shares
per_share_cost = total_cost_basis / @holding.qty
@holding.set_manual_cost_basis!(per_share_cost)
flash[:notice] = t(".success")
else
flash[:alert] = t(".error")
end
# Redirect to account page holdings tab to refresh list and close drawer
redirect_to account_path(@holding.account, tab: "holdings")
end
def unlock_cost_basis
@holding.unlock_cost_basis!
flash[:notice] = t(".success")
# Redirect to account page holdings tab to refresh list and close drawer
redirect_to account_path(@holding.account, tab: "holdings")
end
def destroy
if @holding.account.can_delete_holdings?
@holding.destroy_holding_and_entries!
flash[:notice] = t(".success")
else
flash[:alert] = "You cannot delete this holding"
end
respond_to do |format|
format.html { redirect_back_or_to account_path(@holding.account) }
format.turbo_stream { render turbo_stream: turbo_stream.action(:redirect, account_path(@holding.account)) }
end
end
def remap_security
# Combobox returns "TICKER|EXCHANGE" format
ticker, exchange = params[:security_id].to_s.split("|")
# Validate ticker is present (form has required: true, but can be bypassed)
if ticker.blank?
flash[:alert] = t(".security_not_found")
redirect_to account_path(@holding.account, tab: "holdings")
return
end
new_security = Security::Resolver.new(
ticker,
exchange_operating_mic: exchange,
country_code: Current.family.country
).resolve
if new_security.nil?
flash[:alert] = t(".security_not_found")
redirect_to account_path(@holding.account, tab: "holdings")
return
end
# The user explicitly selected this security from provider search results,
# so we know the provider can handle it. Bring it back online if it was
# previously marked offline (e.g. by a failed QIF import resolution).
if new_security.offline?
new_security.update!(offline: false, failed_fetch_count: 0, failed_fetch_at: nil)
end
@holding.remap_security!(new_security)
flash[:notice] = t(".success")
respond_to do |format|
format.html { redirect_to account_path(@holding.account, tab: "holdings") }
format.turbo_stream { render turbo_stream: turbo_stream.action(:redirect, account_path(@holding.account, tab: "holdings")) }
end
end
def sync_prices
security = @holding.security
if security.offline?
redirect_to account_path(@holding.account, tab: "holdings"),
alert: t("holdings.sync_prices.unavailable")
return
end
prices_updated, @provider_error = security.import_provider_prices(
start_date: 31.days.ago.to_date,
end_date: Date.current,
clear_cache: true
)
security.import_provider_details
@last_price_updated = @holding.security.prices.maximum(:updated_at)
if prices_updated == 0
@provider_error = @provider_error.presence || t("holdings.sync_prices.provider_error")
respond_to do |format|
format.html { redirect_to account_path(@holding.account, tab: "holdings"), alert: @provider_error }
format.turbo_stream
end
return
end
strategy = @holding.account.linked? ? :reverse : :forward
Balance::Materializer.new(@holding.account, strategy: strategy, security_ids: [ @holding.security_id ]).materialize_balances
@holding.reload
@last_price_updated = @holding.security.prices.maximum(:updated_at)
respond_to do |format|
format.html { redirect_to account_path(@holding.account, tab: "holdings"), notice: t("holdings.sync_prices.success") }
format.turbo_stream
end
end
def reset_security
@holding.reset_security_to_provider!
flash[:notice] = t(".success")
respond_to do |format|
format.html { redirect_to account_path(@holding.account, tab: "holdings") }
format.turbo_stream { render turbo_stream: turbo_stream.action(:redirect, account_path(@holding.account, tab: "holdings")) }
end
end
private
def set_holding
@holding = Current.family.holdings
.joins(:account)
.merge(Account.accessible_by(Current.user))
.find(params[:id])
end
def require_holding_write_permission!
permission = @holding.account.permission_for(Current.user)
unless permission.in?([ :owner, :full_control ])
respond_to do |format|
format.html { redirect_back_or_to account_path(@holding.account), alert: t("accounts.not_authorized") }
format.turbo_stream { stream_redirect_back_or_to(account_path(@holding.account), alert: t("accounts.not_authorized")) }
end
end
end
def holding_params
params.require(:holding).permit(:cost_basis)
end
end