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