1
0
Fork 0
mirror of https://github.com/maybe-finance/maybe.git synced 2025-07-19 05:09:38 +02:00

Remove manual merchant management (rules will replace)

This commit is contained in:
Zach Gollwitzer 2025-04-02 17:48:34 -04:00
parent f07940bf45
commit 83dcbd9ff0
44 changed files with 123 additions and 386 deletions

View file

@ -1,7 +1,7 @@
class Account::TransactionsController < ApplicationController
include EntryableResource
permitted_entryable_attributes :id, :category_id, :merchant_id, { tag_ids: [] }
permitted_entryable_attributes :id, :category_id, { tag_ids: [] }
def bulk_delete
destroyed = Current.family.entries.destroy_by(id: bulk_delete_params[:entry_ids])
@ -27,11 +27,11 @@ class Account::TransactionsController < ApplicationController
end
def bulk_update_params
params.require(:bulk_update).permit(:date, :notes, :category_id, :merchant_id, entry_ids: [])
params.require(:bulk_update).permit(:date, :notes, :category_id, entry_ids: [])
end
def search_params
params.fetch(:q, {})
.permit(:start_date, :end_date, :search, :amount, :amount_operator, accounts: [], account_ids: [], categories: [], merchants: [], types: [], tags: [])
.permit(:start_date, :end_date, :search, :amount, :amount_operator, accounts: [], account_ids: [], categories: [], types: [], tags: [])
end
end

View file

@ -1,46 +0,0 @@
class MerchantsController < ApplicationController
before_action :set_merchant, only: %i[edit update destroy]
def index
@merchants = Current.family.merchants.alphabetically
render layout: "settings"
end
def new
@merchant = Merchant.new
end
def create
@merchant = Current.family.merchants.new(merchant_params)
if @merchant.save
redirect_to merchants_path, notice: t(".success")
else
redirect_to merchants_path, alert: t(".error", error: @merchant.errors.full_messages.to_sentence)
end
end
def edit
end
def update
@merchant.update!(merchant_params)
redirect_to merchants_path, notice: t(".success")
end
def destroy
@merchant.destroy!
redirect_to merchants_path, notice: t(".success")
end
private
def set_merchant
@merchant = Current.family.merchants.find(params[:id])
end
def merchant_params
params.require(:merchant).permit(:name, :color)
end
end

View file

@ -10,7 +10,6 @@ module SettingsHelper
{ name: I18n.t("settings.settings_nav.tags_label"), path: :tags_path },
{ name: I18n.t("settings.settings_nav.categories_label"), path: :categories_path },
{ name: "Rules", path: :rules_path },
{ name: I18n.t("settings.settings_nav.merchants_label"), path: :merchants_path },
{ name: I18n.t("settings.settings_nav.whats_new_label"), path: :changelog_path },
{ name: I18n.t("settings.settings_nav.feedback_label"), path: :feedback_path }
]

View file

@ -8,7 +8,6 @@ class FamilyResetJob < ApplicationJob
family.accounts.destroy_all
family.categories.destroy_all
family.tags.destroy_all
family.merchants.destroy_all
family.plaid_items.destroy_all
family.imports.destroy_all
family.budgets.destroy_all

View file

@ -1,71 +0,0 @@
module Account::Enrichable
extend ActiveSupport::Concern
def enrich_data
total_unenriched = entries.account_transactions
.joins("JOIN account_transactions at ON at.id = account_entries.entryable_id AND account_entries.entryable_type = 'Account::Transaction'")
.where("account_entries.enriched_at IS NULL OR at.merchant_id IS NULL OR at.category_id IS NULL")
.count
if total_unenriched > 0
batch_size = 50
batches = (total_unenriched.to_f / batch_size).ceil
batches.times do |batch|
EnrichTransactionBatchJob.perform_now(self, batch_size, batch * batch_size)
# EnrichTransactionBatchJob.perform_later(self, batch_size, batch * batch_size)
end
end
end
def enrich_transaction_batch(batch_size = 50, offset = 0)
transactions_batch = enrichable_transactions.offset(offset).limit(batch_size)
Rails.logger.info("Enriching batch of #{transactions_batch.count} transactions for account #{id} (offset: #{offset})")
merchants = {}
transactions_batch.each do |transaction|
begin
info = transaction.fetch_enrichment_info
next unless info.present?
if info.name.present?
merchant = merchants[info.name] ||= family.merchants.find_or_create_by(name: info.name)
if info.icon_url.present?
merchant.icon_url = info.icon_url
end
end
Account.transaction do
merchant.save! if merchant.present?
transaction.update!(merchant: merchant) if merchant.present? && transaction.merchant_id.nil?
transaction.entry.update!(
enriched_at: Time.current,
enriched_name: info.name,
)
end
rescue => e
Rails.logger.warn("Error enriching transaction #{transaction.id}: #{e.message}")
end
end
end
private
def enrichable?
family.data_enrichment_enabled? || (linked? && Rails.application.config.app_mode.hosted?)
end
def enrichable_transactions
transactions.active
.includes(:merchant, :category)
.where(
"account_entries.enriched_at IS NULL",
"OR merchant_id IS NULL",
"OR category_id IS NULL"
)
end
end

View file

@ -65,10 +65,7 @@ class Account::Entry < ApplicationRecord
bulk_attributes = {
date: bulk_update_params[:date],
notes: bulk_update_params[:notes],
entryable_attributes: {
category_id: bulk_update_params[:category_id],
merchant_id: bulk_update_params[:merchant_id]
}.compact_blank
entryable_attributes: { category_id: bulk_update_params[:category_id] }.compact_blank
}.compact_blank
return 0 if bulk_attributes.blank?

View file

@ -39,9 +39,9 @@ class Demo::Generator
ActiveRecord::Base.transaction do
create_tags!(family)
create_categories!(family)
create_merchants!(family)
create_merchants!
create_rules!(family)
puts "tags, categories, merchants created for #{family_name}"
puts "tags, categories, merchants created"
create_credit_card_account!(family)
create_checking_account!(family)
@ -213,13 +213,13 @@ class Demo::Generator
family.categories.create!(name: "Alcohol & Bars", parent: food, color: COLORS.sample, lucide_icon: "beer", classification: "expense")
end
def create_merchants!(family)
def create_merchants!
merchants = [ "Amazon", "Starbucks", "McDonald's", "Target", "Costco",
"Home Depot", "Shell", "Whole Foods", "Walgreens", "Nike",
"Uber", "Netflix", "Spotify", "Delta Airlines", "Airbnb", "Sephora" ]
merchants.each do |merchant|
family.merchants.create!(name: merchant, color: COLORS.sample)
Merchant.find_or_create_by_normalized_name!(merchant)
end
end

View file

@ -28,7 +28,6 @@ class Family < ApplicationRecord
has_many :tags, dependent: :destroy
has_many :categories, dependent: :destroy
has_many :merchants, dependent: :destroy
has_many :budgets, dependent: :destroy
has_many :budget_categories, through: :budgets
@ -36,6 +35,11 @@ class Family < ApplicationRecord
validates :locale, inclusion: { in: I18n.available_locales.map(&:to_s) }
validates :date_format, inclusion: { in: DATE_FORMATS.map(&:last) }
def merchants
transaction_merchant_ids = self.transactions.where.not(merchant_id: nil).pluck(:merchant_id).uniq
Merchant.where(id: transaction_merchant_ids)
end
def balance_sheet
@balance_sheet ||= BalanceSheet.new(self)
end

View file

@ -1,11 +1,24 @@
class Merchant < ApplicationRecord
has_many :transactions, dependent: :nullify, class_name: "Account::Transaction"
belongs_to :family
validates :name, :color, :family, presence: true
validates :name, uniqueness: { scope: :family }
validates :name, presence: true, uniqueness: true
scope :alphabetically, -> { order(:name) }
COLORS = %w[#e99537 #4da568 #6471eb #db5a54 #df4e92 #c44fe9 #eb5429 #61c9ea #805dee #6ad28a]
before_save :normalize_name
class << self
def normalize_name(name)
name.downcase.strip.titleize
end
def find_or_create_by_normalized_name!(name)
find_or_create_by!(name: normalize_name(name))
end
end
private
def normalize_name
self.name = self.class.normalize_name(name)
end
end

View file

@ -137,7 +137,7 @@ class PlaidAccount < ApplicationRecord
def get_merchant(plaid_merchant_name)
return nil if plaid_merchant_name.blank?
family.merchants.find_or_create_by!(name: plaid_merchant_name)
Merchant.find_or_create_by!(name: plaid_merchant_name)
end
def derive_plaid_cash_balance(plaid_balances)

View file

@ -39,7 +39,6 @@
<div class="space-y-2">
<%= form.collection_select :category_id, Current.family.categories.alphabetically, :id, :name, { prompt: t(".category_placeholder"), label: t(".category_label"), class: "text-subdued" } %>
<%= form.collection_select :merchant_id, Current.family.merchants.alphabetically, :id, :name, { prompt: t(".merchant_placeholder"), label: t(".merchant_label"), class: "text-subdued" } %>
<%= form.text_area :notes, label: t(".note_label"), placeholder: t(".note_placeholder"), rows: 5 %>
</div>
</details>

View file

@ -68,10 +68,7 @@
<%= ef.collection_select :merchant_id,
Current.family.merchants.alphabetically,
:id, :name,
{ include_blank: t(".none"),
label: t(".merchant_label"),
class: "text-subdued" },
"data-auto-submit-form-target": "auto" %>
{ label: "Merchant", class: "text-subdued" }, disabled: true %>
<%= ef.select :tag_ids,
Current.family.tags.alphabetically.pluck(:name, :id),

View file

@ -1,24 +0,0 @@
<div data-controller="color-avatar">
<%= styled_form_with model: @merchant, class: "space-y-4", data: { turbo_frame: :_top } do |f| %>
<section class="space-y-4">
<div class="w-fit m-auto">
<%= render partial: "shared/color_avatar", locals: { name: @merchant.name, color: @merchant.color } %>
</div>
<div class="flex gap-2 items-center justify-center">
<% Merchant::COLORS.each do |color| %>
<label class="relative">
<%= f.radio_button :color, color, class: "sr-only peer", data: { action: "change->color-avatar#handleColorChange" } %>
<div class="w-6 h-6 rounded-full cursor-pointer peer-checked:ring-2 peer-checked:ring-offset-2 peer-checked:ring-blue-500" style="background-color: <%= color %>"></div>
</label>
<% end %>
</div>
<div class="relative flex items-center border border-gray-200 rounded-lg">
<%= f.text_field :name, placeholder: t(".name_placeholder"), autofocus: true, required: true, data: { color_avatar_target: "name" } %>
</div>
</section>
<section>
<%= f.submit %>
</section>
<% end %>
</div>

View file

@ -1,33 +0,0 @@
<%# locals: (merchant:) %>
<div class="flex justify-between items-center p-4 bg-white">
<div class="flex w-full items-center gap-2.5">
<% if merchant.icon_url %>
<div class="w-8 h-8 rounded-full flex justify-center items-center">
<%= image_tag merchant.icon_url, class: "w-8 h-8 rounded-full" %>
</div>
<% else %>
<%= render partial: "shared/color_avatar", locals: { name: merchant.name, color: merchant.color } %>
<% end %>
<p class="text-primary text-sm truncate">
<%= merchant.name %>
</p>
</div>
<div class="justify-self-end">
<%= contextual_menu do %>
<div class="w-48 p-1 text-sm leading-6 text-primary bg-white shadow-lg shrink rounded-xl ring-1 ring-gray-900/5">
<%= contextual_menu_modal_action_item t(".edit"), edit_merchant_path(merchant) %>
<%= contextual_menu_destructive_item t(".delete"),
merchant_path(merchant),
turbo_frame: "_top",
turbo_confirm: merchant.transactions.any? ? {
title: t(".confirm_title"),
body: t(".confirm_body"),
accept: t(".confirm_accept")
} : nil %>
</div>
<% end %>
</div>
</div>

View file

@ -1,3 +0,0 @@
<div class="bg-white">
<div class="h-px bg-alpha-black-50 ml-14 mr-6"></div>
</div>

View file

@ -1,3 +0,0 @@
<%= modal_form_wrapper title: t(".title") do %>
<%= render "form", merchant: @merchant %>
<% end %>

View file

@ -1,36 +0,0 @@
<header class="flex items-center justify-between">
<h1 class="text-primary text-xl font-medium"><%= t(".title") %></h1>
<%= link_to new_merchant_path, class: "btn btn--primary flex items-center gap-1 justify-center", data: { turbo_frame: :modal } do %>
<%= lucide_icon "plus", class: "w-5 h-5" %>
<p><%= t(".new") %></p>
<% end %>
</header>
<div class="bg-white shadow-border-xs rounded-xl p-4">
<% if @merchants.any? %>
<div class="rounded-xl bg-gray-25 space-y-1 p-1">
<div class="flex items-center gap-1.5 px-4 py-2 text-xs font-medium text-secondary uppercase">
<p><%= t(".title") %></p>
<span class="text-subdued">&middot;</span>
<p><%= @merchants.count %></p>
</div>
<div class="border border-alpha-black-25 rounded-md bg-white shadow-border-xs">
<div class="overflow-hidden rounded-md">
<%= render partial: @merchants, spacer_template: "merchants/ruler" %>
</div>
</div>
</div>
<% else %>
<div class="flex justify-center items-center py-20">
<div class="text-center flex flex-col items-center max-w-[300px]">
<p class="text-primary mb-1 font-medium text-sm"><%= t(".empty") %></p>
<%= link_to new_merchant_path, class: "w-fit flex text-white text-sm font-medium items-center gap-1 bg-gray-900 rounded-lg p-2 pr-3", data: { turbo_frame: "modal" } do %>
<%= lucide_icon("plus", class: "w-5 h-5") %>
<span><%= t(".new") %></span>
<% end %>
</div>
</div>
<% end %>
</div>

View file

@ -1,3 +0,0 @@
<%= modal_form_wrapper title: t(".title") do %>
<%= render "form", merchant: @merchant %>
<% end %>

View file

@ -1,2 +1 @@
<p>Placeholder: rules/_form partial</p>

View file

@ -1,2 +1 @@
<p>Placeholder: rules#edit</p>

View file

@ -1,2 +1 @@
<p>Placeholder: rules#new</p>

View file

@ -1,2 +1 @@
<p>Placeholder: rules#show</p>

View file

@ -63,9 +63,6 @@
<li>
<%= render "settings/settings_nav_item", name: "Rules", path: rules_path, icon: "git-branch" %>
</li>
<li>
<%= render "settings/settings_nav_item", name: t(".merchants_label"), path: merchants_path, icon: "store" %>
</li>
</ul>
</section>

View file

@ -5,7 +5,6 @@
<%= contextual_menu do %>
<%= contextual_menu_modal_action_item t(".edit_categories"), categories_path, icon: "shapes", turbo_frame: :_top %>
<%= contextual_menu_modal_action_item t(".edit_tags"), tags_path, icon: "tags", turbo_frame: :_top %>
<%= contextual_menu_modal_action_item t(".edit_merchants"), merchants_path, icon: "store", turbo_frame: :_top %>
<%= contextual_menu_modal_action_item t(".edit_imports"), imports_path, icon: "hard-drive-upload", turbo_frame: :_top %>
<% end %>

View file

@ -5,6 +5,7 @@
<%= lucide_icon("search", class: "w-5 h-5 text-secondary absolute inset-y-0 left-2 top-1/2 transform -translate-y-1/2") %>
</div>
<div class="my-2" id="list" data-list-filter-target="list">
<% if Current.family.merchants.any? %>
<% Current.family.merchants.alphabetically.each do |merchant| %>
<div class="filterable-item flex items-center gap-2 p-2" data-filter-name="<%= merchant.name %>">
<%= form.check_box :merchants,
@ -16,10 +17,15 @@
merchant.name,
nil %>
<%= form.label :merchants, value: merchant.name, class: "text-sm text-primary flex items-center gap-2" do %>
<%= circle_logo(merchant.name, hex: merchant.color, size: "sm") %>
<%= circle_logo(merchant.name, size: "sm") %>
<%= merchant.name %>
<% end %>
</div>
<% end %>
<% else %>
<div class="px-3 py-2">
<p class="text-sm text-secondary">No merchants associated with your transactions yet</p>
</div>
<% end %>
</div>
</div>

View file

@ -10,8 +10,6 @@ en:
category_placeholder: Select a category
date_label: Date
details: Details
merchant_label: Merchant
merchant_placeholder: Select a merchant
note_label: Notes
note_placeholder: Enter a note that will be applied to selected transactions
overview: Overview

View file

@ -1,27 +0,0 @@
---
en:
merchants:
create:
error: 'Error creating merchant: %{error}'
success: New merchant created successfully
destroy:
success: Merchant deleted successfully
edit:
title: Edit merchant
form:
name_placeholder: Merchant name
index:
empty: No merchants yet
new: New merchant
title: Merchants
merchant:
confirm_accept: Delete merchant
confirm_body: Are you sure you want to delete this merchant? Removing this merchant
will unlink all associated transactions and may effect your reporting.
confirm_title: Delete merchant?
delete: Delete merchant
edit: Edit merchant
new:
title: New merchant
update:
success: Merchant updated successfully

View file

@ -34,7 +34,7 @@ en:
is irreversible.
title: Delete account?
confirm_reset:
body: Are you sure you want to reset your account? This will delete all your accounts, categories, merchants, tags, and other data. This action cannot be undone.
body: Are you sure you want to reset your account? This will delete all your accounts, categories, tags, and other data. This action cannot be undone.
title: Reset account?
confirm_remove_invitation:
body: Are you sure you want to remove the invitation for %{email}?
@ -47,7 +47,7 @@ en:
delete_account_warning: Deleting your account will permanently remove all
your data and cannot be undone.
reset_account: Reset account
reset_account_warning: Resetting your account will delete all your accounts, categories, merchants, tags, and other data, but keep your user account intact.
reset_account_warning: Resetting your account will delete all your accounts, categories, tags, and other data, but keep your user account intact.
email: Email
first_name: First Name
household_form_input_placeholder: Enter household name

View file

@ -4,7 +4,6 @@ en:
header:
edit_categories: Edit categories
edit_imports: Edit imports
edit_merchants: Edit merchants
edit_tags: Edit tags
import: Import
index:

View file

@ -75,8 +75,6 @@ Rails.application.routes.draw do
resources :budget_categories, only: %i[index show update]
end
resources :merchants, only: %i[index new create edit update destroy]
resources :transfers, only: %i[new create destroy show update]
resources :imports, only: %i[index new show create destroy] do

View file

@ -0,0 +1,29 @@
class RemoveMerchantManagement < ActiveRecord::Migration[7.2]
# This migration removes "manual management" of merchants and moves us to 100% automated
# detection of merchants based on transaction name (using Synth + AI).
# -----
# Once we're confident in changes, we'll come back and remove all "legacy" schemas.
def change
rename_table :merchants, :legacy_merchants
rename_column :account_transactions, :merchant_id, :legacy_merchant_id
create_table :merchants, id: :uuid do |t|
t.string :name, null: false, index: { unique: true }
t.string :website_url
t.string :icon_url
t.timestamps
end
add_reference :account_transactions, :merchant, type: :uuid, foreign_key: true
# Users will now opt-in with "rules", so reset enriched names (original name retained)
reversible do |dir|
dir.up do
execute <<-SQL
UPDATE account_entries
SET enriched_name = NULL
SQL
end
end
end
end

26
db/schema.rb generated
View file

@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema[7.2].define(version: 2025_04_01_194500) do
ActiveRecord::Schema[7.2].define(version: 2025_04_02_195137) do
# These are extensions that must be enabled in order to support this database
enable_extension "pgcrypto"
enable_extension "plpgsql"
@ -80,8 +80,10 @@ ActiveRecord::Schema[7.2].define(version: 2025_04_01_194500) do
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.uuid "category_id"
t.uuid "legacy_merchant_id"
t.uuid "merchant_id"
t.index ["category_id"], name: "index_account_transactions_on_category_id"
t.index ["legacy_merchant_id"], name: "index_account_transactions_on_legacy_merchant_id"
t.index ["merchant_id"], name: "index_account_transactions_on_merchant_id"
end
@ -372,6 +374,17 @@ ActiveRecord::Schema[7.2].define(version: 2025_04_01_194500) do
t.index ["token"], name: "index_invite_codes_on_token", unique: true
end
create_table "legacy_merchants", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
t.string "name", null: false
t.string "color", default: "#e99537", null: false
t.uuid "family_id", null: false
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.string "icon_url"
t.datetime "enriched_at"
t.index ["family_id"], name: "index_legacy_merchants_on_family_id"
end
create_table "loans", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
@ -382,13 +395,11 @@ ActiveRecord::Schema[7.2].define(version: 2025_04_01_194500) do
create_table "merchants", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
t.string "name", null: false
t.string "color", default: "#e99537", null: false
t.uuid "family_id", null: false
t.string "website_url"
t.string "icon_url"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.string "icon_url"
t.datetime "enriched_at"
t.index ["family_id"], name: "index_merchants_on_family_id"
t.index ["name"], name: "index_merchants_on_name", unique: true
end
create_table "messages", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
@ -668,6 +679,7 @@ ActiveRecord::Schema[7.2].define(version: 2025_04_01_194500) do
add_foreign_key "account_holdings", "securities"
add_foreign_key "account_trades", "securities"
add_foreign_key "account_transactions", "categories", on_delete: :nullify
add_foreign_key "account_transactions", "legacy_merchants"
add_foreign_key "account_transactions", "merchants"
add_foreign_key "accounts", "families"
add_foreign_key "accounts", "imports"
@ -686,7 +698,7 @@ ActiveRecord::Schema[7.2].define(version: 2025_04_01_194500) do
add_foreign_key "imports", "families"
add_foreign_key "invitations", "families"
add_foreign_key "invitations", "users", column: "inviter_id"
add_foreign_key "merchants", "families"
add_foreign_key "legacy_merchants", "families"
add_foreign_key "messages", "chats"
add_foreign_key "plaid_accounts", "plaid_items"
add_foreign_key "plaid_items", "families"

View file

@ -49,8 +49,7 @@ class Account::TransactionsControllerTest < ActionDispatch::IntegrationTest
entryable_attributes: {
id: @entry.entryable_id,
tag_ids: [ Tag.first.id, Tag.second.id ],
category_id: Category.first.id,
merchant_id: Merchant.first.id
category_id: Category.first.id
}
}
}
@ -64,7 +63,6 @@ class Account::TransactionsControllerTest < ActionDispatch::IntegrationTest
assert_equal -100, @entry.amount
assert_equal [ Tag.first.id, Tag.second.id ], @entry.entryable.tag_ids.sort
assert_equal Category.first.id, @entry.entryable.category_id
assert_equal Merchant.first.id, @entry.entryable.merchant_id
assert_equal "test notes", @entry.notes
assert_equal false, @entry.excluded
@ -98,7 +96,6 @@ class Account::TransactionsControllerTest < ActionDispatch::IntegrationTest
entry_ids: transactions.map(&:id),
date: 1.day.ago.to_date,
category_id: Category.second.id,
merchant_id: Merchant.second.id,
notes: "Updated note"
}
}
@ -110,7 +107,6 @@ class Account::TransactionsControllerTest < ActionDispatch::IntegrationTest
transactions.reload.each do |transaction|
assert_equal 1.day.ago.to_date, transaction.date
assert_equal Category.second, transaction.account_transaction.category
assert_equal Merchant.second, transaction.account_transaction.merchant
assert_equal "Updated note", transaction.notes
end
end

View file

@ -1,39 +0,0 @@
require "test_helper"
class MerchantsControllerTest < ActionDispatch::IntegrationTest
setup do
sign_in @user = users(:family_admin)
@merchant = merchants(:netflix)
end
test "index" do
get merchants_path
assert_response :success
end
test "new" do
get new_merchant_path
assert_response :success
end
test "should create merchant" do
assert_difference("Merchant.count") do
post merchants_url, params: { merchant: { name: "new merchant", color: "#000000" } }
end
assert_redirected_to merchants_path
end
test "should update merchant" do
patch merchant_url(@merchant), params: { merchant: { name: "new name", color: "#000000" } }
assert_redirected_to merchants_path
end
test "should destroy merchant" do
assert_difference("Merchant.count", -1) do
delete merchant_url(@merchant)
end
assert_redirected_to merchants_path
end
end

View file

@ -50,7 +50,6 @@ class UsersControllerTest < ActionDispatch::IntegrationTest
assert_not Account.exists?(account.id)
assert_not Category.exists?(category.id)
assert_not Tag.exists?(tag.id)
assert_not Merchant.exists?(merchant.id)
assert_not Import.exists?(import.id)
assert_not Budget.exists?(budget.id)
assert_not PlaidItem.exists?(plaid_item.id)

View file

@ -1,13 +1,5 @@
one:
name: Test
family: empty
netflix:
name: Netflix
color: "#fd7f6f"
family: dylan_family
amazon:
name: Amazon
color: "#fd7f6f"
family: dylan_family

View file

@ -8,9 +8,6 @@ class Provider::SynthTest < ActiveSupport::TestCase
@subject = @synth = Provider::Synth.new(ENV["SYNTH_API_KEY"])
end
test "health check" do
VCR.use_cassette("synth/health") do
assert @synth.healthy?
test "health check" do
VCR.use_cassette("synth/health") do
assert @synth.healthy?

View file

@ -8,14 +8,13 @@ class Rule::ActionTest < ActiveSupport::TestCase
@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")
@shopping_category = @family.categories.create!(name: "Shopping")
# 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: 100, name: "Rule test transaction1", merchant: merchants(:amazon))
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: 10, name: "Rule test transaction4", merchant: merchants(:amazon))
create_transaction(date: 1.year.ago.to_date, account: @account, amount: 1000, name: "Rule test transaction5")
@rule_scope = @account.transactions
@ -25,13 +24,13 @@ class Rule::ActionTest < ActiveSupport::TestCase
action = Rule::Action.new(
rule: @transaction_rule,
action_type: "set_transaction_category",
value: @grocery_category.id
value: @shopping_category.id
)
action.apply(@rule_scope)
@rule_scope.reload.each do |transaction|
assert_equal @grocery_category.id, transaction.category_id
assert_equal @shopping_category.id, transaction.category_id
end
end
end

View file

@ -8,14 +8,13 @@ class Rule::ConditionTest < ActiveSupport::TestCase
@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")
@shopping_category = @family.categories.create!(name: "Shopping")
# 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: 100, name: "Rule test transaction1", merchant: merchants(:amazon))
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: 10, name: "Rule test transaction4", merchant: merchants(:amazon))
create_transaction(date: 1.year.ago.to_date, account: @account, amount: 1000, name: "Rule test transaction5")
@rule_scope = @account.transactions
@ -53,7 +52,7 @@ class Rule::ConditionTest < ActiveSupport::TestCase
rule: @transaction_rule,
condition_type: "transaction_merchant",
operator: "=",
value: "Whole Foods"
value: "Amazon"
)
filtered = condition.apply(@rule_scope)
@ -69,7 +68,7 @@ class Rule::ConditionTest < ActiveSupport::TestCase
Rule::Condition.new(
condition_type: "transaction_merchant",
operator: "=",
value: "Whole Foods"
value: "Amazon"
),
Rule::Condition.new(
condition_type: "transaction_amount",
@ -92,7 +91,7 @@ class Rule::ConditionTest < ActiveSupport::TestCase
Rule::Condition.new(
condition_type: "transaction_merchant",
operator: "=",
value: "Whole Foods"
value: "Amazon"
),
Rule::Condition.new(
condition_type: "transaction_amount",

View file

@ -6,31 +6,30 @@ class RuleTest < ActiveSupport::TestCase
setup do
@family = families(:empty)
@account = @family.accounts.create!(name: "Rule test", balance: 1000, currency: "USD", accountable: Depository.new)
@whole_foods_merchant = @family.merchants.create!(name: "Whole Foods")
@groceries_category = @family.categories.create!(name: "Groceries")
@shopping_category = @family.categories.create!(name: "Shopping")
end
test "basic rule" do
transaction_entry = create_transaction(date: Date.current, account: @account, merchant: @whole_foods_merchant)
transaction_entry = create_transaction(date: Date.current, account: @account, merchant: merchants(:amazon))
rule = Rule.create!(
family: @family,
resource_type: "transaction",
effective_date: 1.day.ago.to_date,
conditions: [ Rule::Condition.new(condition_type: "transaction_merchant", operator: "=", value: "Whole Foods") ],
actions: [ Rule::Action.new(action_type: "set_transaction_category", value: "Groceries") ]
conditions: [ Rule::Condition.new(condition_type: "transaction_merchant", operator: "=", value: "Amazon") ],
actions: [ Rule::Action.new(action_type: "set_transaction_category", value: @shopping_category.id) ]
)
rule.apply
transaction_entry.reload
assert_equal @groceries_category, transaction_entry.account_transaction.category
assert_equal @shopping_category, transaction_entry.account_transaction.category
end
test "compound rule" do
transaction_entry1 = create_transaction(date: Date.current, amount: 50, account: @account, merchant: @whole_foods_merchant)
transaction_entry2 = create_transaction(date: Date.current, amount: 100, account: @account, merchant: @whole_foods_merchant)
transaction_entry1 = create_transaction(date: Date.current, amount: 50, account: @account, merchant: merchants(:amazon))
transaction_entry2 = create_transaction(date: Date.current, amount: 100, account: @account, merchant: merchants(:amazon))
# Assign "Groceries" to transactions with a merchant of "Whole Foods" and an amount greater than $60
rule = Rule.create!(
@ -39,11 +38,11 @@ class RuleTest < ActiveSupport::TestCase
effective_date: 1.day.ago.to_date,
conditions: [
Rule::Condition.new(condition_type: "compound", operator: "and", sub_conditions: [
Rule::Condition.new(condition_type: "transaction_merchant", operator: "=", value: "Whole Foods"),
Rule::Condition.new(condition_type: "transaction_merchant", operator: "=", value: "Amazon"),
Rule::Condition.new(condition_type: "transaction_amount", operator: ">", value: 60)
])
],
actions: [ Rule::Action.new(action_type: "set_transaction_category", value: "Groceries") ]
actions: [ Rule::Action.new(action_type: "set_transaction_category", value: @shopping_category.id) ]
)
rule.apply
@ -52,6 +51,6 @@ class RuleTest < ActiveSupport::TestCase
transaction_entry2.reload
assert_nil transaction_entry1.account_transaction.category
assert_equal @groceries_category, transaction_entry2.account_transaction.category
assert_equal @shopping_category, transaction_entry2.account_transaction.category
end
end

View file

@ -63,7 +63,7 @@ class TransactionImportTest < ActiveSupport::TestCase
-> { Tag.count } => 1,
-> { Category.count } => 1,
-> { Account.count } => 1 do
@import.publish
result = @import.publish
end
assert_equal "complete", @import.status

View file

@ -10,7 +10,6 @@ class SettingsTest < ApplicationSystemTestCase
[ "Accounts", accounts_path ],
[ "Tags", tags_path ],
[ "Categories", categories_path ],
[ "Merchants", merchants_path ],
[ "Imports", imports_path ],
[ "What's new", changelog_path ],
[ "Feedback", feedback_path ]