Skip to content

Commit

Permalink
Feature: Add the ability to "revert" a CSV import (#1814)
Browse files Browse the repository at this point in the history
* Allow reverting imports

* Fix tests

* Add currency column to all imports

* Don't auto-enrich demo account
  • Loading branch information
zachgoll authored Feb 7, 2025
1 parent 60925bd commit 536c82f
Show file tree
Hide file tree
Showing 17 changed files with 125 additions and 6 deletions.
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

0 comments on commit 536c82f

Please sign in to comment.