Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature: Add the ability to "revert" a CSV import #1814

Merged
merged 5 commits into from
Feb 7, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion app/controllers/imports_controller.rb
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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

Expand Down
7 changes: 7 additions & 0 deletions app/jobs/revert_import_job.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
class RevertImportJob < ApplicationJob
queue_as :latency_low

def perform(import)
import.revert
end
end
2 changes: 1 addition & 1 deletion app/models/demo/generator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
34 changes: 33 additions & 1 deletion app/models/import.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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: [ ",", ";" ] }
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions app/views/import/configurations/_account_import.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -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 %>
2 changes: 2 additions & 0 deletions app/views/import/configurations/_mint_import.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@
<%= form.select :signage_convention, [["Incomes are negative", "inflows_negative"], ["Incomes are positive", "inflows_positive"]], { label: true }, disabled: import.complete? %>
</div>

<%= 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? %>
Expand Down
2 changes: 2 additions & 0 deletions app/views/import/configurations/_trade_import.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@
<%= form.select :signage_convention, [["Buys are positive qty", "inflows_positive"], ["Buys are negative qty", "inflows_negative"]], label: true %>
</div>

<%= 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)" } %>
Expand Down
2 changes: 2 additions & 0 deletions app/views/import/configurations/_transaction_import.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@
<%= form.select :signage_convention, [["Incomes are positive", "inflows_positive"], ["Incomes are negative", "inflows_negative"]], label: true %>
</div>

<%= 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)" } %>
Expand Down
19 changes: 18 additions & 1 deletion app/views/imports/_import.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,14 @@
<span class="px-1 py text-xs rounded-full bg-red-500/5 text-red-500 border border-alpha-black-50">
<%= t(".failed") %>
</span>
<% elsif import.reverting? %>
<span class="px-1 py text-xs rounded-full bg-orange-500/5 text-orange-500 border border-alpha-black-50">
<%= t(".reverting") %>
</span>
<% elsif import.revert_failed? %>
<span class="px-1 py text-xs rounded-full bg-red-500/5 text-red-500 border border-alpha-black-50">
<%= t(".revert_failed") %>
</span>
<% elsif import.complete? %>
<span class="px-1 py text-xs rounded-full bg-green-500/5 text-green-500 border border-alpha-black-50">
<%= t(".complete") %>
Expand All @@ -33,7 +41,16 @@
<span><%= t(".view") %></span>
<% 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" %>

<span>Revert</span>
<% 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",
Expand Down
18 changes: 18 additions & 0 deletions app/views/imports/_revert_failure.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<%# locals: (import:) %>

<div class="h-full flex flex-col justify-center items-center">
<div class="space-y-6 max-w-sm">
<div class="mx-auto bg-red-500/5 h-8 w-8 rounded-full flex items-center justify-center">
<%= lucide_icon "alert-octagon", class: "w-5 h-5 text-red-500" %>
</div>

<div class="text-center space-y-2">
<h1 class="font-medium text-gray-900 text-center text-3xl">Reverting import failed</h1>
<p class="text-sm text-gray-500">Please try again or contact support.</p>
</div>

<div>
<%= button_to "Try again", revert_import_path(import), class: "btn btn--primary text-center w-full" %>
</div>
</div>
</div>
2 changes: 2 additions & 0 deletions app/views/imports/show.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -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 %>
2 changes: 2 additions & 0 deletions config/locales/views/imports/en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
1 change: 1 addition & 0 deletions config/routes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
21 changes: 21 additions & 0 deletions db/migrate/20250206003115_remove_import_status_enum.rb
Original file line number Diff line number Diff line change
@@ -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
3 changes: 1 addition & 2 deletions db/schema.rb

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions test/fixtures/imports.yml
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
transaction:
family: dylan_family
type: TransactionImport
status: pending
7 changes: 7 additions & 0 deletions test/jobs/revert_import_job_test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
require "test_helper"

class RevertImportJobTest < ActiveJob::TestCase
# test "the truth" do
# assert true
# end
end