diff --git a/app/controllers/account/transactions_controller.rb b/app/controllers/account/transactions_controller.rb index 028fd5d0..f067e92a 100644 --- a/app/controllers/account/transactions_controller.rb +++ b/app/controllers/account/transactions_controller.rb @@ -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 diff --git a/app/controllers/merchants_controller.rb b/app/controllers/merchants_controller.rb deleted file mode 100644 index cbcf1cdb..00000000 --- a/app/controllers/merchants_controller.rb +++ /dev/null @@ -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 diff --git a/app/helpers/settings_helper.rb b/app/helpers/settings_helper.rb index b7cfbcb6..84fba93e 100644 --- a/app/helpers/settings_helper.rb +++ b/app/helpers/settings_helper.rb @@ -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 } ] diff --git a/app/jobs/family_reset_job.rb b/app/jobs/family_reset_job.rb index 185df111..ba216924 100644 --- a/app/jobs/family_reset_job.rb +++ b/app/jobs/family_reset_job.rb @@ -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 diff --git a/app/models/account/enrichable.rb b/app/models/account/enrichable.rb deleted file mode 100644 index 260aec5a..00000000 --- a/app/models/account/enrichable.rb +++ /dev/null @@ -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 diff --git a/app/models/account/entry.rb b/app/models/account/entry.rb index b53db19b..dedd4db6 100644 --- a/app/models/account/entry.rb +++ b/app/models/account/entry.rb @@ -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? diff --git a/app/models/demo/generator.rb b/app/models/demo/generator.rb index d9b3e726..668bf2d2 100644 --- a/app/models/demo/generator.rb +++ b/app/models/demo/generator.rb @@ -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 diff --git a/app/models/family.rb b/app/models/family.rb index 3a8d95f0..52487aa5 100644 --- a/app/models/family.rb +++ b/app/models/family.rb @@ -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 diff --git a/app/models/merchant.rb b/app/models/merchant.rb index e363f6aa..fcc8d8bf 100644 --- a/app/models/merchant.rb +++ b/app/models/merchant.rb @@ -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 diff --git a/app/models/plaid_account.rb b/app/models/plaid_account.rb index e0e71f67..7528567f 100644 --- a/app/models/plaid_account.rb +++ b/app/models/plaid_account.rb @@ -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) diff --git a/app/views/account/transactions/bulk_edit.html.erb b/app/views/account/transactions/bulk_edit.html.erb index ed9a6194..99d29c3a 100644 --- a/app/views/account/transactions/bulk_edit.html.erb +++ b/app/views/account/transactions/bulk_edit.html.erb @@ -39,7 +39,6 @@
<%= 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 %>
diff --git a/app/views/account/transactions/show.html.erb b/app/views/account/transactions/show.html.erb index e101e8cf..d359e304 100644 --- a/app/views/account/transactions/show.html.erb +++ b/app/views/account/transactions/show.html.erb @@ -67,11 +67,8 @@ <%= 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" %> + :id, :name, + { label: "Merchant", class: "text-subdued" }, disabled: true %> <%= ef.select :tag_ids, Current.family.tags.alphabetically.pluck(:name, :id), diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb index d250f1e8..6b6e303b 100644 --- a/app/views/layouts/application.html.erb +++ b/app/views/layouts/application.html.erb @@ -1,8 +1,8 @@ <%= render "layouts/shared/htmldoc" do %> <% sidebar_config = app_sidebar_config(Current.user) %> -
- <%= tag.div class: class_names("py-4 shrink-0 h-full overflow-y-auto transition-all duration-300"), + <%= tag.div class: class_names("py-4 shrink-0 h-full overflow-y-auto transition-all duration-300"), style: "width: #{sidebar_config.dig(:left_panel, :initial_width)}px", data: { sidebar_target: "leftPanel" } do %> <% if content_for?(:sidebar) %> diff --git a/app/views/merchants/_form.html.erb b/app/views/merchants/_form.html.erb deleted file mode 100644 index 93efecb9..00000000 --- a/app/views/merchants/_form.html.erb +++ /dev/null @@ -1,24 +0,0 @@ -
- <%= styled_form_with model: @merchant, class: "space-y-4", data: { turbo_frame: :_top } do |f| %> -
-
- <%= render partial: "shared/color_avatar", locals: { name: @merchant.name, color: @merchant.color } %> -
-
- <% Merchant::COLORS.each do |color| %> - - <% end %> -
-
- <%= f.text_field :name, placeholder: t(".name_placeholder"), autofocus: true, required: true, data: { color_avatar_target: "name" } %> -
-
- -
- <%= f.submit %> -
- <% end %> -
diff --git a/app/views/merchants/_merchant.html.erb b/app/views/merchants/_merchant.html.erb deleted file mode 100644 index 12e23c6c..00000000 --- a/app/views/merchants/_merchant.html.erb +++ /dev/null @@ -1,33 +0,0 @@ -<%# locals: (merchant:) %> - -
-
- <% if merchant.icon_url %> -
- <%= image_tag merchant.icon_url, class: "w-8 h-8 rounded-full" %> -
- <% else %> - <%= render partial: "shared/color_avatar", locals: { name: merchant.name, color: merchant.color } %> - <% end %> - -

- <%= merchant.name %> -

-
-
- <%= contextual_menu do %> -
- <%= 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 %> -
- <% end %> -
-
diff --git a/app/views/merchants/_ruler.html.erb b/app/views/merchants/_ruler.html.erb deleted file mode 100644 index 70442f58..00000000 --- a/app/views/merchants/_ruler.html.erb +++ /dev/null @@ -1,3 +0,0 @@ -
-
-
diff --git a/app/views/merchants/edit.html.erb b/app/views/merchants/edit.html.erb deleted file mode 100644 index a8776d3a..00000000 --- a/app/views/merchants/edit.html.erb +++ /dev/null @@ -1,3 +0,0 @@ -<%= modal_form_wrapper title: t(".title") do %> - <%= render "form", merchant: @merchant %> -<% end %> diff --git a/app/views/merchants/index.html.erb b/app/views/merchants/index.html.erb deleted file mode 100644 index 55b1a080..00000000 --- a/app/views/merchants/index.html.erb +++ /dev/null @@ -1,36 +0,0 @@ -
-

<%= t(".title") %>

- - <%= 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" %> -

<%= t(".new") %>

- <% end %> -
- -
- <% if @merchants.any? %> -
-
-

<%= t(".title") %>

- · -

<%= @merchants.count %>

-
- -
-
- <%= render partial: @merchants, spacer_template: "merchants/ruler" %> -
-
-
- <% else %> -
-
-

<%= t(".empty") %>

- <%= 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") %> - <%= t(".new") %> - <% end %> -
-
- <% end %> -
diff --git a/app/views/merchants/new.html.erb b/app/views/merchants/new.html.erb deleted file mode 100644 index a8776d3a..00000000 --- a/app/views/merchants/new.html.erb +++ /dev/null @@ -1,3 +0,0 @@ -<%= modal_form_wrapper title: t(".title") do %> - <%= render "form", merchant: @merchant %> -<% end %> diff --git a/app/views/rules/_form.html.erb b/app/views/rules/_form.html.erb index 4ac65aae..fd8aae6a 100644 --- a/app/views/rules/_form.html.erb +++ b/app/views/rules/_form.html.erb @@ -1,2 +1 @@

Placeholder: rules/_form partial

- \ No newline at end of file diff --git a/app/views/rules/edit.html.erb b/app/views/rules/edit.html.erb index df7abe4c..8fbad0fb 100644 --- a/app/views/rules/edit.html.erb +++ b/app/views/rules/edit.html.erb @@ -1,2 +1 @@

Placeholder: rules#edit

- \ No newline at end of file diff --git a/app/views/rules/index.html.erb b/app/views/rules/index.html.erb index 4d581fc4..35a63598 100644 --- a/app/views/rules/index.html.erb +++ b/app/views/rules/index.html.erb @@ -1,3 +1,3 @@ <% content_for :page_title, "Rules" %> -

Placeholder: rules#index

\ No newline at end of file +

Placeholder: rules#index

diff --git a/app/views/rules/new.html.erb b/app/views/rules/new.html.erb index a72fe5c4..164d108d 100644 --- a/app/views/rules/new.html.erb +++ b/app/views/rules/new.html.erb @@ -1,2 +1 @@

Placeholder: rules#new

- \ No newline at end of file diff --git a/app/views/rules/show.html.erb b/app/views/rules/show.html.erb index 661b881f..53b3c97b 100644 --- a/app/views/rules/show.html.erb +++ b/app/views/rules/show.html.erb @@ -1,2 +1 @@

Placeholder: rules#show

- \ No newline at end of file diff --git a/app/views/settings/_settings_nav.html.erb b/app/views/settings/_settings_nav.html.erb index 683a0b68..965c53b4 100644 --- a/app/views/settings/_settings_nav.html.erb +++ b/app/views/settings/_settings_nav.html.erb @@ -63,9 +63,6 @@
  • <%= render "settings/settings_nav_item", name: "Rules", path: rules_path, icon: "git-branch" %>
  • -
  • - <%= render "settings/settings_nav_item", name: t(".merchants_label"), path: merchants_path, icon: "store" %> -
  • diff --git a/app/views/transactions/_header.html.erb b/app/views/transactions/_header.html.erb index cbb77fb2..9a55ed11 100644 --- a/app/views/transactions/_header.html.erb +++ b/app/views/transactions/_header.html.erb @@ -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 %> diff --git a/app/views/transactions/searches/filters/_merchant_filter.html.erb b/app/views/transactions/searches/filters/_merchant_filter.html.erb index 9910134c..e783539a 100644 --- a/app/views/transactions/searches/filters/_merchant_filter.html.erb +++ b/app/views/transactions/searches/filters/_merchant_filter.html.erb @@ -5,9 +5,10 @@ <%= lucide_icon("search", class: "w-5 h-5 text-secondary absolute inset-y-0 left-2 top-1/2 transform -translate-y-1/2") %>
    - <% Current.family.merchants.alphabetically.each do |merchant| %> -
    - <%= form.check_box :merchants, + <% if Current.family.merchants.any? %> + <% Current.family.merchants.alphabetically.each do |merchant| %> +
    + <%= form.check_box :merchants, { multiple: true, checked: @q[:merchants]&.include?(merchant.name), @@ -15,10 +16,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") %> - <%= merchant.name %> - <% end %> + <%= form.label :merchants, value: merchant.name, class: "text-sm text-primary flex items-center gap-2" do %> + <%= circle_logo(merchant.name, size: "sm") %> + <%= merchant.name %> + <% end %> +
    + <% end %> + <% else %> +
    +

    No merchants associated with your transactions yet

    <% end %>
    diff --git a/config/locales/views/account/transactions/en.yml b/config/locales/views/account/transactions/en.yml index 397c7bb5..d68d502f 100644 --- a/config/locales/views/account/transactions/en.yml +++ b/config/locales/views/account/transactions/en.yml @@ -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 diff --git a/config/locales/views/merchants/en.yml b/config/locales/views/merchants/en.yml deleted file mode 100644 index 3f31dd59..00000000 --- a/config/locales/views/merchants/en.yml +++ /dev/null @@ -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 diff --git a/config/locales/views/settings/en.yml b/config/locales/views/settings/en.yml index 049d2768..928c4348 100644 --- a/config/locales/views/settings/en.yml +++ b/config/locales/views/settings/en.yml @@ -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 diff --git a/config/locales/views/transactions/en.yml b/config/locales/views/transactions/en.yml index 5e6ab631..fde072e5 100644 --- a/config/locales/views/transactions/en.yml +++ b/config/locales/views/transactions/en.yml @@ -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: diff --git a/config/routes.rb b/config/routes.rb index 1f75c197..0ff7a7e9 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -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 diff --git a/db/migrate/20250402195137_remove_merchant_management.rb b/db/migrate/20250402195137_remove_merchant_management.rb new file mode 100644 index 00000000..7b7044ff --- /dev/null +++ b/db/migrate/20250402195137_remove_merchant_management.rb @@ -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 diff --git a/db/schema.rb b/db/schema.rb index 85bc69b3..40d3371b 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -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" diff --git a/test/controllers/account/transactions_controller_test.rb b/test/controllers/account/transactions_controller_test.rb index d490bfa7..38cbcb48 100644 --- a/test/controllers/account/transactions_controller_test.rb +++ b/test/controllers/account/transactions_controller_test.rb @@ -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 diff --git a/test/controllers/merchants_controller_test.rb b/test/controllers/merchants_controller_test.rb deleted file mode 100644 index 4f84bb0d..00000000 --- a/test/controllers/merchants_controller_test.rb +++ /dev/null @@ -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 diff --git a/test/controllers/users_controller_test.rb b/test/controllers/users_controller_test.rb index bd68fe77..1bdeac82 100644 --- a/test/controllers/users_controller_test.rb +++ b/test/controllers/users_controller_test.rb @@ -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) diff --git a/test/fixtures/merchants.yml b/test/fixtures/merchants.yml index 3e3ca05a..d96aadd1 100644 --- a/test/fixtures/merchants.yml +++ b/test/fixtures/merchants.yml @@ -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 diff --git a/test/models/provider/synth_test.rb b/test/models/provider/synth_test.rb index 83b12d03..5dfa3bed 100644 --- a/test/models/provider/synth_test.rb +++ b/test/models/provider/synth_test.rb @@ -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? diff --git a/test/models/rule/action_test.rb b/test/models/rule/action_test.rb index 8ecc7a67..d5bb51c5 100644 --- a/test/models/rule/action_test.rb +++ b/test/models/rule/action_test.rb @@ -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 diff --git a/test/models/rule/condition_test.rb b/test/models/rule/condition_test.rb index a1a2bf13..c2fc1bf9 100644 --- a/test/models/rule/condition_test.rb +++ b/test/models/rule/condition_test.rb @@ -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", diff --git a/test/models/rule_test.rb b/test/models/rule_test.rb index 4a01892f..874301ad 100644 --- a/test/models/rule_test.rb +++ b/test/models/rule_test.rb @@ -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 diff --git a/test/models/transaction_import_test.rb b/test/models/transaction_import_test.rb index 2299194c..ca6c80e3 100644 --- a/test/models/transaction_import_test.rb +++ b/test/models/transaction_import_test.rb @@ -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 diff --git a/test/system/settings_test.rb b/test/system/settings_test.rb index 0fe32c6b..6c2e61ac 100644 --- a/test/system/settings_test.rb +++ b/test/system/settings_test.rb @@ -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 ]