mirror of
https://github.com/maybe-finance/maybe.git
synced 2025-07-24 15:49:39 +02:00
Basic implementation + tests
This commit is contained in:
parent
016b48a71c
commit
f07940bf45
8 changed files with 191 additions and 19 deletions
|
@ -1,12 +1,11 @@
|
|||
class Rule < ApplicationRecord
|
||||
RESOURCE_TYPES = %w[transaction].freeze
|
||||
UnsupportedResourceTypeError = Class.new(StandardError)
|
||||
|
||||
belongs_to :family
|
||||
has_many :conditions, dependent: :destroy
|
||||
has_many :actions, dependent: :destroy
|
||||
|
||||
validates :effective_date, presence: true
|
||||
validates :resource_type, inclusion: { in: RESOURCE_TYPES }
|
||||
validates :resource_type, presence: true
|
||||
|
||||
def apply
|
||||
scope = resource_scope
|
||||
|
@ -20,11 +19,15 @@ class Rule < ApplicationRecord
|
|||
end
|
||||
end
|
||||
|
||||
private
|
||||
def resource_scope
|
||||
case resource_type
|
||||
when "transaction"
|
||||
family.transactions
|
||||
end
|
||||
def resource_scope
|
||||
case resource_type
|
||||
when "transaction"
|
||||
family.transactions
|
||||
.active
|
||||
.with_entry
|
||||
.where(account_entries: { date: effective_date..nil })
|
||||
else
|
||||
raise UnsupportedResourceTypeError, "Unsupported resource type: #{resource_type}"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -8,9 +8,8 @@ class Rule::Action < ApplicationRecord
|
|||
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)
|
||||
category = rule.family.categories.find(value)
|
||||
resource_scope.update_all(category_id: category.id, updated_at: Time.current)
|
||||
when "set_transaction_tags"
|
||||
# TODO
|
||||
when "set_transaction_frequency"
|
||||
|
|
|
@ -16,13 +16,11 @@ class Rule::Condition < ApplicationRecord
|
|||
|
||||
case condition_type
|
||||
when "compound"
|
||||
sub_conditions.each do |sub_condition|
|
||||
filtered_scope = sub_condition.apply(filtered_scope)
|
||||
end
|
||||
filtered_scope = build_compound_scope(filtered_scope)
|
||||
when "transaction_name"
|
||||
filtered_scope = filtered_scope.with_entry.where("account_entries.name #{Arel.sql(sanitize_operator(operator))} ?", value)
|
||||
filtered_scope = filtered_scope.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)
|
||||
filtered_scope = filtered_scope.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
|
||||
|
@ -37,4 +35,28 @@ class Rule::Condition < ApplicationRecord
|
|||
raise UnsupportedOperatorError, "Unsupported operator: #{operator}" unless OPERATORS.include?(operator)
|
||||
operator
|
||||
end
|
||||
|
||||
def build_compound_scope(filtered_scope)
|
||||
if operator == "or"
|
||||
combined_scope = nil
|
||||
|
||||
sub_conditions.each do |sub_condition|
|
||||
sub_scope = sub_condition.apply(filtered_scope)
|
||||
|
||||
if combined_scope.nil?
|
||||
combined_scope = sub_scope
|
||||
else
|
||||
combined_scope = combined_scope.or(sub_scope)
|
||||
end
|
||||
end
|
||||
|
||||
filtered_scope = combined_scope || filtered_scope
|
||||
else
|
||||
sub_conditions.each do |sub_condition|
|
||||
filtered_scope = sub_condition.apply(filtered_scope)
|
||||
end
|
||||
end
|
||||
|
||||
filtered_scope
|
||||
end
|
||||
end
|
||||
|
|
|
@ -4,7 +4,7 @@ class CreateRules < ActiveRecord::Migration[7.2]
|
|||
t.references :family, null: false, foreign_key: true, type: :uuid
|
||||
|
||||
t.string :resource_type, null: false
|
||||
t.date :effective_date, null: false
|
||||
t.date :effective_date
|
||||
t.boolean :active, null: false, default: true
|
||||
t.timestamps
|
||||
end
|
||||
|
|
2
db/schema.rb
generated
2
db/schema.rb
generated
|
@ -492,7 +492,7 @@ ActiveRecord::Schema[7.2].define(version: 2025_04_01_194500) do
|
|||
create_table "rules", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
|
||||
t.uuid "family_id", null: false
|
||||
t.string "resource_type", null: false
|
||||
t.date "effective_date", null: false
|
||||
t.date "effective_date"
|
||||
t.boolean "active", default: true, null: false
|
||||
t.datetime "created_at", null: false
|
||||
t.datetime "updated_at", null: false
|
||||
|
|
3
test/fixtures/rules.yml
vendored
Normal file
3
test/fixtures/rules.yml
vendored
Normal file
|
@ -0,0 +1,3 @@
|
|||
one:
|
||||
family: dylan_family
|
||||
resource_type: "transaction"
|
37
test/models/rule/action_test.rb
Normal file
37
test/models/rule/action_test.rb
Normal file
|
@ -0,0 +1,37 @@
|
|||
require "test_helper"
|
||||
|
||||
class Rule::ActionTest < ActiveSupport::TestCase
|
||||
include Account::EntriesTestHelper
|
||||
|
||||
setup do
|
||||
@family = families(:empty)
|
||||
@transaction_rule = @family.rules.create!(resource_type: "transaction")
|
||||
@account = @family.accounts.create!(name: "Rule test", balance: 1000, currency: "USD", accountable: Depository.new)
|
||||
|
||||
@grocery_category = @family.categories.create!(name: "Grocery")
|
||||
@whole_foods_merchant = @family.merchants.create!(name: "Whole Foods")
|
||||
|
||||
# Some sample transactions to work with
|
||||
create_transaction(date: Date.current, account: @account, amount: 100, name: "Rule test transaction1", merchant: @whole_foods_merchant)
|
||||
create_transaction(date: Date.current, account: @account, amount: -200, name: "Rule test transaction2")
|
||||
create_transaction(date: 1.day.ago.to_date, account: @account, amount: 50, name: "Rule test transaction3")
|
||||
create_transaction(date: 1.year.ago.to_date, account: @account, amount: 10, name: "Rule test transaction4", merchant: @whole_foods_merchant)
|
||||
create_transaction(date: 1.year.ago.to_date, account: @account, amount: 1000, name: "Rule test transaction5")
|
||||
|
||||
@rule_scope = @account.transactions
|
||||
end
|
||||
|
||||
test "set_transaction_category" do
|
||||
action = Rule::Action.new(
|
||||
rule: @transaction_rule,
|
||||
action_type: "set_transaction_category",
|
||||
value: @grocery_category.id
|
||||
)
|
||||
|
||||
action.apply(@rule_scope)
|
||||
|
||||
@rule_scope.reload.each do |transaction|
|
||||
assert_equal @grocery_category.id, transaction.category_id
|
||||
end
|
||||
end
|
||||
end
|
108
test/models/rule/condition_test.rb
Normal file
108
test/models/rule/condition_test.rb
Normal file
|
@ -0,0 +1,108 @@
|
|||
require "test_helper"
|
||||
|
||||
class Rule::ConditionTest < ActiveSupport::TestCase
|
||||
include Account::EntriesTestHelper
|
||||
|
||||
setup do
|
||||
@family = families(:empty)
|
||||
@transaction_rule = @family.rules.create!(resource_type: "transaction")
|
||||
@account = @family.accounts.create!(name: "Rule test", balance: 1000, currency: "USD", accountable: Depository.new)
|
||||
|
||||
@grocery_category = @family.categories.create!(name: "Grocery")
|
||||
@whole_foods_merchant = @family.merchants.create!(name: "Whole Foods")
|
||||
|
||||
# Some sample transactions to work with
|
||||
create_transaction(date: Date.current, account: @account, amount: 100, name: "Rule test transaction1", merchant: @whole_foods_merchant)
|
||||
create_transaction(date: Date.current, account: @account, amount: -200, name: "Rule test transaction2")
|
||||
create_transaction(date: 1.day.ago.to_date, account: @account, amount: 50, name: "Rule test transaction3")
|
||||
create_transaction(date: 1.year.ago.to_date, account: @account, amount: 10, name: "Rule test transaction4", merchant: @whole_foods_merchant)
|
||||
create_transaction(date: 1.year.ago.to_date, account: @account, amount: 1000, name: "Rule test transaction5")
|
||||
|
||||
@rule_scope = @account.transactions
|
||||
end
|
||||
|
||||
test "applies transaction_name condition" do
|
||||
condition = Rule::Condition.new(
|
||||
rule: @transaction_rule,
|
||||
condition_type: "transaction_name",
|
||||
operator: "=",
|
||||
value: "Rule test transaction1"
|
||||
)
|
||||
|
||||
assert_equal 5, @rule_scope.count
|
||||
|
||||
filtered = condition.apply(@rule_scope)
|
||||
|
||||
assert_equal 1, filtered.count
|
||||
end
|
||||
|
||||
test "applies transaction_amount condition" do
|
||||
condition = Rule::Condition.new(
|
||||
rule: @transaction_rule,
|
||||
condition_type: "transaction_amount",
|
||||
operator: ">",
|
||||
value: "50"
|
||||
)
|
||||
|
||||
filtered = condition.apply(@rule_scope)
|
||||
assert_equal 2, filtered.count
|
||||
end
|
||||
|
||||
test "applies transaction_merchant condition" do
|
||||
condition = Rule::Condition.new(
|
||||
rule: @transaction_rule,
|
||||
condition_type: "transaction_merchant",
|
||||
operator: "=",
|
||||
value: "Whole Foods"
|
||||
)
|
||||
|
||||
filtered = condition.apply(@rule_scope)
|
||||
assert_equal 2, filtered.count
|
||||
end
|
||||
|
||||
test "applies compound and condition" do
|
||||
parent_condition = Rule::Condition.new(
|
||||
rule: @transaction_rule,
|
||||
condition_type: "compound",
|
||||
operator: "and",
|
||||
sub_conditions: [
|
||||
Rule::Condition.new(
|
||||
condition_type: "transaction_merchant",
|
||||
operator: "=",
|
||||
value: "Whole Foods"
|
||||
),
|
||||
Rule::Condition.new(
|
||||
condition_type: "transaction_amount",
|
||||
operator: "<",
|
||||
value: "50"
|
||||
)
|
||||
]
|
||||
)
|
||||
|
||||
filtered = parent_condition.apply(@rule_scope)
|
||||
assert_equal 1, filtered.count
|
||||
end
|
||||
|
||||
test "applies compound or condition" do
|
||||
parent_condition = Rule::Condition.new(
|
||||
rule: @transaction_rule,
|
||||
condition_type: "compound",
|
||||
operator: "or",
|
||||
sub_conditions: [
|
||||
Rule::Condition.new(
|
||||
condition_type: "transaction_merchant",
|
||||
operator: "=",
|
||||
value: "Whole Foods"
|
||||
),
|
||||
Rule::Condition.new(
|
||||
condition_type: "transaction_amount",
|
||||
operator: "<",
|
||||
value: "50"
|
||||
)
|
||||
]
|
||||
)
|
||||
|
||||
filtered = parent_condition.apply(@rule_scope)
|
||||
assert_equal 3, filtered.count
|
||||
end
|
||||
end
|
Loading…
Add table
Add a link
Reference in a new issue