From 53f478a77bee6d9370656a1fa1b4fd4699abad7d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juan=20Jos=C3=A9=20Mata?= Date: Tue, 17 Mar 2026 19:41:26 +0100 Subject: [PATCH] Add scheduled `DemoFamilyRefreshJob` to rebuild demo data daily (#1217) * Add scheduled demo family refresh job Rebuild demo data daily at 5am UTC by anonymizing and enqueueing deletion of the existing demo family while immediately generating new sample data. Add super-admin email notifications with 24-hour session and signup metrics, plus tests for the new job and mailer. * Delete demo monitoring key before family refresh Ensure DemoFamilyRefreshJob removes ApiKey::DEMO_MONITORING_KEY from the old demo family before enqueueing async family destruction and generating replacement sample data. Adds a regression assertion that the key is gone before generator execution. --- app/jobs/demo_family_refresh_job.rb | 80 +++++++++++++++++++ app/mailers/demo_family_refresh_mailer.rb | 16 ++++ .../completed.text.erb | 6 ++ config/schedule.yml | 6 ++ test/jobs/demo_family_refresh_job_test.rb | 64 +++++++++++++++ .../demo_family_refresh_mailer_test.rb | 23 ++++++ 6 files changed, 195 insertions(+) create mode 100644 app/jobs/demo_family_refresh_job.rb create mode 100644 app/mailers/demo_family_refresh_mailer.rb create mode 100644 app/views/demo_family_refresh_mailer/completed.text.erb create mode 100644 test/jobs/demo_family_refresh_job_test.rb create mode 100644 test/mailers/demo_family_refresh_mailer_test.rb diff --git a/app/jobs/demo_family_refresh_job.rb b/app/jobs/demo_family_refresh_job.rb new file mode 100644 index 000000000..65f6a8c9b --- /dev/null +++ b/app/jobs/demo_family_refresh_job.rb @@ -0,0 +1,80 @@ +class DemoFamilyRefreshJob < ApplicationJob + queue_as :scheduled + + def perform + period_end = Time.current + period_start = period_end - 24.hours + + demo_email = Rails.application.config_for(:demo).fetch("email") + demo_user = User.find_by(email: demo_email) + old_family = demo_user&.family + + old_family_session_count = sessions_count_for(old_family, period_start:, period_end:) + newly_created_families_count = Family.where(created_at: period_start...period_end).count + + if old_family + delete_old_family_monitoring_key!(old_family) + anonymize_family_emails!(old_family) + DestroyJob.perform_later(old_family) + end + + Demo::Generator.new.generate_default_data!(skip_clear: true, email: demo_email) + + notify_super_admins!( + old_family:, + old_family_session_count:, + newly_created_families_count:, + period_start:, + period_end: + ) + end + + private + + def sessions_count_for(family, period_start:, period_end:) + return 0 unless family + + Session + .joins(:user) + .where(users: { family_id: family.id }) + .where(created_at: period_start...period_end) + .distinct + .count(:id) + end + + + def delete_old_family_monitoring_key!(family) + ApiKey + .where(user_id: family.users.select(:id), display_key: ApiKey::DEMO_MONITORING_KEY) + .delete_all + end + + def anonymize_family_emails!(family) + family.users.find_each do |user| + user.update_columns( + email: deleted_email_for(user), + unconfirmed_email: nil, + updated_at: Time.current + ) + end + end + + def deleted_email_for(user) + local_part, domain = user.email.split("@", 2) + "#{local_part}+deleting-#{user.id}-#{SecureRandom.hex(4)}@#{domain}" + end + + def notify_super_admins!(old_family:, old_family_session_count:, newly_created_families_count:, period_start:, period_end:) + User.super_admin.find_each do |super_admin| + DemoFamilyRefreshMailer.with( + super_admin:, + old_family_id: old_family&.id, + old_family_name: old_family&.name, + old_family_session_count:, + newly_created_families_count:, + period_start:, + period_end: + ).completed.deliver_later + end + end +end diff --git a/app/mailers/demo_family_refresh_mailer.rb b/app/mailers/demo_family_refresh_mailer.rb new file mode 100644 index 000000000..f44d435eb --- /dev/null +++ b/app/mailers/demo_family_refresh_mailer.rb @@ -0,0 +1,16 @@ +class DemoFamilyRefreshMailer < ApplicationMailer + def completed + @super_admin = params.fetch(:super_admin) + @old_family_id = params[:old_family_id] + @old_family_name = params[:old_family_name] + @old_family_session_count = params.fetch(:old_family_session_count) + @newly_created_families_count = params.fetch(:newly_created_families_count) + @period_start = params.fetch(:period_start) + @period_end = params.fetch(:period_end) + + mail( + to: @super_admin.email, + subject: "Demo family refresh completed" + ) + end +end diff --git a/app/views/demo_family_refresh_mailer/completed.text.erb b/app/views/demo_family_refresh_mailer/completed.text.erb new file mode 100644 index 000000000..cd2e05ce0 --- /dev/null +++ b/app/views/demo_family_refresh_mailer/completed.text.erb @@ -0,0 +1,6 @@ +Demo family refresh has completed. + +Period (UTC): <%= @period_start.iso8601 %> to <%= @period_end.iso8601 %> +Old demo family: <%= @old_family_name || "not found" %><% if @old_family_id %> (ID: <%= @old_family_id %>)<% end %> +Unique login sessions for old demo family in period: <%= @old_family_session_count %> +New family accounts created in period: <%= @newly_created_families_count %> diff --git a/config/schedule.yml b/config/schedule.yml index 74ac99122..c3903a229 100644 --- a/config/schedule.yml +++ b/config/schedule.yml @@ -36,3 +36,9 @@ clean_inactive_families: class: "InactiveFamilyCleanerJob" queue: "scheduled" description: "Archives and destroys families that expired their trial without subscribing (managed mode only)" + +refresh_demo_family: + cron: "0 5 * * *" # daily at 5:00 AM UTC + class: "DemoFamilyRefreshJob" + queue: "scheduled" + description: "Refreshes demo family data and emails super admins with daily usage summary" diff --git a/test/jobs/demo_family_refresh_job_test.rb b/test/jobs/demo_family_refresh_job_test.rb new file mode 100644 index 000000000..991b2ed3c --- /dev/null +++ b/test/jobs/demo_family_refresh_job_test.rb @@ -0,0 +1,64 @@ +require "test_helper" + +class DemoFamilyRefreshJobTest < ActiveJob::TestCase + setup do + @demo_email = "demo-user@example.com" + Rails.application.stubs(:config_for).with(:demo).returns({ "email" => @demo_email }) + + @demo_family = Family.create!(name: "Demo Family") + @demo_user = @demo_family.users.create!( + first_name: "Demo", + last_name: "Admin", + email: @demo_email, + password: "password123", + role: :admin, + onboarded_at: Time.current, + ai_enabled: true, + show_sidebar: true, + show_ai_sidebar: true, + ui_layout: :dashboard + ) + + @super_admin = families(:dylan_family).users.create!( + first_name: "Super", + last_name: "Admin", + email: "super-admin@example.com", + password: "password123", + role: :super_admin, + onboarded_at: Time.current, + ai_enabled: true, + show_sidebar: true, + show_ai_sidebar: true, + ui_layout: :dashboard + ) + end + + test "anonymizes old demo user email, enqueues deletion, regenerates data, and notifies super admins" do + travel_to Time.utc(2026, 1, 1, 5, 0, 0) do + Session.create!(user: @demo_user) + Family.create!(name: "New Family Today", created_at: 6.hours.ago) + Family.create!(name: "Old Family", created_at: 2.days.ago) + @demo_user.api_keys.create!( + name: "monitoring", + key: ApiKey::DEMO_MONITORING_KEY, + scopes: [ "read" ], + source: "monitoring" + ) + + generator = mock + generator.expects(:generate_default_data!).with(skip_clear: true, email: @demo_email) do + assert_nil ApiKey.find_by(display_key: ApiKey::DEMO_MONITORING_KEY) + end + Demo::Generator.expects(:new).returns(generator) + + assert_enqueued_with(job: DestroyJob, args: [ @demo_family ]) do + assert_enqueued_jobs 2, only: ActionMailer::MailDeliveryJob do + DemoFamilyRefreshJob.perform_now + end + end + + assert_not_equal @demo_email, @demo_user.reload.email + assert_match(/\+deleting-/, @demo_user.email) + end + end +end diff --git a/test/mailers/demo_family_refresh_mailer_test.rb b/test/mailers/demo_family_refresh_mailer_test.rb new file mode 100644 index 000000000..53b540bcb --- /dev/null +++ b/test/mailers/demo_family_refresh_mailer_test.rb @@ -0,0 +1,23 @@ +require "test_helper" + +class DemoFamilyRefreshMailerTest < ActionMailer::TestCase + test "completed email includes summary metrics" do + period_start = Time.utc(2026, 1, 1, 5, 0, 0) + period_end = period_start + 24.hours + + email = DemoFamilyRefreshMailer.with( + super_admin: users(:sure_support_staff), + old_family_id: families(:empty).id, + old_family_name: families(:empty).name, + old_family_session_count: 12, + newly_created_families_count: 4, + period_start:, + period_end: + ).completed + + assert_equal [ "support@sure.am" ], email.to + assert_equal "Demo family refresh completed", email.subject + assert_includes email.body.to_s, "Unique login sessions for old demo family in period: 12" + assert_includes email.body.to_s, "New family accounts created in period: 4" + end +end