1
0
Fork 0
mirror of https://github.com/maybe-finance/maybe.git synced 2025-07-24 15:49:39 +02:00

Account:: namespace simplifications and cleanup (#2110)

* Flatten Holding model

* Flatten balance model

* Entries domain renames

* Fix valuations reference

* Fix trades stream

* Fix brakeman warnings

* Fix tests

* Replace existing entryable type references in DB
This commit is contained in:
Zach Gollwitzer 2025-04-14 11:40:34 -04:00 committed by GitHub
parent f181ba941f
commit e657c40d19
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
172 changed files with 1297 additions and 1258 deletions

View file

@ -1,37 +0,0 @@
class Account::TradesController < ApplicationController
include EntryableResource
permitted_entryable_attributes :id, :qty, :price
private
def build_entry
Account::TradeBuilder.new(create_entry_params)
end
def create_entry_params
params.require(:account_entry).permit(
:account_id, :date, :amount, :currency, :qty, :price, :ticker, :manual_ticker, :type, :transfer_account_id
).tap do |params|
account_id = params.delete(:account_id)
params[:account] = Current.family.accounts.find(account_id)
end
end
def update_entry_params
return entry_params unless entry_params[:entryable_attributes].present?
update_params = entry_params
update_params = update_params.merge(entryable_type: "Account::Trade")
qty = update_params[:entryable_attributes][:qty]
price = update_params[:entryable_attributes][:price]
if qty.present? && price.present?
qty = update_params[:nature] == "inflow" ? -qty.to_d : qty.to_d
update_params[:entryable_attributes][:qty] = qty
update_params[:amount] = qty * price.to_d
end
update_params.except(:nature)
end
end

View file

@ -1,22 +0,0 @@
class Account::TransactionCategoriesController < ApplicationController
def update
@entry = Current.family.entries.account_transactions.find(params[:transaction_id])
@entry.update!(entry_params)
respond_to do |format|
format.html { redirect_back_or_to account_transaction_path(@entry) }
format.turbo_stream do
render turbo_stream: turbo_stream.replace(
"category_menu_account_transaction_#{@entry.account_transaction_id}",
partial: "categories/menu",
locals: { transaction: @entry.account_transaction }
)
end
end
end
private
def entry_params
params.require(:account_entry).permit(:entryable_type, entryable_attributes: [ :id, :category_id ])
end
end

View file

@ -1,37 +0,0 @@
class Account::TransactionsController < ApplicationController
include EntryableResource
permitted_entryable_attributes :id, :category_id, :merchant_id, { tag_ids: [] }
def bulk_delete
destroyed = Current.family.entries.destroy_by(id: bulk_delete_params[:entry_ids])
destroyed.map(&:account).uniq.each(&:sync_later)
redirect_back_or_to transactions_url, notice: t(".success", count: destroyed.count)
end
def bulk_edit
end
def bulk_update
updated = Current.family
.entries
.where(id: bulk_update_params[:entry_ids])
.bulk_update!(bulk_update_params)
redirect_back_or_to transactions_url, notice: t(".success", count: updated)
end
private
def bulk_delete_params
params.require(:bulk_delete).permit(entry_ids: [])
end
def bulk_update_params
params.require(:bulk_update).permit(:date, :notes, :category_id, :merchant_id, entry_ids: [], tag_ids: [])
end
def search_params
params.fetch(:q, {})
.permit(:start_date, :end_date, :search, :amount, :amount_operator, accounts: [], account_ids: [], categories: [], merchants: [], types: [], tags: [])
end
end

View file

@ -1,3 +0,0 @@
class Account::ValuationsController < ApplicationController
include EntryableResource
end

View file

@ -11,14 +11,14 @@ class BudgetCategoriesController < ApplicationController
if params[:id] == BudgetCategory.uncategorized.id
@budget_category = @budget.uncategorized_budget_category
@recent_transactions = @recent_transactions.where(account_transactions: { category_id: nil })
@recent_transactions = @recent_transactions.where(transactions: { category_id: nil })
else
@budget_category = Current.family.budget_categories.find(params[:id])
@recent_transactions = @recent_transactions.joins("LEFT JOIN categories ON categories.id = account_transactions.category_id")
@recent_transactions = @recent_transactions.joins("LEFT JOIN categories ON categories.id = transactions.category_id")
.where("categories.id = ? OR categories.parent_id = ?", @budget_category.category.id, @budget_category.category.id)
end
@recent_transactions = @recent_transactions.order("account_entries.date DESC, ABS(account_entries.amount) DESC").take(3)
@recent_transactions = @recent_transactions.order("entries.date DESC, ABS(entries.amount) DESC").take(3)
end
def update

View file

@ -2,14 +2,9 @@ module EntryableResource
extend ActiveSupport::Concern
included do
before_action :set_entry, only: %i[show update destroy]
end
include StreamExtensions, ActionView::RecordIdentifier
class_methods do
def permitted_entryable_attributes(*attrs)
@permitted_entryable_attributes = attrs if attrs.any?
@permitted_entryable_attributes ||= [ :id ]
end
before_action :set_entry, only: %i[show update destroy]
end
def show
@ -21,49 +16,16 @@ module EntryableResource
@entry = Current.family.entries.new(
account: account,
currency: account ? account.currency : Current.family.currency,
entryable: entryable_type.new
entryable: entryable
)
end
def create
@entry = build_entry
if @entry.save
@entry.sync_account_later
flash[:notice] = t("account.entries.create.success")
respond_to do |format|
format.html { redirect_back_or_to account_path(@entry.account) }
redirect_target_url = request.referer || account_path(@entry.account)
format.turbo_stream { render turbo_stream: turbo_stream.action(:redirect, redirect_target_url) }
end
else
render :new, status: :unprocessable_entity
end
raise NotImplementedError, "Entryable resources must implement #create"
end
def update
if @entry.update(update_entry_params)
@entry.sync_account_later
respond_to do |format|
format.html { redirect_back_or_to account_path(@entry.account), notice: t("account.entries.update.success") }
format.turbo_stream do
render turbo_stream: [
turbo_stream.replace(
"header_account_entry_#{@entry.id}",
partial: "#{entryable_type.name.underscore.pluralize}/header",
locals: { entry: @entry }
),
turbo_stream.replace("account_entry_#{@entry.id}", partial: "account/entries/entry", locals: { entry: @entry })
]
end
end
else
render :show, status: :unprocessable_entity
end
raise NotImplementedError, "Entryable resources must implement #update"
end
def destroy
@ -71,58 +33,15 @@ module EntryableResource
@entry.destroy!
@entry.sync_account_later
flash[:notice] = t("account.entries.destroy.success")
respond_to do |format|
format.html { redirect_back_or_to account_path(account) }
redirect_target_url = request.referer || account_path(@entry.account)
format.turbo_stream { render turbo_stream: turbo_stream.action(:redirect, redirect_target_url) }
end
redirect_back_or_to account_path(account), notice: t("account.entries.destroy.success")
end
private
def entryable_type
permitted_entryable_types = %w[Account::Transaction Account::Valuation Account::Trade]
klass = params[:entryable_type] || "Account::#{controller_name.classify}"
klass.constantize if permitted_entryable_types.include?(klass)
def entryable
controller_name.classify.constantize.new
end
def set_entry
@entry = Current.family.entries.find(params[:id])
end
def build_entry
Current.family.entries.new(create_entry_params)
end
def update_entry_params
prepared_entry_params
end
def create_entry_params
prepared_entry_params.merge({
entryable_type: entryable_type.name,
entryable_attributes: entry_params[:entryable_attributes] || {}
})
end
def prepared_entry_params
default_params = entry_params.except(:nature)
default_params = default_params.merge(entryable_type: entryable_type.name) if entry_params[:entryable_attributes].present?
if entry_params[:nature].present? && entry_params[:amount].present?
signed_amount = entry_params[:nature] == "inflow" ? -entry_params[:amount].to_d : entry_params[:amount].to_d
default_params = default_params.merge(amount: signed_amount)
end
default_params
end
def entry_params
params.require(:account_entry).permit(
:account_id, :name, :enriched_name, :date, :amount, :currency, :excluded, :notes, :nature,
entryable_attributes: self.class.permitted_entryable_attributes
)
end
end

View file

@ -0,0 +1,20 @@
module StreamExtensions
extend ActiveSupport::Concern
def stream_redirect_to(path, notice: nil, alert: nil)
custom_stream_redirect(path, notice: notice, alert: alert)
end
def stream_redirect_back_or_to(path, notice: nil, alert: nil)
custom_stream_redirect(path, redirect_back: true, notice: notice, alert: alert)
end
private
def custom_stream_redirect(path, redirect_back: false, notice: nil, alert: nil)
flash[:notice] = notice if notice.present?
flash[:alert] = alert if alert.present?
redirect_target_url = redirect_back ? request.referer : path
render turbo_stream: turbo_stream.action(:redirect, redirect_target_url)
end
end

View file

@ -1,4 +1,4 @@
class Account::HoldingsController < ApplicationController
class HoldingsController < ApplicationController
before_action :set_holding, only: %i[show destroy]
def index

View file

@ -0,0 +1,79 @@
class TradesController < ApplicationController
include EntryableResource
def create
@entry = build_entry
if @entry.save
@entry.sync_account_later
flash[:notice] = t("entries.create.success")
respond_to do |format|
format.html { redirect_back_or_to account_path(@entry.account) }
format.turbo_stream { stream_redirect_back_or_to account_path(@entry.account) }
end
else
render :new, status: :unprocessable_entity
end
end
def update
if @entry.update(update_entry_params)
@entry.sync_account_later
respond_to do |format|
format.html { redirect_back_or_to account_path(@entry.account), notice: t("entries.update.success") }
format.turbo_stream do
render turbo_stream: [
turbo_stream.replace(
"header_entry_#{@entry.id}",
partial: "trades/header",
locals: { entry: @entry }
),
turbo_stream.replace("entry_#{@entry.id}", partial: "entries/entry", locals: { entry: @entry })
]
end
end
else
render :show, status: :unprocessable_entity
end
end
private
def build_entry
account = Current.family.accounts.find(params.dig(:entry, :account_id))
TradeBuilder.new(create_entry_params.merge(account: account))
end
def entry_params
params.require(:entry).permit(
:name, :enriched_name, :date, :amount, :currency, :excluded, :notes, :nature,
entryable_attributes: [ :id, :qty, :price ]
)
end
def create_entry_params
params.require(:entry).permit(
:date, :amount, :currency, :qty, :price, :ticker, :manual_ticker, :type, :transfer_account_id
)
end
def update_entry_params
return entry_params unless entry_params[:entryable_attributes].present?
update_params = entry_params
update_params = update_params.merge(entryable_type: "Trade")
qty = update_params[:entryable_attributes][:qty]
price = update_params[:entryable_attributes][:price]
if qty.present? && price.present?
qty = update_params[:nature] == "inflow" ? -qty.to_d : qty.to_d
update_params[:entryable_attributes][:qty] = qty
update_params[:amount] = qty * price.to_d
end
update_params.except(:nature)
end
end

View file

@ -0,0 +1,22 @@
class TransactionCategoriesController < ApplicationController
def update
@entry = Current.family.entries.transactions.find(params[:transaction_id])
@entry.update!(entry_params)
respond_to do |format|
format.html { redirect_back_or_to transaction_path(@entry) }
format.turbo_stream do
render turbo_stream: turbo_stream.replace(
"category_menu_transaction_#{@entry.transaction_id}",
partial: "categories/menu",
locals: { transaction: @entry.transaction }
)
end
end
end
private
def entry_params
params.require(:entry).permit(:entryable_type, entryable_attributes: [ :id, :category_id ])
end
end

View file

@ -0,0 +1,12 @@
class Transactions::BulkDeletionsController < ApplicationController
def create
destroyed = Current.family.entries.destroy_by(id: bulk_delete_params[:entry_ids])
destroyed.map(&:account).uniq.each(&:sync_later)
redirect_back_or_to transactions_url, notice: "#{destroyed.count} transaction#{destroyed.count == 1 ? "" : "s"} deleted"
end
private
def bulk_delete_params
params.require(:bulk_delete).permit(entry_ids: [])
end
end

View file

@ -0,0 +1,19 @@
class Transactions::BulkUpdatesController < ApplicationController
def new
end
def create
updated = Current.family
.entries
.where(id: bulk_update_params[:entry_ids])
.bulk_update!(bulk_update_params)
redirect_back_or_to transactions_path, notice: "#{updated} transactions updated"
end
private
def bulk_update_params
params.require(:bulk_update)
.permit(:date, :notes, :category_id, :merchant_id, entry_ids: [], tag_ids: [])
end
end

View file

@ -1,5 +1,5 @@
class TransactionsController < ApplicationController
include ScrollFocusable
include ScrollFocusable, EntryableResource
before_action :store_params!, only: :index
@ -48,7 +48,62 @@ class TransactionsController < ApplicationController
redirect_to transactions_path(updated_params)
end
def create
account = Current.family.accounts.find(params.dig(:entry, :account_id))
@entry = account.entries.new(entry_params)
if @entry.save
@entry.sync_account_later
flash[:notice] = "Transaction created"
respond_to do |format|
format.html { redirect_back_or_to account_path(@entry.account) }
format.turbo_stream { stream_redirect_back_or_to(account_path(@entry.account)) }
end
else
render :new, status: :unprocessable_entity
end
end
def update
if @entry.update(entry_params)
@entry.sync_account_later
respond_to do |format|
format.html { redirect_back_or_to account_path(@entry.account), notice: "Transaction updated" }
format.turbo_stream do
render turbo_stream: [
turbo_stream.replace(
dom_id(@entry, :header),
partial: "transactions/header",
locals: { entry: @entry }
),
turbo_stream.replace(@entry)
]
end
end
else
render :show, status: :unprocessable_entity
end
end
private
def entry_params
entry_params = params.require(:entry).permit(
:name, :enriched_name, :date, :amount, :currency, :excluded, :notes, :nature, :entryable_type,
entryable_attributes: [ :id, :category_id, :merchant_id, { tag_ids: [] } ]
)
nature = entry_params.delete(:nature)
if nature.present? && entry_params[:amount].present?
signed_amount = nature == "inflow" ? -entry_params[:amount].to_d : entry_params[:amount].to_d
entry_params = entry_params.merge(amount: signed_amount)
end
entry_params
end
def search_params
cleaned_params = params.fetch(:q, {})

View file

@ -1,9 +1,9 @@
class Account::TransferMatchesController < ApplicationController
class TransferMatchesController < ApplicationController
before_action :set_entry
def new
@accounts = Current.family.accounts.alphabetically.where.not(id: @entry.account_id)
@transfer_match_candidates = @entry.account_transaction.transfer_match_candidates
@transfer_match_candidates = @entry.transaction.transfer_match_candidates
end
def create
@ -11,7 +11,7 @@ class Account::TransferMatchesController < ApplicationController
@transfer.save!
@transfer.sync_account_later
redirect_back_or_to transactions_path, notice: t(".success")
redirect_back_or_to transactions_path, notice: "Transfer created"
end
private
@ -27,7 +27,7 @@ class Account::TransferMatchesController < ApplicationController
if transfer_match_params[:method] == "new"
target_account = Current.family.accounts.find(transfer_match_params[:target_account_id])
missing_transaction = Account::Transaction.new(
missing_transaction = Transaction.new(
entry: target_account.entries.build(
amount: @entry.amount * -1,
currency: @entry.currency,
@ -37,8 +37,8 @@ class Account::TransferMatchesController < ApplicationController
)
transfer = Transfer.find_or_initialize_by(
inflow_transaction: @entry.amount.positive? ? missing_transaction : @entry.account_transaction,
outflow_transaction: @entry.amount.positive? ? @entry.account_transaction : missing_transaction
inflow_transaction: @entry.amount.positive? ? missing_transaction : @entry.transaction,
outflow_transaction: @entry.amount.positive? ? @entry.transaction : missing_transaction
)
transfer.status = "confirmed"
transfer
@ -46,8 +46,8 @@ class Account::TransferMatchesController < ApplicationController
target_transaction = Current.family.entries.find(transfer_match_params[:matched_entry_id])
transfer = Transfer.find_or_initialize_by(
inflow_transaction: @entry.amount.negative? ? @entry.account_transaction : target_transaction.account_transaction,
outflow_transaction: @entry.amount.negative? ? target_transaction.account_transaction : @entry.account_transaction
inflow_transaction: @entry.amount.negative? ? @entry.transaction : target_transaction.transaction,
outflow_transaction: @entry.amount.negative? ? target_transaction.transaction : @entry.transaction
)
transfer.status = "confirmed"
transfer

View file

@ -0,0 +1,49 @@
class ValuationsController < ApplicationController
include EntryableResource
def create
account = Current.family.accounts.find(params.dig(:entry, :account_id))
@entry = account.entries.new(entry_params.merge(entryable: Valuation.new))
if @entry.save
@entry.sync_account_later
flash[:notice] = "Balance created"
respond_to do |format|
format.html { redirect_back_or_to account_path(@entry.account) }
format.turbo_stream { stream_redirect_back_or_to(account_path(@entry.account)) }
end
else
render :new, status: :unprocessable_entity
end
end
def update
if @entry.update(entry_params)
@entry.sync_account_later
respond_to do |format|
format.html { redirect_back_or_to account_path(@entry.account), notice: "Balance updated" }
format.turbo_stream do
render turbo_stream: [
turbo_stream.replace(
dom_id(@entry, :header),
partial: "valuations/header",
locals: { entry: @entry }
),
turbo_stream.replace(@entry)
]
end
end
else
render :show, status: :unprocessable_entity
end
end
private
def entry_params
params.require(:entry)
.permit(:name, :enriched_name, :date, :amount, :currency, :notes)
end
end