From 7d0fe057d34fcbbe94b90fcc871b297f187da460 Mon Sep 17 00:00:00 2001 From: Alessio Cappa <104093777+alessiocappa@users.noreply.github.com> Date: Fri, 6 Feb 2026 21:17:04 +0100 Subject: [PATCH] feat: Allow to create rules to mark transactions as transfers or payments (#920) * feat: Allow to create rules to define transfer or payments * fix: lint issues * Update app/models/rule/action_executor/set_as_transfer_or_payment.rb Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> Signed-off-by: Alessio Cappa <104093777+alessiocappa@users.noreply.github.com> * fix: indentation issue * fix: add explicit return * fix: add guard on target_account * fix: use local variable for transfer and revert explicit return as it doesn't work * fix: Adjust transaction naming --------- Signed-off-by: Alessio Cappa <104093777+alessiocappa@users.noreply.github.com> Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- .../set_as_transfer_or_payment.rb | 48 +++++++++++++++++++ .../rule/registry/transaction_resource.rb | 3 +- 2 files changed, 50 insertions(+), 1 deletion(-) create mode 100644 app/models/rule/action_executor/set_as_transfer_or_payment.rb diff --git a/app/models/rule/action_executor/set_as_transfer_or_payment.rb b/app/models/rule/action_executor/set_as_transfer_or_payment.rb new file mode 100644 index 000000000..ad2d825fa --- /dev/null +++ b/app/models/rule/action_executor/set_as_transfer_or_payment.rb @@ -0,0 +1,48 @@ +class Rule::ActionExecutor::SetAsTransferOrPayment < Rule::ActionExecutor + def type + "select" + end + + def options + family.accounts.alphabetically.pluck(:name, :id) + end + + def execute(transaction_scope, value: nil, ignore_attribute_locks: false, rule_run: nil) + target_account = family.accounts.find_by_id(value) + return 0 unless target_account + scope = transaction_scope.with_entry + + count_modified_resources(scope) do |txn| + entry = txn.entry + unless txn.transfer? + transfer = build_transfer(target_account, entry) + Transfer.transaction do + transfer.save! + transfer.outflow_transaction.update!(kind: Transfer.kind_for_account(transfer.outflow_transaction.entry.account)) + transfer.inflow_transaction.update!(kind: "funds_movement") + end + + transfer.sync_account_later + end + end + end + + private + def build_transfer(target_account, entry) + missing_transaction = Transaction.new( + entry: target_account.entries.build( + amount: entry.amount * -1, + currency: entry.currency, + date: entry.date, + name: "#{target_account.liability? ? "Payment" : "Transfer"} #{entry.amount.negative? ? "to #{target_account.name}" : "from #{entry.account.name}"}", + ) + ) + + transfer = Transfer.find_or_initialize_by( + inflow_transaction: entry.amount.positive? ? missing_transaction : entry.transaction, + outflow_transaction: entry.amount.positive? ? entry.transaction : missing_transaction + ) + transfer.status = "confirmed" + transfer + end +end diff --git a/app/models/rule/registry/transaction_resource.rb b/app/models/rule/registry/transaction_resource.rb index 3e0117fc7..a9497d9c0 100644 --- a/app/models/rule/registry/transaction_resource.rb +++ b/app/models/rule/registry/transaction_resource.rb @@ -22,7 +22,8 @@ class Rule::Registry::TransactionResource < Rule::Registry Rule::ActionExecutor::SetTransactionMerchant.new(rule), Rule::ActionExecutor::SetTransactionName.new(rule), Rule::ActionExecutor::SetInvestmentActivityLabel.new(rule), - Rule::ActionExecutor::ExcludeTransaction.new(rule) + Rule::ActionExecutor::ExcludeTransaction.new(rule), + Rule::ActionExecutor::SetAsTransferOrPayment.new(rule) ] if ai_enabled?