mirror of
https://github.com/maybe-finance/maybe.git
synced 2025-07-19 05:09:38 +02:00
Encapsulate enrichment actions, add tests
This commit is contained in:
parent
dd605a577e
commit
94a807c3c9
9 changed files with 149 additions and 71 deletions
|
@ -22,16 +22,23 @@ module Enrichable
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
def log_enrichment!(attribute_name:, attribute_value:, source:, metadata: {})
|
# Convenience method for a single attribute
|
||||||
de = DataEnrichment.find_or_create_by!(
|
def enrich_attribute(attr, value, source:, metadata: {})
|
||||||
enrichable: self,
|
enrich_attributes({ attr => value }, source:, metadata:)
|
||||||
attribute_name: attribute_name,
|
end
|
||||||
source: source,
|
|
||||||
)
|
|
||||||
|
|
||||||
de.value = attribute_value
|
# Enriches all attributes that haven't been locked yet
|
||||||
de.metadata = metadata
|
def enrich_attributes(attrs, source:, metadata: {})
|
||||||
de.save!
|
enrichable_attrs = Array(attrs).reject { |k, _v| locked?(k) }
|
||||||
|
|
||||||
|
ActiveRecord::Base.transaction do
|
||||||
|
enrichable_attrs.each do |attr, value|
|
||||||
|
self.send("#{attr}=", value)
|
||||||
|
log_enrichment(attribute_name: attr, attribute_value: value, source: source, metadata: metadata)
|
||||||
|
end
|
||||||
|
|
||||||
|
save
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def locked?(attr)
|
def locked?(attr)
|
||||||
|
@ -57,6 +64,18 @@ module Enrichable
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
def log_enrichment(attribute_name:, attribute_value:, source:, metadata: {})
|
||||||
|
de = DataEnrichment.find_or_create_by(
|
||||||
|
enrichable: self,
|
||||||
|
attribute_name: attribute_name,
|
||||||
|
source: source,
|
||||||
|
)
|
||||||
|
|
||||||
|
de.value = attribute_value
|
||||||
|
de.metadata = metadata
|
||||||
|
de.save
|
||||||
|
end
|
||||||
|
|
||||||
def ignored_enrichable_attributes
|
def ignored_enrichable_attributes
|
||||||
%w[id updated_at created_at]
|
%w[id updated_at created_at]
|
||||||
end
|
end
|
||||||
|
|
|
@ -27,23 +27,19 @@ class Family::AutoCategorizer
|
||||||
end
|
end
|
||||||
|
|
||||||
scope.each do |transaction|
|
scope.each do |transaction|
|
||||||
transaction.lock_attr!(:category_id)
|
|
||||||
|
|
||||||
auto_categorization = result.data.find { |c| c.transaction_id == transaction.id }
|
auto_categorization = result.data.find { |c| c.transaction_id == transaction.id }
|
||||||
|
|
||||||
category_id = user_categories_input.find { |c| c[:name] == auto_categorization&.category_name }&.dig(:id)
|
category_id = user_categories_input.find { |c| c[:name] == auto_categorization&.category_name }&.dig(:id)
|
||||||
|
|
||||||
if category_id.present?
|
if category_id.present?
|
||||||
Family.transaction do
|
transaction.enrich_attribute(
|
||||||
transaction.log_enrichment!(
|
:category_id,
|
||||||
attribute_name: "category_id",
|
category_id,
|
||||||
attribute_value: category_id,
|
source: "ai"
|
||||||
source: "ai",
|
)
|
||||||
)
|
|
||||||
|
|
||||||
transaction.update!(category_id: category_id)
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
|
transaction.lock_attr!(:category_id)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -27,8 +27,6 @@ class Family::AutoMerchantDetector
|
||||||
end
|
end
|
||||||
|
|
||||||
scope.each do |transaction|
|
scope.each do |transaction|
|
||||||
transaction.lock_attr!(:merchant_id)
|
|
||||||
|
|
||||||
auto_detection = result.data.find { |c| c.transaction_id == transaction.id }
|
auto_detection = result.data.find { |c| c.transaction_id == transaction.id }
|
||||||
|
|
||||||
merchant_id = user_merchants_input.find { |m| m[:name] == auto_detection&.business_name }&.dig(:id)
|
merchant_id = user_merchants_input.find { |m| m[:name] == auto_detection&.business_name }&.dig(:id)
|
||||||
|
@ -46,16 +44,16 @@ class Family::AutoMerchantDetector
|
||||||
merchant_id = merchant_id || ai_provider_merchant&.id
|
merchant_id = merchant_id || ai_provider_merchant&.id
|
||||||
|
|
||||||
if merchant_id.present?
|
if merchant_id.present?
|
||||||
Family.transaction do
|
transaction.enrich_attribute(
|
||||||
transaction.log_enrichment!(
|
:merchant_id,
|
||||||
attribute_name: "merchant_id",
|
merchant_id,
|
||||||
attribute_value: merchant_id,
|
source: "ai"
|
||||||
source: "ai",
|
)
|
||||||
)
|
|
||||||
|
|
||||||
transaction.update!(merchant_id: merchant_id)
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# We lock the attribute so that this Rule doesn't try to run again
|
||||||
|
transaction.lock_attr!(:merchant_id)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -77,14 +77,14 @@ class PlaidItem < ApplicationRecord
|
||||||
category = alias_matcher.match(transaction.plaid_category_detailed)
|
category = alias_matcher.match(transaction.plaid_category_detailed)
|
||||||
|
|
||||||
if category.present?
|
if category.present?
|
||||||
PlaidItem.transaction do
|
# Matcher could either return a string or a Category object
|
||||||
transaction.log_enrichment!(
|
user_category = if category.is_a?(String)
|
||||||
attribute_name: "category_id",
|
family.categories.find_or_create_by!(name: category)
|
||||||
attribute_value: category.id,
|
else
|
||||||
source: "plaid"
|
category
|
||||||
)
|
|
||||||
transaction.set_category!(category)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
|
transaction.enrich_attribute(:category_id, user_category.id, source: "plaid")
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -17,15 +17,11 @@ class Rule::ActionExecutor::SetTransactionCategory < Rule::ActionExecutor
|
||||||
end
|
end
|
||||||
|
|
||||||
scope.each do |txn|
|
scope.each do |txn|
|
||||||
Rule.transaction do
|
txn.enrich_attribute(
|
||||||
txn.log_enrichment!(
|
:category_id,
|
||||||
attribute_name: "category_id",
|
category.id,
|
||||||
attribute_value: category.id,
|
source: "rule"
|
||||||
source: "rule"
|
)
|
||||||
)
|
|
||||||
|
|
||||||
txn.update!(category: category)
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -17,14 +17,11 @@ class Rule::ActionExecutor::SetTransactionMerchant < Rule::ActionExecutor
|
||||||
end
|
end
|
||||||
|
|
||||||
scope.each do |txn|
|
scope.each do |txn|
|
||||||
Rule.transaction do
|
txn.enrich_attribute(
|
||||||
txn.log_enrichment!(
|
:merchant_id,
|
||||||
attribute_name: "merchant_id",
|
merchant.id,
|
||||||
attribute_value: merchant.id,
|
source: "rule"
|
||||||
source: "rule"
|
)
|
||||||
)
|
|
||||||
txn.update!(merchant: merchant)
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -16,14 +16,11 @@ class Rule::ActionExecutor::SetTransactionName < Rule::ActionExecutor
|
||||||
end
|
end
|
||||||
|
|
||||||
scope.each do |txn|
|
scope.each do |txn|
|
||||||
Rule.transaction do
|
txn.entry.enrich_attribute(
|
||||||
txn.entry.log_enrichment!(
|
:name,
|
||||||
attribute_name: "name",
|
value,
|
||||||
attribute_value: value,
|
source: "rule"
|
||||||
source: "rule"
|
)
|
||||||
)
|
|
||||||
txn.entry.update!(name: value)
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -17,15 +17,11 @@ class Rule::ActionExecutor::SetTransactionTags < Rule::ActionExecutor
|
||||||
end
|
end
|
||||||
|
|
||||||
rows = scope.each do |txn|
|
rows = scope.each do |txn|
|
||||||
Rule.transaction do
|
txn.enrich_attribute(
|
||||||
txn.log_enrichment!(
|
:tag_ids,
|
||||||
attribute_name: "tag_ids",
|
[ tag.id ],
|
||||||
attribute_value: [ tag.id ],
|
source: "rule"
|
||||||
source: "rule"
|
)
|
||||||
)
|
|
||||||
|
|
||||||
txn.update!(tag_ids: [ tag.id ])
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
79
test/models/concerns/enrichable_test.rb
Normal file
79
test/models/concerns/enrichable_test.rb
Normal file
|
@ -0,0 +1,79 @@
|
||||||
|
require "test_helper"
|
||||||
|
|
||||||
|
class EnrichableTest < ActiveSupport::TestCase
|
||||||
|
setup do
|
||||||
|
@enrichable = accounts(:depository)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "can enrich multiple attributes" do
|
||||||
|
assert_difference "DataEnrichment.count", 2 do
|
||||||
|
@enrichable.enrich_attributes({ name: "Updated Checking", balance: 6_000 }, source: "plaid")
|
||||||
|
end
|
||||||
|
|
||||||
|
assert_equal "Updated Checking", @enrichable.name
|
||||||
|
assert_equal 6_000, @enrichable.balance.to_d
|
||||||
|
end
|
||||||
|
|
||||||
|
test "can enrich a single attribute" do
|
||||||
|
assert_difference "DataEnrichment.count", 1 do
|
||||||
|
@enrichable.enrich_attribute(:name, "Single Update", source: "ai")
|
||||||
|
end
|
||||||
|
|
||||||
|
assert_equal "Single Update", @enrichable.name
|
||||||
|
end
|
||||||
|
|
||||||
|
test "can lock an attribute" do
|
||||||
|
refute @enrichable.locked?(:name)
|
||||||
|
|
||||||
|
@enrichable.lock_attr!(:name)
|
||||||
|
assert @enrichable.locked?(:name)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "can unlock an attribute" do
|
||||||
|
@enrichable.lock_attr!(:name)
|
||||||
|
assert @enrichable.locked?(:name)
|
||||||
|
|
||||||
|
@enrichable.unlock_attr!(:name)
|
||||||
|
refute @enrichable.locked?(:name)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "can lock saved attributes" do
|
||||||
|
@enrichable.name = "User Override"
|
||||||
|
@enrichable.balance = 1_234
|
||||||
|
@enrichable.save!
|
||||||
|
|
||||||
|
@enrichable.lock_saved_attributes!
|
||||||
|
|
||||||
|
assert @enrichable.locked?(:name)
|
||||||
|
assert @enrichable.locked?(:balance)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "does not enrich locked attributes" do
|
||||||
|
original_name = @enrichable.name
|
||||||
|
|
||||||
|
@enrichable.lock_attr!(:name)
|
||||||
|
|
||||||
|
assert_no_difference "DataEnrichment.count" do
|
||||||
|
@enrichable.enrich_attribute(:name, "Should Not Change", source: "plaid")
|
||||||
|
end
|
||||||
|
|
||||||
|
assert_equal original_name, @enrichable.reload.name
|
||||||
|
end
|
||||||
|
|
||||||
|
test "enrichable? reflects lock state" do
|
||||||
|
assert @enrichable.enrichable?(:name)
|
||||||
|
|
||||||
|
@enrichable.lock_attr!(:name)
|
||||||
|
|
||||||
|
refute @enrichable.enrichable?(:name)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "enrichable scope includes and excludes records based on lock state" do
|
||||||
|
# Initially, the record should be enrichable for :name
|
||||||
|
assert_includes Account.enrichable(:name), @enrichable
|
||||||
|
|
||||||
|
@enrichable.lock_attr!(:name)
|
||||||
|
|
||||||
|
refute_includes Account.enrichable(:name), @enrichable
|
||||||
|
end
|
||||||
|
end
|
Loading…
Add table
Add a link
Reference in a new issue