Files
sure/app/models/transfer/creator.rb
DataEnginr e75f2a0c78 Fix fee display consistency, derive fees from entries, clean schema churn
- Show principal-only transfer amounts on both sides with separate fee and total lines (fixes inconsistent gross/net convention)
- Derive displayed fee amounts from fee_transactions entries (single source of truth) instead of stored columns
- Remove stored source_fee_amount/destination_fee_amount columns from transfers table
- Add foreign key for transactions.transfer_id -> transfers.id (replaces invalid CHECK subquery)
- Move destination fee line inside destination side div for consistent layout
- Remove orphaned view_fee_transaction locale keys from 7 locale files
- Rebuild schema.rb from origin/main to eliminate unrelated column reordering churn
2026-06-28 19:47:53 +00:00

157 lines
4.6 KiB
Ruby

class Transfer::Creator
def initialize(family:, source_account_id:, destination_account_id:, date:, amount:, exchange_rate: nil, source_fee_amount: nil, destination_fee_amount: nil)
@family = family
@source_account = family.accounts.find(source_account_id) # early throw if not found
@destination_account = family.accounts.find(destination_account_id) # early throw if not found
@date = date
@amount = amount.to_d
@source_fee_amount = source_fee_amount.to_d
@destination_fee_amount = destination_fee_amount.to_d
if exchange_rate.present?
rate_value = exchange_rate.to_d
raise ArgumentError, "exchange_rate must be greater than 0" unless rate_value > 0
@exchange_rate = rate_value
else
@exchange_rate = nil
end
end
def create
raise ArgumentError, "source_fee_amount must be non-negative" if source_fee_amount.negative?
raise ArgumentError, "destination_fee_amount must be non-negative" if destination_fee_amount.negative?
transfer = Transfer.new(
inflow_transaction: inflow_transaction,
outflow_transaction: outflow_transaction,
status: "confirmed",
amount: amount
)
Transfer.transaction do
if source_fee_amount > 0
transfer.fee_transactions << build_source_fee_transaction
end
if destination_fee_amount > 0
transfer.fee_transactions << build_destination_fee_transaction
end
transfer.save!
end
source_account.sync_later
destination_account.sync_later
transfer
end
private
attr_reader :family, :source_account, :destination_account, :date, :amount, :exchange_rate, :source_fee_amount, :destination_fee_amount
def outflow_transaction
name = "#{name_prefix} to #{destination_account.name}"
kind = outflow_transaction_kind
Transaction.new(
kind: kind,
category: (investment_contributions_category if kind == "investment_contribution"),
entry: source_account.entries.build(
amount: amount,
currency: source_account.currency,
date: date,
name: name,
user_modified: true,
)
)
end
def investment_contributions_category
source_account.family.investment_contributions_category
end
def inflow_transaction
name = "#{name_prefix} from #{source_account.name}"
net_inflow = inflow_converted_amount
Transaction.new(
kind: "funds_movement",
entry: destination_account.entries.build(
amount: net_inflow * -1,
currency: destination_account.currency,
date: date,
name: name,
user_modified: true,
)
)
end
def build_source_fee_transaction
fee_category = find_or_create_fees_category(source_account.family)
Transaction.new(
kind: "standard",
category: fee_category,
entry: source_account.entries.build(
amount: source_fee_amount,
currency: source_account.currency,
date: date,
name: "Transfer fee — #{name_prefix} to #{destination_account.name}",
)
)
end
def build_destination_fee_transaction
fee_category = find_or_create_fees_category(destination_account.family)
Transaction.new(
kind: "standard",
category: fee_category,
entry: destination_account.entries.build(
amount: destination_fee_amount,
currency: destination_account.currency,
date: date,
name: "Transfer fee — #{name_prefix} from #{source_account.name}",
)
)
end
def find_or_create_fees_category(family)
family.categories.find_or_create_by!(name: I18n.t("models.category.defaults.fees"))
end
def inflow_converted_amount
Money.new(amount.abs, source_account.currency)
.exchange_to(
destination_account.currency,
date: date,
custom_rate: exchange_rate
).amount
end
def outflow_transaction_kind
if destination_account.loan?
"loan_payment"
elsif destination_account.liability?
"cc_payment"
elsif destination_is_investment? && !source_is_investment?
"investment_contribution"
else
"funds_movement"
end
end
def destination_is_investment?
destination_account.investment? || destination_account.crypto?
end
def source_is_investment?
source_account.investment? || source_account.crypto?
end
def name_prefix
if destination_account.liability?
"Payment"
else
"Transfer"
end
end
end