1
0
Fork 0
mirror of https://github.com/maybe-finance/maybe.git synced 2025-07-24 07:39:39 +02:00

Transaction rules engine V1 (#1900)

* Domain model sketch

* Scaffold out rules domain

* Migrations

* Remove existing data enrichment for clean slate

* Sketch out business logic and basic tests

* Simplify rule scope building and action executions

* Get generator working again

* Basic implementation + tests

* Remove manual merchant management (rules will replace)

* Revert "Remove manual merchant management (rules will replace)"

This reverts commit 83dcbd9ff0.

* Family and Provider merchants model

* Fix brakeman warnings

* Fix notification loader

* Update notification position

* Add Rule action and condition registries

* Rule form with compound conditions and tests

* Split out notification types, add CTA type

* Rules form builder and Stimulus controller

* Clean up rule registry domain

* Clean up rules stimulus controller

* CTA message for rule when user changes transaction category

* Fix tests

* Lint updates

* Centralize notifications in Notifiable concern

* Implement category rule prompts with auto backoff and option to disable

* Fix layout bug caused by merge conflict

* Initialize rule with correct action for category CTA

* Add rule deletions, get rules working

* Complete dynamic rule form, split Stimulus controllers by resource

* Fix failing tests

* Change test password to avoid chromium conflicts

* Update integration tests

* Centralize all test password references

* Add re-apply rule action

* Rule confirm modal

* Run migrations

* Trigger rule notification after inline category updates

* Clean up rule styles

* Basic attribute locking for rules

* Apply attribute locks on user edits

* Log data enrichments, only apply rules to unlocked attributes

* Fix merge errors

* Additional merge conflict fixes

* Form UI improvements, ignore attribute locks on manual rule application

* Batch AI auto-categorization of transactions

* Auto merchant detection, ai enrichment in batches

* Fix Plaid merchant assignments

* Plaid category matching

* Cleanup 1

* Test cleanup

* Remove stale route

* Fix desktop chat UI issues

* Fix mobile nav styling issues
This commit is contained in:
Zach Gollwitzer 2025-04-18 11:39:58 -04:00 committed by GitHub
parent 8edd7ecef0
commit 297a695d0f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
152 changed files with 4502 additions and 612 deletions

View file

@ -17,6 +17,103 @@ class Provider::OpenaiTest < ActiveSupport::TestCase
end
end
test "auto categorizes transactions by various attributes" do
VCR.use_cassette("openai/auto_categorize") do
input_transactions = [
{ id: "1", name: "McDonalds", amount: 20, classification: "expense", merchant: "McDonalds", hint: "Fast Food" },
{ id: "2", name: "Amazon purchase", amount: 100, classification: "expense", merchant: "Amazon" },
{ id: "3", name: "Netflix subscription", amount: 10, classification: "expense", merchant: "Netflix", hint: "Subscriptions" },
{ id: "4", name: "paycheck", amount: 3000, classification: "income" },
{ id: "5", name: "Italian dinner with friends", amount: 100, classification: "expense" },
{ id: "6", name: "1212XXXBCaaa charge", amount: 2.99, classification: "expense" }
]
response = @subject.auto_categorize(
transactions: input_transactions,
user_categories: [
{ id: "shopping_id", name: "Shopping", is_subcategory: false, parent_id: nil, classification: "expense" },
{ id: "subscriptions_id", name: "Subscriptions", is_subcategory: true, parent_id: nil, classification: "expense" },
{ id: "restaurants_id", name: "Restaurants", is_subcategory: false, parent_id: nil, classification: "expense" },
{ id: "fast_food_id", name: "Fast Food", is_subcategory: true, parent_id: "restaurants_id", classification: "expense" },
{ id: "income_id", name: "Income", is_subcategory: false, parent_id: nil, classification: "income" }
]
)
assert response.success?
assert_equal input_transactions.size, response.data.size
txn1 = response.data.find { |c| c.transaction_id == "1" }
txn2 = response.data.find { |c| c.transaction_id == "2" }
txn3 = response.data.find { |c| c.transaction_id == "3" }
txn4 = response.data.find { |c| c.transaction_id == "4" }
txn5 = response.data.find { |c| c.transaction_id == "5" }
txn6 = response.data.find { |c| c.transaction_id == "6" }
assert_equal "Fast Food", txn1.category_name
assert_equal "Shopping", txn2.category_name
assert_equal "Subscriptions", txn3.category_name
assert_equal "Income", txn4.category_name
assert_equal "Restaurants", txn5.category_name
assert_nil txn6.category_name
end
end
test "auto detects merchants" do
VCR.use_cassette("openai/auto_detect_merchants") do
input_transactions = [
{ id: "1", name: "McDonalds", amount: 20, classification: "expense" },
{ id: "2", name: "local pub", amount: 20, classification: "expense" },
{ id: "3", name: "WMT purchases", amount: 20, classification: "expense" },
{ id: "4", name: "amzn 123 abc", amount: 20, classification: "expense" },
{ id: "5", name: "chaseX1231", amount: 2000, classification: "income" },
{ id: "6", name: "check deposit 022", amount: 200, classification: "income" },
{ id: "7", name: "shooters bar and grill", amount: 200, classification: "expense" },
{ id: "8", name: "Microsoft Office subscription", amount: 200, classification: "expense" }
]
response = @subject.auto_detect_merchants(
transactions: input_transactions,
user_merchants: [ { name: "Shooters" } ]
)
assert response.success?
assert_equal input_transactions.size, response.data.size
txn1 = response.data.find { |c| c.transaction_id == "1" }
txn2 = response.data.find { |c| c.transaction_id == "2" }
txn3 = response.data.find { |c| c.transaction_id == "3" }
txn4 = response.data.find { |c| c.transaction_id == "4" }
txn5 = response.data.find { |c| c.transaction_id == "5" }
txn6 = response.data.find { |c| c.transaction_id == "6" }
txn7 = response.data.find { |c| c.transaction_id == "7" }
txn8 = response.data.find { |c| c.transaction_id == "8" }
assert_equal "McDonald's", txn1.business_name
assert_equal "mcdonalds.com", txn1.business_url
assert_nil txn2.business_name
assert_nil txn2.business_url
assert_equal "Walmart", txn3.business_name
assert_equal "walmart.com", txn3.business_url
assert_equal "Amazon", txn4.business_name
assert_equal "amazon.com", txn4.business_url
assert_nil txn5.business_name
assert_nil txn5.business_url
assert_nil txn6.business_name
assert_nil txn6.business_url
assert_equal "Shooters", txn7.business_name
assert_nil txn7.business_url
assert_equal "Microsoft", txn8.business_name
assert_equal "microsoft.com", txn8.business_url
end
end
test "basic chat response" do
VCR.use_cassette("openai/chat/basic_response") do
response = @subject.chat_response(