Add "Reset account" followed by sample data preload (#163)

* Add reset with sample data option on profile settings

* No need for "member" user in preload

* Cleanup/shorten copy
This commit is contained in:
Juan José Mata
2025-09-25 11:43:23 +02:00
committed by GitHub
parent 3264a96249
commit dfd467ccb5
12 changed files with 138 additions and 10 deletions

View File

@@ -1,6 +1,6 @@
class UsersController < ApplicationController
before_action :set_user
before_action :ensure_admin, only: :reset
before_action :ensure_admin, only: %i[reset reset_with_sample_data]
def update
@user = Current.user
@@ -40,6 +40,11 @@ class UsersController < ApplicationController
redirect_to settings_profile_path, notice: t(".success")
end
def reset_with_sample_data
FamilyResetJob.perform_later(Current.family, load_sample_data_for_email: @user.email)
redirect_to settings_profile_path, notice: t(".success")
end
def destroy
if @user.deactivate
Current.session.destroy

View File

@@ -1,7 +1,7 @@
class FamilyResetJob < ApplicationJob
queue_as :low_priority
def perform(family)
def perform(family, load_sample_data_for_email: nil)
# Delete all family data except users
ActiveRecord::Base.transaction do
# Delete accounts and related data
@@ -12,7 +12,11 @@ class FamilyResetJob < ApplicationJob
family.plaid_items.destroy_all
family.imports.destroy_all
family.budgets.destroy_all
end
if load_sample_data_for_email.present?
Demo::Generator.new.generate_new_user_data_for!(family.reload, email: load_sample_data_for_email)
else
family.sync_later
end
end

View File

@@ -1,3 +1,5 @@
require "securerandom"
class Demo::Generator
# @param seed [Integer, String, nil] Seed value used to initialise the internal PRNG. If nil, the ENV variable DEMO_DATA_SEED will
# be honoured and default to a random seed when not present.
@@ -59,6 +61,25 @@ class Demo::Generator
end
end
def generate_new_user_data_for!(family, email:)
with_timing(__method__, max_seconds: 1000) do
family = family.reload
admin_user = ensure_admin_user!(family, email)
puts "📊 Creating sample financial data for #{family.name}..."
ActiveRecord::Base.transaction do
create_realistic_categories!(family)
create_realistic_accounts!(family)
create_realistic_transactions!(family)
generate_budget_auto_fill!(family)
end
family.sync_later
puts "✅ Sample data loaded successfully!"
end
end
# Generate comprehensive realistic demo data with multi-currency
def generate_default_data!(skip_clear: false, email: "user@example.com")
if skip_clear
@@ -118,6 +139,17 @@ class Demo::Generator
Demo::DataCleaner.new.destroy_everything!
end
def ensure_admin_user!(family, email)
user = family.users.find_by(email: email)
return user if user&.admin? || user&.super_admin?
raise ActiveRecord::RecordNotFound, "No admin user with email #{email} found in family ##{family.id}"
end
def partner_email_for(email)
"partner_#{email}"
end
def create_family_and_users!(family_name, email, onboarded:, subscribed:)
family = Family.create!(
name: family_name,

View File

@@ -137,9 +137,30 @@
href: reset_user_path(@user),
method: :delete,
confirm: CustomConfirm.new(
title: "Reset account?",
body: "This will delete all data associated with your account. Your user profile will remain active.",
btn_text: "Reset account",
title: t(".confirm_reset.title"),
body: t(".confirm_reset.body"),
btn_text: t(".reset_account"),
destructive: true,
high_severity: true
)
) %>
</div>
<div class="flex flex-col md:flex-row md:items-center md:justify-between gap-4">
<div class="w-full md:w-2/3">
<h3 class="font-medium text-primary"><%= t(".reset_account_with_sample_data") %></h3>
<p class="text-secondary text-sm"><%= t(".reset_account_with_sample_data_warning") %></p>
</div>
<%= render DS::Button.new(
text: t(".reset_account_with_sample_data"),
variant: "destructive",
href: reset_with_sample_data_user_path(@user),
method: :delete,
confirm: CustomConfirm.new(
title: t(".confirm_reset_with_sample_data.title"),
body: t(".confirm_reset_with_sample_data.body"),
btn_text: t(".reset_account_with_sample_data"),
destructive: true,
high_severity: true
)

View File

@@ -51,6 +51,9 @@ en:
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.
title: Reset account?
confirm_reset_with_sample_data:
body: Are you sure you want to reset your account and load sample data? This will delete your existing data and replace it with demo data so you can explore Sure safely.
title: Reset account and load sample data?
confirm_remove_invitation:
body: Are you sure you want to remove the invitation for %{email}?
title: Remove Invitation
@@ -63,6 +66,8 @@ en:
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_with_sample_data: Reset and preload
reset_account_with_sample_data_warning: Delete all your existing data and then load fresh sample data so you can explore with a pre-filled environment.
email: Email
first_name: First Name
household_form_input_placeholder: Enter household name

View File

@@ -35,6 +35,9 @@ nb:
confirm_reset:
body: Er du sikker på at du vil tilbakestille kontoen din? Dette vil slette alle kontoene dine, kategorier, forhandlere, tagger og andre data. Denne handlingen kan ikke angres.
title: Tilbakestill konto?
confirm_reset_with_sample_data:
body: Are you sure you want to reset your account and load sample data? This will delete your existing data and replace it with demo data so you can explore Sure safely.
title: Reset account and load sample data?
confirm_remove_invitation:
body: Er du sikker på at du vil fjerne invitasjonen for %{email}?
title: Fjern invitasjon
@@ -47,6 +50,8 @@ nb:
dataene dine og kan ikke angres.
reset_account: Tilbakestill konto
reset_account_warning: Tilbakestilling av kontoen din vil slette alle kontoene dine, kategorier, forhandlere, tagger og andre data, men beholde brukerkontoen din intakt.
reset_account_with_sample_data: Reset account and load sample data
reset_account_with_sample_data_warning: Resetting your account will delete all your existing data and then load fresh sample data so you can explore Sure with a pre-filled environment.
email: E-post
first_name: Fornavn
household_form_input_placeholder: Angi husholdningsnavn

View File

@@ -35,6 +35,9 @@ tr:
confirm_reset:
body: Hesabınızı sıfırlamak istediğinizden emin misiniz? Bu işlem tüm hesaplarınızı, kategorilerinizi, satıcılarınızı, etiketlerinizi ve diğer verilerinizi silecektir. Bu işlem geri alınamaz.
title: Hesap sıfırlansın mı?
confirm_reset_with_sample_data:
body: Are you sure you want to reset your account and load sample data? This will delete your existing data and replace it with demo data so you can explore Sure safely.
title: Reset account and load sample data?
confirm_remove_invitation:
body: "%{email} için daveti kaldırmak istediğinizden emin misiniz?"
title: "Daveti Kaldır"
@@ -46,6 +49,8 @@ tr:
delete_account_warning: Hesabınızı silmek tüm verilerinizi kalıcı olarak kaldırır ve geri alınamaz.
reset_account: Hesabı sıfırla
reset_account_warning: Hesabınızı sıfırlamak tüm hesaplarınızı, kategorilerinizi, satıcılarınızı, etiketlerinizi ve diğer verilerinizi silecek, ancak kullanıcı hesabınızı koruyacaktır.
reset_account_with_sample_data: Reset account and load sample data
reset_account_with_sample_data_warning: Resetting your account will delete all your existing data and then load fresh sample data so you can explore Sure with a pre-filled environment.
email: E-posta
first_name: Ad
household_form_input_placeholder: Hane adı girin

View File

@@ -11,3 +11,5 @@ en:
reset:
success: Your account has been reset. Data will be deleted in the background in some time.
unauthorized: You are not authorized to perform this action
reset_with_sample_data:
success: Your account has been reset and sample data is being prepared. Youll see demo data shortly.

View File

@@ -7,6 +7,9 @@ nb:
email_change_failed: Kunne ikke endre e-postadresse.
email_change_initiated: Vennligst sjekk din nye e-postadresse for bekreftelsesinstruksjoner.
success: Profilen din har blitt oppdatert.
reset:
success: Kontoen din har blitt tilbakestilt. Data vil bli slettet i bakgrunnen om en stund.
unauthorized: Du er ikke autorisert til å utføre denne handlingen.
reset:
success: Kontoen din har blitt tilbakestilt. Data vil bli slettet i bakgrunnen om en stund.
unauthorized: Du er ikke autorisert til å utføre denne handlingen.
reset_with_sample_data:
success: Kontoen din har blitt tilbakestilt og demodata forberedes. Du vil snart se eksempeldata.

View File

@@ -9,4 +9,7 @@ tr:
success: Profiliniz güncellendi.
reset:
success: Hesabınız sıfırlandı. Verileriniz arka planda bir süre sonra silinecek.
unauthorized: Bu işlemi gerçekleştirmeye yetkiniz yok.
unauthorized: Bu işlemi gerçekleştirmeye yetkiniz yok.
reset_with_sample_data:
success: Hesabınız sıfırlandı ve örnek veriler hazırlanıyor. Kısa süre içinde demo verileri göreceksiniz.

View File

@@ -43,6 +43,7 @@ Rails.application.routes.draw do
resources :users, only: %i[update destroy] do
delete :reset, on: :member
delete :reset_with_sample_data, on: :member
patch :rule_prompt_settings, on: :member
end

View File

@@ -40,7 +40,9 @@ class UsersControllerTest < ActionDispatch::IntegrationTest
budget = budgets(:one)
plaid_item = plaid_items(:one)
Provider::Plaid.any_instance.expects(:remove_item).with(plaid_item.access_token).once
provider = mock
provider.expects(:remove_item).with(plaid_item.access_token).once
PlaidItem.any_instance.stubs(:plaid_provider).returns(provider)
perform_enqueued_jobs(only: FamilyResetJob) do
delete reset_user_url(@user)
@@ -58,6 +60,36 @@ class UsersControllerTest < ActionDispatch::IntegrationTest
assert_not PlaidItem.exists?(plaid_item.id)
end
test "admin can reset family data and load sample data" do
account = accounts(:investment)
category = categories(:income)
tag = tags(:one)
merchant = merchants(:netflix)
import = imports(:transaction)
budget = budgets(:one)
plaid_item = plaid_items(:one)
provider = mock
provider.expects(:remove_item).with(plaid_item.access_token).once
PlaidItem.any_instance.stubs(:plaid_provider).returns(provider)
Demo::Generator.any_instance.expects(:generate_new_user_data_for!).with(@user.family, email: @user.email)
perform_enqueued_jobs(only: FamilyResetJob) do
delete reset_with_sample_data_user_url(@user)
end
assert_redirected_to settings_profile_url
assert_equal I18n.t("users.reset_with_sample_data.success"), flash[:notice]
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)
end
test "non-admin cannot reset family data" do
sign_in @member = users(:family_member)
@@ -68,6 +100,16 @@ class UsersControllerTest < ActionDispatch::IntegrationTest
assert_no_enqueued_jobs only: FamilyResetJob
end
test "non-admin cannot reset family data with sample data" do
sign_in @member = users(:family_member)
delete reset_with_sample_data_user_url(@member)
assert_redirected_to settings_profile_url
assert_equal I18n.t("users.reset.unauthorized"), flash[:alert]
assert_no_enqueued_jobs only: FamilyResetJob
end
test "member can deactivate their account" do
sign_in @member = users(:family_member)
delete user_url(@member)