diff --git a/app/controllers/imports_controller.rb b/app/controllers/imports_controller.rb index 8d311215e11..caee3328f78 100644 --- a/app/controllers/imports_controller.rb +++ b/app/controllers/imports_controller.rb @@ -1,5 +1,5 @@ class ImportsController < ApplicationController - before_action :set_import, only: %i[show publish destroy] + before_action :set_import, only: %i[show publish destroy revert] def publish @import.publish_later @@ -31,6 +31,11 @@ def show end end + def revert + @import.revert_later + redirect_to imports_path, notice: "Import is reverting in the background." + end + def destroy @import.destroy diff --git a/app/jobs/revert_import_job.rb b/app/jobs/revert_import_job.rb new file mode 100644 index 00000000000..ac7090b4a30 --- /dev/null +++ b/app/jobs/revert_import_job.rb @@ -0,0 +1,7 @@ +class RevertImportJob < ApplicationJob + queue_as :latency_low + + def perform(import) + import.revert + end +end diff --git a/app/models/demo/generator.rb b/app/models/demo/generator.rb index c20bfa70db3..22038857666 100644 --- a/app/models/demo/generator.rb +++ b/app/models/demo/generator.rb @@ -24,7 +24,7 @@ def reset_data!(family_names) puts "Data cleared" family_names.each_with_index do |family_name, index| - create_family_and_user!(family_name, "user#{index == 0 ? "" : index + 1}@maybe.local", data_enrichment_enabled: index == 0) + create_family_and_user!(family_name, "user#{index == 0 ? "" : index + 1}@maybe.local") end puts "Users reset" diff --git a/app/models/import.rb b/app/models/import.rb index 15646d4425a..472b5e125ac 100644 --- a/app/models/import.rb +++ b/app/models/import.rb @@ -6,7 +6,14 @@ class Import < ApplicationRecord scope :ordered, -> { order(created_at: :desc) } - enum :status, { pending: "pending", complete: "complete", importing: "importing", failed: "failed" }, validate: true + enum :status, { + pending: "pending", + complete: "complete", + importing: "importing", + reverting: "reverting", + revert_failed: "revert_failed", + failed: "failed" + }, validate: true, default: "pending" validates :type, inclusion: { in: TYPES } validates :col_sep, inclusion: { in: [ ",", ";" ] } @@ -35,6 +42,27 @@ def publish update! status: :failed, error: error.message end + def revert_later + raise "Import is not revertable" unless revertable? + + update! status: :reverting + + RevertImportJob.perform_later(self) + end + + def revert + Import.transaction do + accounts.destroy_all + entries.destroy_all + end + + family.sync + + update! status: :pending + rescue => error + update! status: :revert_failed, error: error.message + end + def csv_rows @csv_rows ||= parsed_csv end @@ -113,6 +141,10 @@ def publishable? cleaned? && mappings.all?(&:valid?) end + def revertable? + complete? || revert_failed? + end + def has_unassigned_account? mappings.accounts.where(key: "").any? end diff --git a/app/views/import/configurations/_account_import.html.erb b/app/views/import/configurations/_account_import.html.erb index 2e2a5cd30f9..afcd3e8bc60 100644 --- a/app/views/import/configurations/_account_import.html.erb +++ b/app/views/import/configurations/_account_import.html.erb @@ -4,6 +4,7 @@ <%= form.select :entity_type_col_label, import.csv_headers, { include_blank: "Leave empty", label: "Entity Type" } %> <%= form.select :name_col_label, import.csv_headers, { include_blank: "Leave empty", label: "Name (optional)" } %> <%= form.select :amount_col_label, import.csv_headers, { include_blank: "Leave empty", label: "Balance" } %> + <%= form.select :currency_col_label, import.csv_headers, { include_blank: "Leave empty", label: "Currency" } %> <%= form.submit "Apply configuration", class: "w-full btn btn--primary", disabled: import.complete? %> <% end %> diff --git a/app/views/import/configurations/_mint_import.html.erb b/app/views/import/configurations/_mint_import.html.erb index f5ff48341ae..d0eeced43ec 100644 --- a/app/views/import/configurations/_mint_import.html.erb +++ b/app/views/import/configurations/_mint_import.html.erb @@ -16,6 +16,8 @@ <%= form.select :signage_convention, [["Incomes are negative", "inflows_negative"], ["Incomes are positive", "inflows_positive"]], { label: true }, disabled: import.complete? %> + <%= form.select :currency_col_label, import.csv_headers, { include_blank: "Leave empty", label: "Currency" }, disabled: import.complete? %> + <%= form.select :account_col_label, import.csv_headers, { include_blank: "Leave empty", label: "Account (optional)" }, disabled: import.complete? %> <%= form.select :name_col_label, import.csv_headers, { include_blank: "Leave empty", label: "Name (optional)" }, disabled: import.complete? %> <%= form.select :category_col_label, import.csv_headers, { include_blank: "Leave empty", label: "Category (optional)" }, disabled: import.complete? %> diff --git a/app/views/import/configurations/_trade_import.html.erb b/app/views/import/configurations/_trade_import.html.erb index 278debf845d..b8dab627e6b 100644 --- a/app/views/import/configurations/_trade_import.html.erb +++ b/app/views/import/configurations/_trade_import.html.erb @@ -11,6 +11,8 @@ <%= form.select :signage_convention, [["Buys are positive qty", "inflows_positive"], ["Buys are negative qty", "inflows_negative"]], label: true %> + <%= form.select :currency_col_label, import.csv_headers, { include_blank: "Leave empty", label: "Currency" } %> + <%= form.select :ticker_col_label, import.csv_headers, { include_blank: "Leave empty", label: "Ticker" } %> <%= form.select :price_col_label, import.csv_headers, { include_blank: "Leave empty", label: "Price" } %> <%= form.select :account_col_label, import.csv_headers, { include_blank: "Leave empty", label: "Account (optional)" } %> diff --git a/app/views/import/configurations/_transaction_import.html.erb b/app/views/import/configurations/_transaction_import.html.erb index fc741c85b94..82abb1c5233 100644 --- a/app/views/import/configurations/_transaction_import.html.erb +++ b/app/views/import/configurations/_transaction_import.html.erb @@ -11,6 +11,8 @@ <%= form.select :signage_convention, [["Incomes are positive", "inflows_positive"], ["Incomes are negative", "inflows_negative"]], label: true %> + <%= form.select :currency_col_label, import.csv_headers, { include_blank: "Leave empty", label: "Currency" } %> + <%= form.select :account_col_label, import.csv_headers, { include_blank: "Leave empty", label: "Account (optional)" } %> <%= form.select :name_col_label, import.csv_headers, { include_blank: "Leave empty", label: "Name (optional)" } %> <%= form.select :category_col_label, import.csv_headers, { include_blank: "Leave empty", label: "Category (optional)" } %> diff --git a/app/views/imports/_import.html.erb b/app/views/imports/_import.html.erb index 6620f3b93eb..edffa2c64a4 100644 --- a/app/views/imports/_import.html.erb +++ b/app/views/imports/_import.html.erb @@ -17,6 +17,14 @@ <%= t(".failed") %> + <% elsif import.reverting? %> + + <%= t(".reverting") %> + + <% elsif import.revert_failed? %> + + <%= t(".revert_failed") %> + <% elsif import.complete? %> <%= t(".complete") %> @@ -33,7 +41,16 @@ <%= t(".view") %> <% end %> - <% unless import.complete? %> + <% if import.complete? || import.revert_failed? %> + <%= button_to revert_import_path(import), + method: :put, + class: "block w-full py-2 px-3 space-x-2 text-orange-600 hover:bg-orange-50 flex items-center rounded-lg", + data: { turbo_confirm: true } do %> + <%= lucide_icon "rotate-ccw", class: "w-5 h-5" %> + + Revert + <% end %> + <% else %> <%= button_to import_path(import), method: :delete, class: "block w-full py-2 px-3 space-x-2 text-red-600 hover:bg-red-50 flex items-center rounded-lg", diff --git a/app/views/imports/_revert_failure.html.erb b/app/views/imports/_revert_failure.html.erb new file mode 100644 index 00000000000..cfdb08c314a --- /dev/null +++ b/app/views/imports/_revert_failure.html.erb @@ -0,0 +1,18 @@ +<%# locals: (import:) %> + + + + + <%= lucide_icon "alert-octagon", class: "w-5 h-5 text-red-500" %> + + + + Reverting import failed + Please try again or contact support. + + + + <%= button_to "Try again", revert_import_path(import), class: "btn btn--primary text-center w-full" %> + + + diff --git a/app/views/imports/show.html.erb b/app/views/imports/show.html.erb index 163be64424e..ffa5529757d 100644 --- a/app/views/imports/show.html.erb +++ b/app/views/imports/show.html.erb @@ -10,6 +10,8 @@ <%= render "imports/success", import: @import %> <% elsif @import.failed? %> <%= render "imports/failure", import: @import %> +<% elsif @import.revert_failed? %> + <%= render "imports/revert_failure", import: @import %> <% else %> <%= render "imports/ready", import: @import %> <% end %> diff --git a/config/locales/views/imports/en.yml b/config/locales/views/imports/en.yml index 77bbf059b1b..01fe473f52e 100644 --- a/config/locales/views/imports/en.yml +++ b/config/locales/views/imports/en.yml @@ -63,6 +63,8 @@ en: failed: Failed in_progress: In progress label: "%{type}: %{datetime}" + reverting: Reverting + revert_failed: Revert failed uploading: Processing rows view: View index: diff --git a/config/routes.rb b/config/routes.rb index 26fcc22a3f6..dc04a771d68 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -65,6 +65,7 @@ resources :imports, only: %i[index new show create destroy] do post :publish, on: :member + put :revert, on: :member resource :upload, only: %i[show update], module: :import resource :configuration, only: %i[show update], module: :import diff --git a/db/migrate/20250206003115_remove_import_status_enum.rb b/db/migrate/20250206003115_remove_import_status_enum.rb new file mode 100644 index 00000000000..2ea0866d256 --- /dev/null +++ b/db/migrate/20250206003115_remove_import_status_enum.rb @@ -0,0 +1,21 @@ +class RemoveImportStatusEnum < ActiveRecord::Migration[7.2] + def up + change_column_default :imports, :status, nil + change_column :imports, :status, :string + execute "DROP TYPE IF EXISTS import_status" + end + + def down + execute <<-SQL + CREATE TYPE import_status AS ENUM ( + 'pending', + 'importing', + 'complete', + 'failed' + ); + SQL + + change_column :imports, :status, :import_status, using: 'status::import_status' + change_column_default :imports, :status, 'pending' + end +end diff --git a/db/schema.rb b/db/schema.rb index a7ec28bc3af..5d6a8e12b88 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -18,7 +18,6 @@ # Custom types defined in this database. # Note that some types may not work with other database engines. Be careful if changing database. create_enum "account_status", ["ok", "syncing", "error"] - create_enum "import_status", ["pending", "importing", "complete", "failed"] create_table "account_balances", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| t.uuid "account_id", null: false @@ -391,7 +390,7 @@ create_table "imports", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| t.jsonb "column_mappings" - t.enum "status", default: "pending", enum_type: "import_status" + t.string "status" t.string "raw_file_str" t.string "normalized_csv_str" t.datetime "created_at", null: false diff --git a/test/fixtures/imports.yml b/test/fixtures/imports.yml index 00c09eff0f6..97e2cd72a87 100644 --- a/test/fixtures/imports.yml +++ b/test/fixtures/imports.yml @@ -1,3 +1,4 @@ transaction: family: dylan_family type: TransactionImport + status: pending diff --git a/test/jobs/revert_import_job_test.rb b/test/jobs/revert_import_job_test.rb new file mode 100644 index 00000000000..ca65d717472 --- /dev/null +++ b/test/jobs/revert_import_job_test.rb @@ -0,0 +1,7 @@ +require "test_helper" + +class RevertImportJobTest < ActiveJob::TestCase + # test "the truth" do + # assert true + # end +end
Please try again or contact support.