mirror of
https://github.com/maybe-finance/maybe.git
synced 2025-08-08 15:05:22 +02:00
Merge branch 'main' of github.com:maybe-finance/maybe into zachgoll/plaid-sync-domain-improvements
This commit is contained in:
commit
13b1560438
15 changed files with 184 additions and 102 deletions
|
@ -1,4 +1,4 @@
|
|||
ARG RUBY_VERSION=3.4.1
|
||||
ARG RUBY_VERSION=3.4.4
|
||||
FROM ruby:${RUBY_VERSION}-slim-bullseye
|
||||
|
||||
RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \
|
||||
|
|
|
@ -1 +1 @@
|
|||
3.4.1
|
||||
3.4.4
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
# syntax = docker/dockerfile:1
|
||||
|
||||
# Make sure RUBY_VERSION matches the Ruby version in .ruby-version and Gemfile
|
||||
ARG RUBY_VERSION=3.4.1
|
||||
ARG RUBY_VERSION=3.4.4
|
||||
FROM registry.docker.com/library/ruby:$RUBY_VERSION-slim AS base
|
||||
|
||||
# Rails app lives here
|
||||
|
|
2
Gemfile
2
Gemfile
|
@ -24,7 +24,6 @@ gem "stimulus-rails"
|
|||
gem "turbo-rails"
|
||||
gem "view_component"
|
||||
gem "lookbook", ">= 2.3.7"
|
||||
|
||||
gem "hotwire_combobox"
|
||||
|
||||
# Background Jobs
|
||||
|
@ -45,6 +44,7 @@ gem "aws-sdk-s3", "~> 1.177.0", require: false
|
|||
gem "image_processing", ">= 1.2"
|
||||
|
||||
# Other
|
||||
gem "ostruct"
|
||||
gem "bcrypt", "~> 3.1"
|
||||
gem "jwt"
|
||||
gem "faraday"
|
||||
|
|
54
Gemfile.lock
54
Gemfile.lock
|
@ -90,15 +90,15 @@ GEM
|
|||
activesupport
|
||||
ast (2.4.3)
|
||||
aws-eventstream (1.3.2)
|
||||
aws-partitions (1.1093.0)
|
||||
aws-sdk-core (3.222.3)
|
||||
aws-partitions (1.1105.0)
|
||||
aws-sdk-core (3.224.0)
|
||||
aws-eventstream (~> 1, >= 1.3.0)
|
||||
aws-partitions (~> 1, >= 1.992.0)
|
||||
aws-sigv4 (~> 1.9)
|
||||
base64
|
||||
jmespath (~> 1, >= 1.6.1)
|
||||
logger
|
||||
aws-sdk-kms (1.99.0)
|
||||
aws-sdk-kms (1.101.0)
|
||||
aws-sdk-core (~> 3, >= 3.216.0)
|
||||
aws-sigv4 (~> 1.5)
|
||||
aws-sdk-s3 (1.177.0)
|
||||
|
@ -160,6 +160,7 @@ GEM
|
|||
dotenv (= 3.1.8)
|
||||
railties (>= 6.1)
|
||||
drb (2.2.1)
|
||||
erb (5.0.1)
|
||||
erb_lint (0.9.0)
|
||||
activesupport
|
||||
better_html (>= 2.0.1)
|
||||
|
@ -269,7 +270,7 @@ GEM
|
|||
logtail (~> 0.1, >= 0.1.14)
|
||||
logtail-rack (~> 0.1)
|
||||
railties (>= 5.0.0)
|
||||
loofah (2.24.0)
|
||||
loofah (2.24.1)
|
||||
crass (~> 1.0.2)
|
||||
nokogiri (>= 1.12.0)
|
||||
lookbook (2.3.9)
|
||||
|
@ -303,7 +304,7 @@ GEM
|
|||
multipart-post (2.4.1)
|
||||
net-http (0.6.0)
|
||||
uri
|
||||
net-imap (0.5.7)
|
||||
net-imap (0.5.8)
|
||||
date
|
||||
net-protocol
|
||||
net-pop (0.1.2)
|
||||
|
@ -332,13 +333,14 @@ GEM
|
|||
octokit (10.0.0)
|
||||
faraday (>= 1, < 3)
|
||||
sawyer (~> 0.9)
|
||||
ostruct (0.6.1)
|
||||
pagy (9.3.4)
|
||||
parallel (1.27.0)
|
||||
parser (3.3.8.0)
|
||||
ast (~> 2.4.1)
|
||||
racc
|
||||
pg (1.5.9)
|
||||
plaid (38.0.0)
|
||||
plaid (39.0.0)
|
||||
faraday (>= 1.0.1, < 3.0)
|
||||
faraday-multipart (>= 1.0.1, < 2.0)
|
||||
platform_agent (1.0.1)
|
||||
|
@ -353,10 +355,10 @@ GEM
|
|||
activesupport (>= 7.0.0)
|
||||
rack
|
||||
railties (>= 7.0.0)
|
||||
psych (5.2.3)
|
||||
psych (5.2.6)
|
||||
date
|
||||
stringio
|
||||
public_suffix (6.0.1)
|
||||
public_suffix (6.0.2)
|
||||
puma (6.6.0)
|
||||
nio4r (~> 2.0)
|
||||
raabro (1.4.0)
|
||||
|
@ -364,7 +366,7 @@ GEM
|
|||
rack (3.1.15)
|
||||
rack-mini-profiler (3.3.1)
|
||||
rack (>= 1.2.0)
|
||||
rack-session (2.1.0)
|
||||
rack-session (2.1.1)
|
||||
base64 (>= 0.1.0)
|
||||
rack (>= 3.0.0)
|
||||
rack-test (2.2.0)
|
||||
|
@ -413,7 +415,8 @@ GEM
|
|||
ffi (~> 1.0)
|
||||
rbs (3.9.4)
|
||||
logger
|
||||
rdoc (6.13.1)
|
||||
rdoc (6.14.0)
|
||||
erb
|
||||
psych (>= 4.0.0)
|
||||
redcarpet (3.6.1)
|
||||
redis (5.4.0)
|
||||
|
@ -430,7 +433,7 @@ GEM
|
|||
chunky_png (~> 1.0)
|
||||
rqrcode_core (~> 2.0)
|
||||
rqrcode_core (2.0.0)
|
||||
rubocop (1.75.4)
|
||||
rubocop (1.75.6)
|
||||
json (~> 2.3)
|
||||
language_server-protocol (~> 3.17.0.2)
|
||||
lint_roller (~> 1.1.0)
|
||||
|
@ -448,12 +451,12 @@ GEM
|
|||
lint_roller (~> 1.1)
|
||||
rubocop (>= 1.75.0, < 2.0)
|
||||
rubocop-ast (>= 1.38.0, < 2.0)
|
||||
rubocop-rails (2.31.0)
|
||||
rubocop-rails (2.32.0)
|
||||
activesupport (>= 4.2.0)
|
||||
lint_roller (~> 1.1)
|
||||
rack (>= 1.1)
|
||||
rubocop (>= 1.75.0, < 2.0)
|
||||
rubocop-ast (>= 1.38.0, < 2.0)
|
||||
rubocop-ast (>= 1.44.0, < 2.0)
|
||||
rubocop-rails-omakase (1.1.0)
|
||||
rubocop (>= 1.72)
|
||||
rubocop-performance (>= 1.24)
|
||||
|
@ -514,21 +517,21 @@ GEM
|
|||
skylight (6.0.4)
|
||||
activesupport (>= 5.2.0)
|
||||
smart_properties (1.17.0)
|
||||
sorbet-runtime (0.5.12115)
|
||||
sorbet-runtime (0.5.12117)
|
||||
stimulus-rails (1.3.4)
|
||||
railties (>= 6.0.0)
|
||||
stringio (3.1.7)
|
||||
stripe (15.1.0)
|
||||
tailwindcss-rails (4.2.2)
|
||||
tailwindcss-rails (4.2.3)
|
||||
railties (>= 7.0.0)
|
||||
tailwindcss-ruby (~> 4.0)
|
||||
tailwindcss-ruby (4.1.4)
|
||||
tailwindcss-ruby (4.1.4-aarch64-linux-gnu)
|
||||
tailwindcss-ruby (4.1.4-aarch64-linux-musl)
|
||||
tailwindcss-ruby (4.1.4-arm64-darwin)
|
||||
tailwindcss-ruby (4.1.4-x86_64-darwin)
|
||||
tailwindcss-ruby (4.1.4-x86_64-linux-gnu)
|
||||
tailwindcss-ruby (4.1.4-x86_64-linux-musl)
|
||||
tailwindcss-ruby (4.1.7)
|
||||
tailwindcss-ruby (4.1.7-aarch64-linux-gnu)
|
||||
tailwindcss-ruby (4.1.7-aarch64-linux-musl)
|
||||
tailwindcss-ruby (4.1.7-arm64-darwin)
|
||||
tailwindcss-ruby (4.1.7-x86_64-darwin)
|
||||
tailwindcss-ruby (4.1.7-x86_64-linux-gnu)
|
||||
tailwindcss-ruby (4.1.7-x86_64-linux-musl)
|
||||
terminal-table (4.0.0)
|
||||
unicode-display_width (>= 1.1.1, < 4)
|
||||
thor (1.3.2)
|
||||
|
@ -568,7 +571,7 @@ GEM
|
|||
xpath (3.2.0)
|
||||
nokogiri (~> 1.8)
|
||||
yard (0.9.37)
|
||||
zeitwerk (2.7.2)
|
||||
zeitwerk (2.7.3)
|
||||
|
||||
PLATFORMS
|
||||
aarch64-linux-gnu
|
||||
|
@ -614,6 +617,7 @@ DEPENDENCIES
|
|||
lucide-rails!
|
||||
mocha
|
||||
octokit
|
||||
ostruct
|
||||
pagy
|
||||
pg (~> 1.5)
|
||||
plaid
|
||||
|
@ -649,7 +653,7 @@ DEPENDENCIES
|
|||
webmock
|
||||
|
||||
RUBY VERSION
|
||||
ruby 3.4.1p0
|
||||
ruby 3.4.4p34
|
||||
|
||||
BUNDLED WITH
|
||||
2.6.3
|
||||
2.6.9
|
||||
|
|
|
@ -22,16 +22,23 @@ module Enrichable
|
|||
}
|
||||
end
|
||||
|
||||
def log_enrichment!(attribute_name:, attribute_value:, source:, metadata: {})
|
||||
de = DataEnrichment.find_or_create_by!(
|
||||
enrichable: self,
|
||||
attribute_name: attribute_name,
|
||||
source: source,
|
||||
)
|
||||
# Convenience method for a single attribute
|
||||
def enrich_attribute(attr, value, source:, metadata: {})
|
||||
enrich_attributes({ attr => value }, source:, metadata:)
|
||||
end
|
||||
|
||||
de.value = attribute_value
|
||||
de.metadata = metadata
|
||||
de.save!
|
||||
# Enriches all attributes that haven't been locked yet
|
||||
def enrich_attributes(attrs, source:, metadata: {})
|
||||
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
|
||||
|
||||
def locked?(attr)
|
||||
|
@ -57,6 +64,18 @@ module Enrichable
|
|||
end
|
||||
|
||||
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
|
||||
%w[id updated_at created_at]
|
||||
end
|
||||
|
|
|
@ -27,23 +27,19 @@ class Family::AutoCategorizer
|
|||
end
|
||||
|
||||
scope.each do |transaction|
|
||||
transaction.lock_attr!(:category_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)
|
||||
|
||||
if category_id.present?
|
||||
Family.transaction do
|
||||
transaction.log_enrichment!(
|
||||
attribute_name: "category_id",
|
||||
attribute_value: category_id,
|
||||
source: "ai",
|
||||
transaction.enrich_attribute(
|
||||
:category_id,
|
||||
category_id,
|
||||
source: "ai"
|
||||
)
|
||||
end
|
||||
|
||||
transaction.update!(category_id: category_id)
|
||||
end
|
||||
end
|
||||
transaction.lock_attr!(:category_id)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -27,8 +27,6 @@ class Family::AutoMerchantDetector
|
|||
end
|
||||
|
||||
scope.each do |transaction|
|
||||
transaction.lock_attr!(:merchant_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)
|
||||
|
@ -46,16 +44,16 @@ class Family::AutoMerchantDetector
|
|||
merchant_id = merchant_id || ai_provider_merchant&.id
|
||||
|
||||
if merchant_id.present?
|
||||
Family.transaction do
|
||||
transaction.log_enrichment!(
|
||||
attribute_name: "merchant_id",
|
||||
attribute_value: merchant_id,
|
||||
source: "ai",
|
||||
transaction.enrich_attribute(
|
||||
:merchant_id,
|
||||
merchant_id,
|
||||
source: "ai"
|
||||
)
|
||||
|
||||
transaction.update!(merchant_id: merchant_id)
|
||||
end
|
||||
end
|
||||
|
||||
# We lock the attribute so that this Rule doesn't try to run again
|
||||
transaction.lock_attr!(:merchant_id)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -98,14 +98,14 @@ class PlaidItem < ApplicationRecord
|
|||
category = alias_matcher.match(transaction.plaid_category_detailed)
|
||||
|
||||
if category.present?
|
||||
PlaidItem.transaction do
|
||||
transaction.log_enrichment!(
|
||||
attribute_name: "category_id",
|
||||
attribute_value: category.id,
|
||||
source: "plaid"
|
||||
)
|
||||
transaction.set_category!(category)
|
||||
# Matcher could either return a string or a Category object
|
||||
user_category = if category.is_a?(String)
|
||||
family.categories.find_or_create_by!(name: category)
|
||||
else
|
||||
category
|
||||
end
|
||||
|
||||
transaction.enrich_attribute(:category_id, user_category.id, source: "plaid")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -17,15 +17,11 @@ class Rule::ActionExecutor::SetTransactionCategory < Rule::ActionExecutor
|
|||
end
|
||||
|
||||
scope.each do |txn|
|
||||
Rule.transaction do
|
||||
txn.log_enrichment!(
|
||||
attribute_name: "category_id",
|
||||
attribute_value: category.id,
|
||||
txn.enrich_attribute(
|
||||
:category_id,
|
||||
category.id,
|
||||
source: "rule"
|
||||
)
|
||||
|
||||
txn.update!(category: category)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -17,14 +17,11 @@ class Rule::ActionExecutor::SetTransactionMerchant < Rule::ActionExecutor
|
|||
end
|
||||
|
||||
scope.each do |txn|
|
||||
Rule.transaction do
|
||||
txn.log_enrichment!(
|
||||
attribute_name: "merchant_id",
|
||||
attribute_value: merchant.id,
|
||||
txn.enrich_attribute(
|
||||
:merchant_id,
|
||||
merchant.id,
|
||||
source: "rule"
|
||||
)
|
||||
txn.update!(merchant: merchant)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -16,14 +16,11 @@ class Rule::ActionExecutor::SetTransactionName < Rule::ActionExecutor
|
|||
end
|
||||
|
||||
scope.each do |txn|
|
||||
Rule.transaction do
|
||||
txn.entry.log_enrichment!(
|
||||
attribute_name: "name",
|
||||
attribute_value: value,
|
||||
txn.entry.enrich_attribute(
|
||||
:name,
|
||||
value,
|
||||
source: "rule"
|
||||
)
|
||||
txn.entry.update!(name: value)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -17,15 +17,11 @@ class Rule::ActionExecutor::SetTransactionTags < Rule::ActionExecutor
|
|||
end
|
||||
|
||||
rows = scope.each do |txn|
|
||||
Rule.transaction do
|
||||
txn.log_enrichment!(
|
||||
attribute_name: "tag_ids",
|
||||
attribute_value: [ tag.id ],
|
||||
txn.enrich_attribute(
|
||||
:tag_ids,
|
||||
[ tag.id ],
|
||||
source: "rule"
|
||||
)
|
||||
|
||||
txn.update!(tag_ids: [ tag.id ])
|
||||
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