diff --git a/app/jobs/weekly_summary_email_job.rb b/app/jobs/weekly_summary_email_job.rb index 38fa04f..47c027e 100644 --- a/app/jobs/weekly_summary_email_job.rb +++ b/app/jobs/weekly_summary_email_job.rb @@ -5,14 +5,28 @@ class WeeklySummaryEmailJob < ApplicationJob return unless Flipper.enabled?(:weekly_summary_emails) now_utc = reference_time.utc - cutoff = 3.weeks.ago + cutoff = now_utc - 3.weeks - User.subscribed("weekly_summary") - .where(created_at: cutoff..) - .or(User.subscribed("weekly_summary").joins(:heartbeats).where(heartbeats: { time: cutoff.. })) - .distinct - .find_each do |user| + eligible_users(cutoff).find_each do |user| WeeklySummaryUserEmailJob.perform_later(user.id, now_utc.iso8601) end end + + private + + def eligible_users(cutoff) + users = User.arel_table + heartbeats = Heartbeat.arel_table + + recent_activity_exists = Heartbeat.unscoped + .where(heartbeats[:user_id].eq(users[:id])) + .where(heartbeats[:deleted_at].eq(nil)) + .where(heartbeats[:time].gteq(cutoff.to_f)) + .arel + .exists + + User.subscribed("weekly_summary").where( + users[:created_at].gteq(cutoff).or(recent_activity_exists) + ) + end end diff --git a/test/jobs/weekly_summary_email_job_test.rb b/test/jobs/weekly_summary_email_job_test.rb index 0e39b65..576cb6e 100644 --- a/test/jobs/weekly_summary_email_job_test.rb +++ b/test/jobs/weekly_summary_email_job_test.rb @@ -5,6 +5,53 @@ class WeeklySummaryEmailJobTest < ActiveJob::TestCase setup do ActionMailer::Base.deliveries.clear + Flipper.enable(:weekly_summary_emails) + GoodJob::Job.delete_all + end + + teardown do + Flipper.disable(:weekly_summary_emails) + GoodJob::Job.delete_all + end + + test "enqueues for subscribed users who signed up recently or coded recently" do + reference_time = Time.utc(2026, 3, 1, 12, 0, 0) + cutoff = reference_time - 3.weeks + + recent_signup = User.create!(timezone: "UTC") + recent_signup.update_column(:created_at, cutoff + 1.hour) + + recent_coder = User.create!(timezone: "UTC") + recent_coder.update_column(:created_at, cutoff - 1.day) + create_coding_heartbeat(recent_coder, cutoff + 2.hours, "recent-coder", "Ruby") + + stale_user = User.create!(timezone: "UTC") + stale_user.update_column(:created_at, cutoff - 1.day) + create_coding_heartbeat(stale_user, cutoff - 2.hours, "stale-user", "Ruby") + + unsubscribed_recent_coder = User.create!(timezone: "UTC") + unsubscribed_recent_coder.unsubscribe("weekly_summary") + create_coding_heartbeat(unsubscribed_recent_coder, cutoff + 3.hours, "unsubscribed", "Ruby") + + assert_difference -> { GoodJob::Job.where(job_class: "WeeklySummaryUserEmailJob").count }, 2 do + WeeklySummaryEmailJob.perform_now(reference_time) + end + + jobs = GoodJob::Job.where(job_class: "WeeklySummaryUserEmailJob").order(:id) + enqueued_user_ids = jobs.map { |job| job.serialized_params.fetch("arguments").first.to_i }.sort + enqueued_reference_times = jobs.map { |job| job.serialized_params.fetch("arguments").second }.uniq + + assert_equal [ recent_signup.id, recent_coder.id ].sort, enqueued_user_ids + assert_equal [ reference_time.iso8601 ], enqueued_reference_times + end + + test "does not enqueue summaries when feature flag is disabled" do + Flipper.disable(:weekly_summary_emails) + User.create!(timezone: "UTC") + + assert_no_difference -> { GoodJob::Job.where(job_class: "WeeklySummaryUserEmailJob").count } do + WeeklySummaryEmailJob.perform_now(Time.utc(2026, 3, 1, 12, 0, 0)) + end end test "sends weekly summaries only for opted-in users with email at friday 17:30 utc" do