From dfd467ccb52cb74098105ef03442e02ba46bd235 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juan=20Jos=C3=A9=20Mata?= Date: Thu, 25 Sep 2025 11:43:23 +0200 Subject: [PATCH] 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 --- app/controllers/users_controller.rb | 7 +++- app/jobs/family_reset_job.rb | 6 +++- app/models/demo/generator.rb | 32 +++++++++++++++++ app/views/settings/profiles/show.html.erb | 27 ++++++++++++-- config/locales/views/settings/en.yml | 5 +++ config/locales/views/settings/nb.yml | 5 +++ config/locales/views/settings/tr.yml | 5 +++ config/locales/views/users/en.yml | 2 ++ config/locales/views/users/nb.yml | 9 +++-- config/locales/views/users/tr.yml | 5 ++- config/routes.rb | 1 + test/controllers/users_controller_test.rb | 44 ++++++++++++++++++++++- 12 files changed, 138 insertions(+), 10 deletions(-) diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index 6632a47fa..f2e3ff00c 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -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 diff --git a/app/jobs/family_reset_job.rb b/app/jobs/family_reset_job.rb index 185df1111..60d4916f7 100644 --- a/app/jobs/family_reset_job.rb +++ b/app/jobs/family_reset_job.rb @@ -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 diff --git a/app/models/demo/generator.rb b/app/models/demo/generator.rb index 278dcb3f6..e45771dfa 100644 --- a/app/models/demo/generator.rb +++ b/app/models/demo/generator.rb @@ -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, diff --git a/app/views/settings/profiles/show.html.erb b/app/views/settings/profiles/show.html.erb index e1f0d6536..810d2bbdf 100644 --- a/app/views/settings/profiles/show.html.erb +++ b/app/views/settings/profiles/show.html.erb @@ -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 + ) + ) %> + + +
+
+

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

+

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

+
+ + <%= 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 ) diff --git a/config/locales/views/settings/en.yml b/config/locales/views/settings/en.yml index 3c18e2f00..8c26d0eea 100644 --- a/config/locales/views/settings/en.yml +++ b/config/locales/views/settings/en.yml @@ -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 diff --git a/config/locales/views/settings/nb.yml b/config/locales/views/settings/nb.yml index 0ffe61fed..02c31b7d6 100644 --- a/config/locales/views/settings/nb.yml +++ b/config/locales/views/settings/nb.yml @@ -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 diff --git a/config/locales/views/settings/tr.yml b/config/locales/views/settings/tr.yml index 603f020ef..6c778d470 100644 --- a/config/locales/views/settings/tr.yml +++ b/config/locales/views/settings/tr.yml @@ -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 diff --git a/config/locales/views/users/en.yml b/config/locales/views/users/en.yml index e04ebc8ce..44ab2ff8a 100644 --- a/config/locales/views/users/en.yml +++ b/config/locales/views/users/en.yml @@ -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. You’ll see demo data shortly. diff --git a/config/locales/views/users/nb.yml b/config/locales/views/users/nb.yml index f49be5e82..9b3686868 100644 --- a/config/locales/views/users/nb.yml +++ b/config/locales/views/users/nb.yml @@ -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. \ No newline at end of file + 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. + diff --git a/config/locales/views/users/tr.yml b/config/locales/views/users/tr.yml index cf9f29d56..971b18186 100644 --- a/config/locales/views/users/tr.yml +++ b/config/locales/views/users/tr.yml @@ -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. \ No newline at end of file + 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. + diff --git a/config/routes.rb b/config/routes.rb index 7702f689d..7c049f6c4 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -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 diff --git a/test/controllers/users_controller_test.rb b/test/controllers/users_controller_test.rb index 3909c62d9..d5158b584 100644 --- a/test/controllers/users_controller_test.rb +++ b/test/controllers/users_controller_test.rb @@ -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)