diff --git a/app/controllers/account/trades_controller.rb b/app/controllers/account/trades_controller.rb index d93e5a216cf..9d4ba9a2cf9 100644 --- a/app/controllers/account/trades_controller.rb +++ b/app/controllers/account/trades_controller.rb @@ -12,13 +12,14 @@ def index end def create - @builder = Account::TradeBuilder.new(entry_params) + @builder = Account::EntryBuilder.new(entry_params) if entry = @builder.save entry.sync_account_later redirect_to account_path(@account), notice: t(".success") else - render :new, status: :unprocessable_entity + flash[:alert] = t(".failure") + redirect_back_or_to account_path(@account) end end @@ -29,6 +30,8 @@ def set_account end def entry_params - params.require(:account_entry).permit(:type, :date, :qty, :ticker, :price).merge(account: @account) + params.require(:account_entry) + .permit(:type, :date, :qty, :ticker, :price, :amount, :currency, :transfer_account_id) + .merge(account: @account) end end diff --git a/app/helpers/account/entries_helper.rb b/app/helpers/account/entries_helper.rb index c3d3b97dc4a..0f2aff52366 100644 --- a/app/helpers/account/entries_helper.rb +++ b/app/helpers/account/entries_helper.rb @@ -38,7 +38,7 @@ def entry_name(entry) name = entry.name || generated name else - entry.name + entry.name || "Transaction" end end diff --git a/app/javascript/controllers/trade_form_controller.js b/app/javascript/controllers/trade_form_controller.js new file mode 100644 index 00000000000..61751c02853 --- /dev/null +++ b/app/javascript/controllers/trade_form_controller.js @@ -0,0 +1,64 @@ +import {Controller} from "@hotwired/stimulus" + +const TRADE_TYPES = { + BUY: "buy", + SELL: "sell", + TRANSFER_IN: "transfer_in", + TRANSFER_OUT: "transfer_out", + INTEREST: "interest" +} + +const FIELD_VISIBILITY = { + [TRADE_TYPES.BUY]: {ticker: true, qty: true, price: true}, + [TRADE_TYPES.SELL]: {ticker: true, qty: true, price: true}, + [TRADE_TYPES.TRANSFER_IN]: {amount: true, transferAccount: true}, + [TRADE_TYPES.TRANSFER_OUT]: {amount: true, transferAccount: true}, + [TRADE_TYPES.INTEREST]: {amount: true} +} + +// Connects to data-controller="trade-form" +export default class extends Controller { + static targets = ["typeInput", "tickerInput", "amountInput", "transferAccountInput", "qtyInput", "priceInput"] + + connect() { + this.handleTypeChange = this.handleTypeChange.bind(this) + this.typeInputTarget.addEventListener("change", this.handleTypeChange) + this.updateFields(this.typeInputTarget.value || TRADE_TYPES.BUY) + } + + disconnect() { + this.typeInputTarget.removeEventListener("change", this.handleTypeChange) + } + + handleTypeChange(event) { + this.updateFields(event.target.value) + } + + updateFields(type) { + const visibleFields = FIELD_VISIBILITY[type] || {} + + Object.entries(this.fieldTargets).forEach(([field, target]) => { + const isVisible = visibleFields[field] || false + + // Update visibility + target.hidden = !isVisible + + // Update required status based on visibility + if (isVisible) { + target.setAttribute('required', '') + } else { + target.removeAttribute('required') + } + }) + } + + get fieldTargets() { + return { + ticker: this.tickerInputTarget, + amount: this.amountInputTarget, + transferAccount: this.transferAccountInputTarget, + qty: this.qtyInputTarget, + price: this.priceInputTarget + } + } +} \ No newline at end of file diff --git a/app/models/account/entry_builder.rb b/app/models/account/entry_builder.rb new file mode 100644 index 00000000000..e818ac9204b --- /dev/null +++ b/app/models/account/entry_builder.rb @@ -0,0 +1,45 @@ +class Account::EntryBuilder + include ActiveModel::Model + + TYPES = %w[ income expense buy sell interest transfer_in transfer_out ].freeze + + attr_accessor :type, :date, :qty, :ticker, :price, :amount, :currency, :account, :transfer_account_id + + validates :type, inclusion: { in: TYPES } + + def save + if valid? + create_builder.save + end + end + + private + + def create_builder + case type + when "buy", "sell" + create_trade_builder + else + create_transaction_builder + end + end + + def create_trade_builder + Account::TradeBuilder.new \ + type: type, + date: date, + qty: qty, + ticker: ticker, + price: price, + account: account + end + + def create_transaction_builder + Account::TransactionBuilder.new \ + type: type, + date: date, + amount: amount, + account: account, + transfer_account_id: transfer_account_id + end +end diff --git a/app/models/account/trade_builder.rb b/app/models/account/trade_builder.rb index f460a486be0..bc5679441f6 100644 --- a/app/models/account/trade_builder.rb +++ b/app/models/account/trade_builder.rb @@ -1,8 +1,8 @@ -class Account::TradeBuilder - TYPES = %w[ buy sell ].freeze - +class Account::TradeBuilder < Account::EntryBuilder include ActiveModel::Model + TYPES = %w[ buy sell ].freeze + attr_accessor :type, :qty, :price, :ticker, :date, :account validates :type, :qty, :price, :ticker, :date, presence: true diff --git a/app/models/account/transaction_builder.rb b/app/models/account/transaction_builder.rb new file mode 100644 index 00000000000..4c20237389a --- /dev/null +++ b/app/models/account/transaction_builder.rb @@ -0,0 +1,63 @@ +class Account::TransactionBuilder + include ActiveModel::Model + + TYPES = %w[ income expense interest transfer_in transfer_out ].freeze + + attr_accessor :type, :amount, :date, :account, :transfer_account_id + + validates :type, :amount, :date, presence: true + validates :type, inclusion: { in: TYPES } + + def save + if valid? + transfer? ? create_transfer : create_transaction + end + end + + private + + def transfer? + %w[transfer_in transfer_out].include?(type) + end + + def create_transfer + return create_unlinked_transfer(account.id, signed_amount) unless transfer_account_id + + from_account_id = type == "transfer_in" ? transfer_account_id : account.id + to_account_id = type == "transfer_in" ? account.id : transfer_account_id + + outflow = create_unlinked_transfer(from_account_id, signed_amount.abs) + inflow = create_unlinked_transfer(to_account_id, signed_amount.abs * -1) + + Account::Transfer.create! entries: [ outflow, inflow ] + + inflow + end + + def create_unlinked_transfer(account_id, amount) + build_entry(account_id, amount, marked_as_transfer: true).tap(&:save!) + end + + def create_transaction + build_entry(account.id, signed_amount).tap(&:save!) + end + + def build_entry(account_id, amount, marked_as_transfer: false) + Account::Entry.new \ + account_id: account_id, + amount: amount, + currency: account.currency, + date: date, + marked_as_transfer: marked_as_transfer, + entryable: Account::Transaction.new + end + + def signed_amount + case type + when "expense", "transfer_out" + amount.to_d + else + amount.to_d * -1 + end + end +end diff --git a/app/views/account/trades/_form.html.erb b/app/views/account/trades/_form.html.erb index 75a1e748a76..10d0dac8686 100644 --- a/app/views/account/trades/_form.html.erb +++ b/app/views/account/trades/_form.html.erb @@ -1,17 +1,32 @@ <%# locals: (entry:) %> -<%= styled_form_with data: { turbo_frame: "_top" }, +<%= styled_form_with data: { turbo_frame: "_top", controller: "trade-form" }, scope: :account_entry, url: entry.new_record? ? account_trades_path(entry.account) : account_entry_path(entry.account, entry) do |form| %>