mirror of
https://github.com/we-promise/sure.git
synced 2026-09-03 22:01:16 +00:00
* Add tags support for transfer transactions Expose TagSelect on transfer create/edit so users can classify fund movements; apply the same family-scoped tags to both sides. Co-authored-by: Cursor <cursoragent@cursor.com> * Require annotate permission on both transfer sides for tags Prevent tagging a read-only destination transaction when the user only has write access on the outflow account. Co-authored-by: Cursor <cursoragent@cursor.com> * Restore transfer tag selections on create form errors * Localize transfer create validation error messages --------- Co-authored-by: Cursor <cursoragent@cursor.com>
168 lines
5.0 KiB
Ruby
168 lines
5.0 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, tag_ids: 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
|
|
@tag_ids = Array(tag_ids).reject(&:blank?)
|
|
|
|
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!
|
|
apply_tags!(transfer) if tag_ids.any?
|
|
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, :tag_ids
|
|
|
|
def apply_tags!(transfer)
|
|
resolved_ids = family.tags.where(id: tag_ids).pluck(:id)
|
|
return if resolved_ids.empty?
|
|
|
|
[ transfer.outflow_transaction, transfer.inflow_transaction ].each do |transaction|
|
|
transaction.tag_ids = resolved_ids
|
|
end
|
|
end
|
|
|
|
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
|