1
0
Fork 0
mirror of https://github.com/maybe-finance/maybe.git synced 2025-08-02 20:15:22 +02:00

Simplify rule scope building and action executions

This commit is contained in:
Zach Gollwitzer 2025-04-02 11:36:38 -04:00
parent 3bc0c18da0
commit d64e1fc575
5 changed files with 81 additions and 73 deletions

View file

@ -1,72 +1,30 @@
class Rule < ApplicationRecord
RESOURCE_TYPES = %w[transaction].freeze
belongs_to :family
has_many :conditions, dependent: :destroy
has_many :actions, dependent: :destroy
validates :effective_date, presence: true
def get_operator_symbol(operator)
case operator
when "gt"
">"
when "lt"
"<"
when "eq"
"="
end
end
validates :resource_type, inclusion: { in: RESOURCE_TYPES }
def apply
case resource_type
when "transaction"
scope = family.transactions
scope = resource_scope
conditions.each do |condition|
case condition.condition_type
when "match_merchant"
scope = scope.left_joins(:merchant).where(merchant: { name: condition.value })
when "compare_amount"
operator_symbol = get_operator_symbol(condition.operator)
scope = scope.joins(:entry)
.where("account_entries.amount #{Arel.sql(operator_symbol)} ?", condition.value)
when "compound"
subconditions = condition.conditions
conditions.each do |condition|
scope = condition.apply(scope)
end
subconditions.each do |subcondition|
case condition.operator
when "and"
case subcondition.condition_type
when "match_merchant"
scope = scope.left_joins(:merchant).where(merchant: { name: subcondition.value })
when "compare_amount"
operator_symbol = get_operator_symbol(subcondition.operator)
scope = scope.joins(:entry)
.where("account_entries.amount #{Arel.sql(operator_symbol)} ?", subcondition.value.to_f)
end
when "or"
raise "not implemented yet"
else
raise "Invalid compound operator"
end
end
else
raise "Unsupported condition type: #{condition.condition_type}"
end
end
scope.each do |transaction|
actions.each do |action|
case action.action_type
when "set_category"
category = family.categories.find_by(name: action.value)
transaction.update!(category: category)
else
raise "Unsupported action type: #{action.action_type}"
end
end
end
else
raise "Unsupported resource type: #{resource_type}"
actions.each do |action|
action.apply(scope)
end
end
private
def resource_scope
case resource_type
when "transaction"
family.transactions
end
end
end

View file

@ -1,5 +1,26 @@
class Rule::Action < ApplicationRecord
UnsupportedActionError = Class.new(StandardError)
belongs_to :rule
validates :action_type, presence: true
def apply(resource_scope)
case action_type
when "set_transaction_category"
category = rule.family.categories.find_by(name: value)
raise "Category not found: #{value}" unless category
resource_scope.update_all(category_id: category.id)
when "set_transaction_tags"
# TODO
when "set_transaction_frequency"
# TODO
when "ai_enhance_transaction_name"
# TODO
when "ai_categorize_transaction"
# TODO
else
raise UnsupportedActionError, "Unsupported action type: #{action_type}"
end
end
end

View file

@ -1,11 +1,40 @@
class Rule::Condition < ApplicationRecord
OPERATORS = [ "and", "or", "gt", "lt", "eq" ]
TYPES = [ "match_merchant", "compare_amount", "compound" ]
UnsupportedOperatorError = Class.new(StandardError)
UnsupportedConditionTypeError = Class.new(StandardError)
belongs_to :rule, optional: true
OPERATORS = [ "and", "or", "like", ">", ">=", "<", "<=", "=" ]
belongs_to :rule, optional: -> { where.not(parent_id: nil) }
belongs_to :parent, class_name: "Rule::Condition", optional: true
has_many :conditions, class_name: "Rule::Condition", foreign_key: :parent_id, dependent: :destroy
has_many :sub_conditions, class_name: "Rule::Condition", foreign_key: :parent_id, dependent: :destroy
validates :operator, inclusion: { in: OPERATORS }, allow_nil: true
validates :condition_type, presence: true, inclusion: { in: TYPES }
validates :condition_type, presence: true
def apply(resource_scope)
filtered_scope = resource_scope
case condition_type
when "compound"
sub_conditions.each do |sub_condition|
filtered_scope = sub_condition.apply(filtered_scope)
end
when "transaction_name"
filtered_scope = filtered_scope.with_entry.where("account_entries.name #{Arel.sql(sanitize_operator(operator))} ?", value)
when "transaction_amount"
filtered_scope = filtered_scope.with_entry.where("account_entries.amount #{Arel.sql(sanitize_operator(operator))} ?", value.to_d)
when "transaction_merchant"
filtered_scope = filtered_scope.left_joins(:merchant).where(merchant: { name: value })
else
raise UnsupportedConditionTypeError, "Unsupported condition type: #{condition_type}"
end
filtered_scope
end
private
def sanitize_operator(operator)
raise UnsupportedOperatorError, "Unsupported operator: #{operator}" unless OPERATORS.include?(operator)
operator
end
end