From 7e2c917ca0134a6cad962b4800374359340e5f1c Mon Sep 17 00:00:00 2001 From: Nuwan Date: Tue, 19 Aug 2025 00:07:09 +0530 Subject: [PATCH 1/4] trail end reminderd send emails when the trail perieod expired. --- ...1_add_profile_complete_columns_to_users.rb | 6 +- ...2511_add_signup_survey_sent_at_to_users.rb | 6 +- ruby/lib/jam_ruby.rb | 1 + ruby/lib/jam_ruby/app/mailers/user_mailer.rb | 24 ++++ .../jam_ruby/resque/scheduled/hourly_job.rb | 1 + ruby/spec/mailers/user_mailer_spec.rb | 115 +++++++++++++++++- web/config/application.rb | 1 + web/config/environments/development.rb | 1 + web/config/locales/en.yml | 58 +++++++++ 9 files changed, 205 insertions(+), 8 deletions(-) diff --git a/ruby/db/migrate/20250227125441_add_profile_complete_columns_to_users.rb b/ruby/db/migrate/20250227125441_add_profile_complete_columns_to_users.rb index 2fef976d3..f80af7069 100644 --- a/ruby/db/migrate/20250227125441_add_profile_complete_columns_to_users.rb +++ b/ruby/db/migrate/20250227125441_add_profile_complete_columns_to_users.rb @@ -7,9 +7,9 @@ execute "ALTER TABLE users ADD COLUMN profile_complete_reminder2_sent_at TIMESTAMP" execute "ALTER TABLE users ADD COLUMN profile_complete_reminder3_sent_at TIMESTAMP" - User.find_each(batch_size:100) do |user| - User.where(id:user.id).update_all(profile_completed_at: Time.now) - end + # User.find_each(batch_size:100) do |user| + # User.where(id:user.id).update_all(profile_completed_at: Time.now) + # end #User.where('users.id IN (SELECT player_id FROM musicians_instruments) OR users.id IN (SELECT player_id FROM genre_players)').update_all(profile_completed_at: Time.now) end def self.down diff --git a/ruby/db/migrate/20250605092511_add_signup_survey_sent_at_to_users.rb b/ruby/db/migrate/20250605092511_add_signup_survey_sent_at_to_users.rb index f8f271c99..fdd98c0cc 100644 --- a/ruby/db/migrate/20250605092511_add_signup_survey_sent_at_to_users.rb +++ b/ruby/db/migrate/20250605092511_add_signup_survey_sent_at_to_users.rb @@ -1,9 +1,9 @@ class AddSignupSurveySentAtToUsers < ActiveRecord::Migration def self.up execute "ALTER TABLE users ADD COLUMN signup_survey_sent_at TIMESTAMP" - User.find_each(batch_size:100) do |user| - User.where(id:user.id).update_all(signup_survey_sent_at: Time.now) - end + # User.find_each(batch_size:100) do |user| + # User.where(id:user.id).update_all(signup_survey_sent_at: Time.now) + # end end def self.down execute "ALTER TABLE users DROP COLUMN signup_survey_sent_at" diff --git a/ruby/lib/jam_ruby.rb b/ruby/lib/jam_ruby.rb index fb13366a1..ac8de9372 100755 --- a/ruby/lib/jam_ruby.rb +++ b/ruby/lib/jam_ruby.rb @@ -125,6 +125,7 @@ require "jam_ruby/lib/email_signup_survey" require "jam_ruby/lib/gear_setup_reminder" require "jam_ruby/lib/test_gear_reminder" require "jam_ruby/lib/group_session_reminder" +require "jam_ruby/lib/trial_expires_reminder" require "jam_ruby/amqp/amqp_connection_manager" require "jam_ruby/database" require "jam_ruby/message_factory" diff --git a/ruby/lib/jam_ruby/app/mailers/user_mailer.rb b/ruby/lib/jam_ruby/app/mailers/user_mailer.rb index 81b42ac7e..42678bda3 100644 --- a/ruby/lib/jam_ruby/app/mailers/user_mailer.rb +++ b/ruby/lib/jam_ruby/app/mailers/user_mailer.rb @@ -528,6 +528,30 @@ module JamRuby end end + def trial_expires_reminder1(user) + @user = user + mail(:to => user.email, :subject => I18n.t('user_mailer.trial_expires_reminder1.subject')) do |format| + format.text + format.html { render layout: "user_mailer_beta" } + end + end + + def trial_expires_reminder2(user) + @user = user + mail(:to => user.email, :subject => I18n.t('user_mailer.trial_expires_reminder2.subject')) do |format| + format.text + format.html { render layout: "user_mailer_beta" } + end + end + + def trial_expires_reminder3(user) + @user = user + mail(:to => user.email, :subject => I18n.t('user_mailer.trial_expires_reminder3.subject')) do |format| + format.text + format.html { render layout: "user_mailer_beta" } + end + end + def signup_survey(user) @user = user @subject = I18n.t('user_mailer.signup_survey.subject') diff --git a/ruby/lib/jam_ruby/resque/scheduled/hourly_job.rb b/ruby/lib/jam_ruby/resque/scheduled/hourly_job.rb index 1e617f526..87e828396 100644 --- a/ruby/lib/jam_ruby/resque/scheduled/hourly_job.rb +++ b/ruby/lib/jam_ruby/resque/scheduled/hourly_job.rb @@ -18,6 +18,7 @@ module JamRuby GearSetupReminder.send_reminders TestGearReminder.send_reminders GroupSessionReminder.send_reminders + TrialExpiresReminder.send_reminders ConnectionManager.new.cleanup_dangling @@log.info("done") diff --git a/ruby/spec/mailers/user_mailer_spec.rb b/ruby/spec/mailers/user_mailer_spec.rb index c4428cea3..1e8c4888c 100644 --- a/ruby/spec/mailers/user_mailer_spec.rb +++ b/ruby/spec/mailers/user_mailer_spec.rb @@ -240,11 +240,11 @@ describe UserMailer do end it "includes the correct content in the HTML part" do - expect(mail.html_part.body.to_s).to include("Now that you have set up your gearand checked it out in a solo session on your own") + expect(mail.html_part.body.to_s).to include("Now that you have set up your gear and checked it out in a solo session on your own") end it "includes the correct content in the text part" do - expect(mail.text_part.body.to_s).to include("Now that you have set up your gearand checked it out in a solo session on your own") + expect(mail.text_part.body.to_s).to include("Now that you have set up your gear and checked it out in a solo session on your own") end end @@ -322,4 +322,115 @@ describe UserMailer do end end + describe "sends trial expires reminder 1", focus: true do + let(:mail) { ActionMailer::Base.deliveries.last } + + before(:each) do + ActionMailer::Base.deliveries.clear + UserMailer.trial_expires_reminder1(user).deliver_now + end + + it "sends exactly one email" do + expect(ActionMailer::Base.deliveries.length).to eq(1) + end + + it "has the correct from address" do + mail['from'].to_s.should == UserMailer::DEFAULT_SENDER + end + + it "has the correct recipient" do + expect(mail.to).to eq([user.email]) + end + + it "is multipart" do + expect(mail).to be_multipart + end + + it "has the expected subject" do + expect(mail.subject).to include("Your free gold trial has expired, but you have great options to keep playing!") + end + + it "includes the correct content in the HTML part" do + expect(mail.html_part.body.to_s).to include("We hope you’ve enjoyed your 30-day free gold trial with JamKazam") + end + + it "includes the correct content in the text part" do + expect(mail.text_part.body.to_s).to include("We hope you’ve enjoyed your 30-day free gold trial with JamKazam") + end + end + + describe "sends trial expires reminder 2", focus: true do + let(:mail) { ActionMailer::Base.deliveries.last } + + before(:each) do + ActionMailer::Base.deliveries.clear + UserMailer.trial_expires_reminder2(user).deliver_now + end + + it "sends exactly one email" do + expect(ActionMailer::Base.deliveries.length).to eq(1) + end + + it "has the correct from address" do + mail['from'].to_s.should == UserMailer::DEFAULT_SENDER + end + + it "has the correct recipient" do + expect(mail.to).to eq([user.email]) + end + + it "is multipart" do + expect(mail).to be_multipart + end + + it "has the expected subject" do + expect(mail.subject).to include("Don’t forget to check your options to keep playing") + end + + it "includes the correct content in the HTML part" do + expect(mail.html_part.body.to_s).to include("Your 30-day free gold trial with JamKazam has expired") + end + + it "includes the correct content in the text part" do + expect(mail.text_part.body.to_s).to include("Your 30-day free gold trial with JamKazam has expired") + end + end + + describe "sends trial expires reminder 3", focus: true do + let(:mail) { ActionMailer::Base.deliveries.last } + + before(:each) do + ActionMailer::Base.deliveries.clear + UserMailer.trial_expires_reminder3(user).deliver_now + end + + it "sends exactly one email" do + expect(ActionMailer::Base.deliveries.length).to eq(1) + end + + it "has the correct from address" do + mail['from'].to_s.should == UserMailer::DEFAULT_SENDER + end + + it "has the correct recipient" do + expect(mail.to).to eq([user.email]) + end + + it "is multipart" do + expect(mail).to be_multipart + end + + it "has the expected subject" do + expect(mail.subject).to include("One last reminder!") + end + + it "includes the correct content in the HTML part" do + expect(mail.html_part.body.to_s).to include("Your 30-day free gold trial with JamKazam has expired") + end + + it "includes the correct content in the text part" do + expect(mail.text_part.body.to_s).to include("Your 30-day free gold trial with JamKazam has expired") + end + end + end diff --git a/web/config/application.rb b/web/config/application.rb index ef6447f11..72e55ba7a 100644 --- a/web/config/application.rb +++ b/web/config/application.rb @@ -526,6 +526,7 @@ if defined?(Bundler) config.gear_setup_reminders_effective_from_date = "2025-06-10" config.test_gear_reminders_effective_from_date = "2025-07-24" config.group_session_reminders_effective_from_date = "2025-08-12" + config.trial_expires_reminders_effective_from_date = "2025-08-17" config.action_mailer.asset_host = config.action_controller.asset_host end diff --git a/web/config/environments/development.rb b/web/config/environments/development.rb index 4a71c74be..eb08a6617 100644 --- a/web/config/environments/development.rb +++ b/web/config/environments/development.rb @@ -131,4 +131,5 @@ SampleApp::Application.configure do config.gear_setup_reminders_effective_from_date = "2025-06-10" config.test_gear_reminders_effective_from_date = "2025-07-24" config.group_session_reminders_effective_from_date = "2025-08-12" + config.trial_expires_reminders_effective_from_date = "2025-08-17" end diff --git a/web/config/locales/en.yml b/web/config/locales/en.yml index c369b7a4d..c450dc48f 100644 --- a/web/config/locales/en.yml +++ b/web/config/locales/en.yml @@ -247,6 +247,64 @@ en: And if you get stuck, don’t hesitate to reach out to us to ask for help at support@jamkazam.com. regards: "Best Regards," signature: "JamKazam Team" + + trial_expires_reminder1: + subject: "Your free gold trial has expired, but you have great options to keep playing!" + greeting: "Hello" + paragraph1: | + We hope you’ve enjoyed your 30-day free gold trial with JamKazam. If you’ve used it to play music in online sessions with others, below is a summary of your options to keep playing. If you haven’t finished setting up your gear or haven’t played in sessions with others yet, please send us an email at support@jamkazam.com to let us know where you’re stuck. We have a team that would love to help you!
+
+ paragraph2: | + If you’d like to take advantage of one of our premium plans, check this help article that explains how to set up a payment method. Then review this help article that explains how to select your premium plan. + regards: "Best Regards," + signature: "JamKazam Team" + + trial_expires_reminder2: + subject: "Don’t forget to check your options to keep playing" + greeting: "Hello" + paragraph1: | + Your 30-day free gold trial with JamKazam has expired, but you have great options to continue playing music online.
+
+ paragraph2: | + If you’d like to take advantage of one of our premium plans, check this help article that explains how to set up a payment method. Then review this help article that explains how to select your premium plan. + paragraph3: | + If you haven’t finished setting up your gear or haven’t played in sessions with others yet, please send us an email at support@jamkazam.com to let us know where you’re stuck. We have a team that would love to help you! + regards: "Best Regards," + signature: "JamKazam Team" + + trial_expires_reminder3: + subject: "One last reminder!" + greeting: "Hello" + paragraph1: | + Your 30-day free gold trial with JamKazam has expired, but you have great options to continue playing music online. + + paragraph2: | + If you’d like to take advantage of one of our premium plans, check this help article that explains how to set up a payment method. Then review this help article that explains how to select your premium plan. + paragraph3: | + If you haven’t finished setting up your gear or haven’t played in sessions with others yet, please send us an email at support@jamkazam.com to let us know where you’re stuck. We have a team that would love to help you! + regards: "Best Regards," + signature: "JamKazam Team" signup_survey: subject: "Let us help you to be successful on JamKazam" From d00a0c08f7c1ebb3c290c4d7f75b4bce0bc1f2dc Mon Sep 17 00:00:00 2001 From: Nuwan Date: Tue, 19 Aug 2025 02:13:16 +0530 Subject: [PATCH 2/4] add more changes which were missed in prev. commit --- ...trail_expires_reminder_columns_to_users.rb | 12 ++ .../trial_expires_reminder1.html.erb | 14 +++ .../trial_expires_reminder1.text.erb | 8 ++ .../trial_expires_reminder2.html.erb | 18 +++ .../trial_expires_reminder2.text.erb | 10 ++ .../trial_expires_reminder3.html.erb | 18 +++ .../trial_expires_reminder3.text.erb | 10 ++ .../jam_ruby/lib/trial_expires_reminder.rb | 64 ++++++++++ .../lib/trail_expires_reminder_spec.rb | 116 ++++++++++++++++++ 9 files changed, 270 insertions(+) create mode 100644 ruby/db/migrate/20250817162004_add_trail_expires_reminder_columns_to_users.rb create mode 100644 ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/trial_expires_reminder1.html.erb create mode 100644 ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/trial_expires_reminder1.text.erb create mode 100644 ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/trial_expires_reminder2.html.erb create mode 100644 ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/trial_expires_reminder2.text.erb create mode 100644 ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/trial_expires_reminder3.html.erb create mode 100644 ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/trial_expires_reminder3.text.erb create mode 100644 ruby/lib/jam_ruby/lib/trial_expires_reminder.rb create mode 100644 ruby/spec/jam_ruby/lib/trail_expires_reminder_spec.rb diff --git a/ruby/db/migrate/20250817162004_add_trail_expires_reminder_columns_to_users.rb b/ruby/db/migrate/20250817162004_add_trail_expires_reminder_columns_to_users.rb new file mode 100644 index 000000000..869fa84a2 --- /dev/null +++ b/ruby/db/migrate/20250817162004_add_trail_expires_reminder_columns_to_users.rb @@ -0,0 +1,12 @@ + class AddTrailExpiresReminderColumnsToUsers < ActiveRecord::Migration + def self.up + execute "ALTER TABLE users ADD COLUMN trial_expires_reminder1_sent_at TIMESTAMP" + execute "ALTER TABLE users ADD COLUMN trial_expires_reminder2_sent_at TIMESTAMP" + execute "ALTER TABLE users ADD COLUMN trial_expires_reminder3_sent_at TIMESTAMP" + end + def self.down + execute "ALTER TABLE users DROP COLUMN trial_expires_reminder1_sent_at" + execute "ALTER TABLE users DROP COLUMN trial_expires_reminder2_sent_at" + execute "ALTER TABLE users DROP COLUMN trial_expires_reminder3_sent_at" + end + end diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/trial_expires_reminder1.html.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/trial_expires_reminder1.html.erb new file mode 100644 index 000000000..b4ad59e1e --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/trial_expires_reminder1.html.erb @@ -0,0 +1,14 @@ +

<%=I18n.t('user_mailer.trial_expires_reminder1.greeting') -%> <%= @user.first_name -%> -

+ +

+ <%=I18n.t('user_mailer.trial_expires_reminder1.paragraph1').html_safe -%> +

+ +

+ <%=I18n.t('user_mailer.trial_expires_reminder1.paragraph2').html_safe -%> +

+ +

+ <%=I18n.t('user_mailer.trial_expires_reminder1.regards') -%>,
+ <%=I18n.t('user_mailer.trial_expires_reminder1.signature') -%> +

\ No newline at end of file diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/trial_expires_reminder1.text.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/trial_expires_reminder1.text.erb new file mode 100644 index 000000000..36c478dfb --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/trial_expires_reminder1.text.erb @@ -0,0 +1,8 @@ +<%=I18n.t('user_mailer.trial_expires_reminder1.greeting') -%> <%= @user.first_name -%> - + +<%=I18n.t('user_mailer.trial_expires_reminder1.paragraph1') -%> + +<%=I18n.t('user_mailer.trial_expires_reminder1.paragraph2') -%> + +<%=I18n.t('user_mailer.trial_expires_reminder1.regards') -%>, +<%=I18n.t('user_mailer.trial_expires_reminder1.signature') -%> \ No newline at end of file diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/trial_expires_reminder2.html.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/trial_expires_reminder2.html.erb new file mode 100644 index 000000000..88f61b0aa --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/trial_expires_reminder2.html.erb @@ -0,0 +1,18 @@ +

<%=I18n.t('user_mailer.trial_expires_reminder2.greeting') -%> <%= @user.first_name -%> -

+ +

+ <%=I18n.t('user_mailer.trial_expires_reminder2.paragraph1').html_safe -%> +

+ +

+ <%=I18n.t('user_mailer.trial_expires_reminder2.paragraph2').html_safe -%> +

+ +

+ <%=I18n.t('user_mailer.trial_expires_reminder2.paragraph3').html_safe -%> +

+ +

+ <%=I18n.t('user_mailer.trial_expires_reminder2.regards') -%>,
+ <%=I18n.t('user_mailer.trial_expires_reminder2.signature') -%> +

\ No newline at end of file diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/trial_expires_reminder2.text.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/trial_expires_reminder2.text.erb new file mode 100644 index 000000000..85d7112b2 --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/trial_expires_reminder2.text.erb @@ -0,0 +1,10 @@ +<%=I18n.t('user_mailer.trial_expires_reminder2.greeting') -%> <%= @user.first_name -%> - + +<%=I18n.t('user_mailer.trial_expires_reminder2.paragraph1') -%> + +<%=I18n.t('user_mailer.trial_expires_reminder2.paragraph2') -%> + +<%=I18n.t('user_mailer.trial_expires_reminder2.paragraph3') -%> + +<%=I18n.t('user_mailer.trial_expires_reminder2.regards') -%>, +<%=I18n.t('user_mailer.trial_expires_reminder2.signature') -%> \ No newline at end of file diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/trial_expires_reminder3.html.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/trial_expires_reminder3.html.erb new file mode 100644 index 000000000..b5d7f9cc3 --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/trial_expires_reminder3.html.erb @@ -0,0 +1,18 @@ +

<%=I18n.t('user_mailer.trial_expires_reminder3.greeting') -%> <%= @user.first_name -%> -

+ +

+ <%=I18n.t('user_mailer.trial_expires_reminder3.paragraph1').html_safe -%> +

+ +

+ <%=I18n.t('user_mailer.trial_expires_reminder3.paragraph2').html_safe -%> +

+ +

+ <%=I18n.t('user_mailer.trial_expires_reminder3.paragraph3').html_safe -%> +

+ +

+ <%=I18n.t('user_mailer.trial_expires_reminder3.regards') -%>,
+ <%=I18n.t('user_mailer.trial_expires_reminder3.signature') -%> +

\ No newline at end of file diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/trial_expires_reminder3.text.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/trial_expires_reminder3.text.erb new file mode 100644 index 000000000..2461bb066 --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/trial_expires_reminder3.text.erb @@ -0,0 +1,10 @@ +<%=I18n.t('user_mailer.trial_expires_reminder3.greeting') -%> <%= @user.first_name -%> - + +<%=I18n.t('user_mailer.trial_expires_reminder3.paragraph1') -%> + +<%=I18n.t('user_mailer.trial_expires_reminder3.paragraph2') -%> + +<%=I18n.t('user_mailer.trial_expires_reminder3.paragraph3') -%> + +<%=I18n.t('user_mailer.trial_expires_reminder3.regards') -%>, +<%=I18n.t('user_mailer.trial_expires_reminder3.signature') -%> \ No newline at end of file diff --git a/ruby/lib/jam_ruby/lib/trial_expires_reminder.rb b/ruby/lib/jam_ruby/lib/trial_expires_reminder.rb new file mode 100644 index 000000000..e4dd9540e --- /dev/null +++ b/ruby/lib/jam_ruby/lib/trial_expires_reminder.rb @@ -0,0 +1,64 @@ +# lib/jam_ruby/lib/trial_expires_reminder.rb +# +# Sends reminder emails after a user's free trial has ended. +# +# Usage: JamRuby::TrialExpiresReminder.send_reminders +# +module JamRuby + class TrialExpiresReminder + @@log = Logging.logger[TrialExpiresReminder] + + FIRST_NOTIFICATION_CHECK = 1.day.ago.to_s + SECOND_NOTIFICATION_CHECK = 5.days.ago.to_s + THIRD_NOTIFICATION_CHECK = 9.days.ago + + def self.prospect_users(cutoff_date) + User.where("(users.subscription_trial_ends_at IS NOT NULL AND users.subscription_trial_ends_at > ?)", cutoff_date) + end + + def self.reminder1_users(cutoff_date) + prospect_users(cutoff_date).where("users.subscription_last_checked_at < ? AND users.subscription_trial_ends_at < ? AND users.trial_expires_reminder1_sent_at IS NULL", 1.day.ago, FIRST_NOTIFICATION_CHECK) + end + + def self.reminder2_users(cutoff_date) + prospect_users(cutoff_date).where("users.subscription_last_checked_at < ? AND users.subscription_trial_ends_at < ? AND trial_expires_reminder1_sent_at IS NOT NULL AND users.trial_expires_reminder2_sent_at IS NULL", 1.day.ago, SECOND_NOTIFICATION_CHECK) + end + + def self.reminder3_users(cutoff_date) + prospect_users(cutoff_date).where("users.subscription_last_checked_at < ? AND users.subscription_trial_ends_at < ? AND trial_expires_reminder2_sent_at IS NOT NULL AND users.trial_expires_reminder3_sent_at IS NULL", 1.day.ago, THIRD_NOTIFICATION_CHECK) + end + + def self.send_reminders + cod = Date.parse(Rails.application.config.trial_expires_reminders_effective_from_date) # Define a cutoff date for the trial expires reminder emails + + reminder1_users(cod).find_each(batch_size: 100) do |user| + begin + UserMailer.trial_expires_reminder1(user).deliver_now + user.update_attribute(:trial_expires_reminder1_sent_at, Time.now) + rescue Exception => e + @@log.error("unable to send trial_expires_reminder1 email #{e}") + puts "unable to send trial_expires_reminder1 email #{e}" + end + end + reminder2_users(cod).find_each(batch_size: 100) do |user| + begin + UserMailer.trial_expires_reminder2(user).deliver_now + user.update_attribute(:trial_expires_reminder2_sent_at, Time.now) + rescue Exception => e + @@log.error("unable to send trial_expires_reminder2 email #{e}") + puts "unable to send trial_expires_reminder2 email #{e}" + end + end + reminder3_users(cod).find_each(batch_size: 100) do |user| + begin + UserMailer.trial_expires_reminder3(user).deliver_now + user.update_attribute(:trial_expires_reminder3_sent_at, Time.now) + rescue Exception => e + @@log.error("unable to send trial_expires_reminder3 email #{e}") + puts "unable to send trial_expires_reminder3 email #{e}" + end + end + end + + end +end \ No newline at end of file diff --git a/ruby/spec/jam_ruby/lib/trail_expires_reminder_spec.rb b/ruby/spec/jam_ruby/lib/trail_expires_reminder_spec.rb new file mode 100644 index 000000000..9943e6368 --- /dev/null +++ b/ruby/spec/jam_ruby/lib/trail_expires_reminder_spec.rb @@ -0,0 +1,116 @@ +require "spec_helper" + +describe TrialExpiresReminder do + let(:user1) { FactoryGirl.create(:user) } + let(:user2) { FactoryGirl.create(:user) } + let(:user3) { FactoryGirl.create(:user) } + + before(:each) do + ActionMailer::Base.delivery_method = :test + ActionMailer::Base.perform_deliveries = true + ActionMailer::Base.deliveries = [] + + User.delete_all + UserMailer.deliveries.clear + Rails.application.config.trial_expires_reminders_effective_from_date = 2.weeks.ago.to_s + end + + after(:each) do + ActionMailer::Base.deliveries.clear + end + + it "sends reminder emails to users whose trials are about to expire" do + user1.subscription_trial_ends_at = 1.days.from_now + user1.subscription_last_checked_at = 2.days.ago + user1.save! + + user2.subscription_trial_ends_at = 1.days.ago + user2.subscription_last_checked_at = 2.days.ago + user2.save! + + TrialExpiresReminder.send_reminders + + expect(ActionMailer::Base.deliveries.count).to eq(1) + expect(ActionMailer::Base.deliveries.map(&:to).flatten).to include(user2.email) + # Check if the first reminder email was sent by verifying the subject + expect(ActionMailer::Base.deliveries.last.subject).to include("Your free gold trial has expired, but you have great options to keep playing!") + + expect(user2.reload.trial_expires_reminder1_sent_at).not_to be_nil + end + + it "does not send reminder emails to users who have already received them" do + user1.subscription_trial_ends_at = 1.days.ago + user1.subscription_last_checked_at = 2.days.ago + user1.trial_expires_reminder1_sent_at = Time.now + user1.save! + + TrialExpiresReminder.send_reminders + + expect(ActionMailer::Base.deliveries.count).to eq(0) + end + + it "sends the second reminder email to users whose trials are about to expire" do + user1.subscription_trial_ends_at = 4.days.ago + user1.subscription_last_checked_at = 1.days.ago + user1.trial_expires_reminder1_sent_at = Time.now + user1.save! + + user2.subscription_trial_ends_at = 5.days.ago + user2.subscription_last_checked_at = 1.days.ago + user2.trial_expires_reminder1_sent_at = Time.now + user2.save! + + TrialExpiresReminder.send_reminders + + expect(ActionMailer::Base.deliveries.count).to eq(1) + expect(ActionMailer::Base.deliveries.map(&:to).flatten).to include(user2.email) + # Check if the second reminder email was sent by verifying the subject + expect(ActionMailer::Base.deliveries.last.subject).to include("Don’t forget to check your options to keep playing") + + expect(user2.reload.trial_expires_reminder2_sent_at).not_to be_nil + end + + it "sends the third reminder email to users whose trials are about to expire" do + user1.subscription_trial_ends_at = 10.days.ago + user1.subscription_last_checked_at = 1.days.ago + user1.trial_expires_reminder1_sent_at = 6.days.ago + user1.trial_expires_reminder2_sent_at = 4.days.ago + user1.save! + + TrialExpiresReminder.send_reminders + + expect(ActionMailer::Base.deliveries.count).to eq(1) + expect(ActionMailer::Base.deliveries.map(&:to).flatten).to include(user1.email) + # Check if the third reminder email was sent by verifying the subject + expect(ActionMailer::Base.deliveries.last.subject).to include("One last reminder!") + + expect(user1.reload.trial_expires_reminder3_sent_at).not_to be_nil + end + + it "sends first and second and third reminder emails to users whose trials are about to expire" do + user1.subscription_trial_ends_at = 2.days.ago + user1.subscription_last_checked_at = 1.days.ago + user1.save! + + user2.subscription_trial_ends_at = 6.days.ago + user2.subscription_last_checked_at = 1.days.ago + user2.trial_expires_reminder1_sent_at = 4.days.ago + user2.save! + + user3.subscription_trial_ends_at = 10.days.ago + user3.subscription_last_checked_at = 2.days.ago + user3.trial_expires_reminder1_sent_at = 6.days.ago + user3.trial_expires_reminder2_sent_at = 4.days.ago + user3.save! + + TrialExpiresReminder.send_reminders + + expect(user1.reload.trial_expires_reminder1_sent_at).not_to be_nil + expect(user2.reload.trial_expires_reminder2_sent_at).not_to be_nil + expect(user3.reload.trial_expires_reminder3_sent_at).not_to be_nil + + expect(ActionMailer::Base.deliveries.count).to eq(3) + expect(ActionMailer::Base.deliveries.map(&:to).flatten).to include(user1.email, user2.email, user3.email) + + end +end \ No newline at end of file From 9d6c71829f98ab5670a833e79db0639e203be8b3 Mon Sep 17 00:00:00 2001 From: Nuwan Date: Tue, 19 Aug 2025 20:39:39 +0530 Subject: [PATCH 3/4] unsubscribe/change email confirmation fixes --- .../account/change-email-confirm-page.cy.js | 3 +- jam-ui/cypress/e2e/home/unsubscribe.cy.js | 15 +++- .../components/dashboard/JKDashboardMain.js | 5 ++ .../components/public/JKConfirmEmailChange.js | 21 +++-- jam-ui/src/components/public/JKUnsubscribe.js | 78 +++++++++++-------- jam-ui/src/i18n/locales/en/account.json | 6 ++ jam-ui/src/i18n/locales/en/unsubscribe.json | 5 +- jam-ui/src/layouts/JKPublicRoutes.js | 4 +- .../views/layouts/user_mailer_beta.html.erb | 2 +- web/app/controllers/api_users_controller.rb | 2 +- web/app/controllers/users_controller.rb | 21 +++-- 11 files changed, 99 insertions(+), 63 deletions(-) diff --git a/jam-ui/cypress/e2e/account/change-email-confirm-page.cy.js b/jam-ui/cypress/e2e/account/change-email-confirm-page.cy.js index 9cdb5448f..829a45c74 100644 --- a/jam-ui/cypress/e2e/account/change-email-confirm-page.cy.js +++ b/jam-ui/cypress/e2e/account/change-email-confirm-page.cy.js @@ -8,6 +8,7 @@ describe('Change Email Confirm Page', () => { email: 'sam@example.com' }); cy.stubAuthenticate({ ...currentUser }); + cy.intercept('POST', /\S+\/update_email/, { statusCode: 200, body: { ok: true } }); }); it('should display the confirm page when visiting the confirm URL', () => { @@ -15,7 +16,7 @@ describe('Change Email Confirm Page', () => { const token = 'dummy-confirm-token'; // Visit the confirm URL - cy.visit(`/public/confirm-email-change?token=${token}`); + cy.visit(`/confirm-email-change?token=${token}`); // Assert that the JKChangeEmailConfirm page is rendered // Adjust selectors/texts as per your actual component diff --git a/jam-ui/cypress/e2e/home/unsubscribe.cy.js b/jam-ui/cypress/e2e/home/unsubscribe.cy.js index d018ba172..1f85a6c40 100644 --- a/jam-ui/cypress/e2e/home/unsubscribe.cy.js +++ b/jam-ui/cypress/e2e/home/unsubscribe.cy.js @@ -1,24 +1,31 @@ /// +import makeFakeUser from '../../factories/user'; + describe('Unsubscribe from email link', () => { - beforeEach(() =>{ + beforeEach(() => { // cy.intercept('POST', /\S+\/unsubscribe_user_match\/\S+/, { statusCode: 200, body: { ok: true } }); + const currentUser = makeFakeUser({ + email: 'sam@example.com' + }); + cy.stubAuthenticate({ ...currentUser }); cy.intercept('POST', /\S+\/unsubscribe\/\S+/, { statusCode: 200, body: { ok: true } }); }) it("redirect to home page if tok is not provided", () => { - cy.visit('/public/unsubscribe'); + cy.visit('/unsubscribe'); cy.location('pathname').should('eq', '/errors/404'); }); it("show unsubscribed message", () => { - cy.visit('/public/unsubscribe/123'); + cy.visit('/unsubscribe/123'); // cy.location('search') // .should('equal', '?tok=123') // .then((s) => new URLSearchParams(s)) // .invoke('get', 'tok') // .should('equal', '123') - cy.contains("Unsubscribe from JamKazam emails") + cy.contains("You have successfully unsubscribed from JamKazam emails.").should('be.visible'); + cy.contains("Loading...").should('not.exist'); }); }) \ No newline at end of file diff --git a/jam-ui/src/components/dashboard/JKDashboardMain.js b/jam-ui/src/components/dashboard/JKDashboardMain.js index c5cb2255c..356825a0b 100644 --- a/jam-ui/src/components/dashboard/JKDashboardMain.js +++ b/jam-ui/src/components/dashboard/JKDashboardMain.js @@ -55,6 +55,9 @@ import JKMyJamTracks from '../jamtracks/JKMyJamTracks'; import JKJamTrackShow from '../jamtracks/JKJamTrackShow'; import JKPayPalConfirmation from '../shopping-cart/JKPayPalConfirmation'; +import JKUnsubscribe from '../public/JKUnsubscribe'; +import JKConfirmEmailChange from '../public/JKConfirmEmailChange'; + //import loadable from '@loadable/component'; //const DashboardRoutes = loadable(() => import('../../layouts/JKDashboardRoutes')); @@ -319,6 +322,8 @@ function JKDashboardMain() { + + {/*Redirect*/} diff --git a/jam-ui/src/components/public/JKConfirmEmailChange.js b/jam-ui/src/components/public/JKConfirmEmailChange.js index 1029eff38..2d0c33ff7 100644 --- a/jam-ui/src/components/public/JKConfirmEmailChange.js +++ b/jam-ui/src/components/public/JKConfirmEmailChange.js @@ -3,13 +3,17 @@ import { useLocation } from "react-router-dom"; import { Card, CardBody, CardText, CardTitle } from 'reactstrap'; import { useState, useEffect } from 'react'; import { updateEmail } from '../../helpers/rest'; +import { useTranslation } from 'react-i18next'; const JKConfirmEmailChange = () => { + const { t } = useTranslation("account"); const location = useLocation(); const params = new URLSearchParams(location.search); const token = params.get('token'); const [success, setSuccess] = useState(false); + const [error, setError] = useState(null); + const [loading, setLoading] = useState(true); useEffect(() => { if (token) { @@ -23,6 +27,9 @@ const JKConfirmEmailChange = () => { }) .catch(() => { setSuccess(false); + setError(t('identity.changed_email_confirmation.error')); + }).finally(() => { + setLoading(false); }); } }, [token]); @@ -31,15 +38,13 @@ const JKConfirmEmailChange = () => { - Change Email Confirmation + {t('identity.changed_email_confirmation.title')} - - { - success? - 'Your email has been successfully updated.' : - 'Loading...' - } - + <> + {loading &&
{t('identity.changed_email_confirmation.loading')}
} + {success &&
{t('identity.changed_email_confirmation.success')}
} + {error &&
{error}
} +
) diff --git a/jam-ui/src/components/public/JKUnsubscribe.js b/jam-ui/src/components/public/JKUnsubscribe.js index 54704abe2..b26def779 100644 --- a/jam-ui/src/components/public/JKUnsubscribe.js +++ b/jam-ui/src/components/public/JKUnsubscribe.js @@ -1,16 +1,18 @@ import React, { useEffect, useState } from 'react'; -import { Card, CardBody, CardText, CardTitle } from 'reactstrap'; -import { useTranslation } from "react-i18next"; +import { Card, CardBody, CardText, CardTitle } from 'reactstrap'; +import { useTranslation } from 'react-i18next'; import { useBrowserQuery } from '../../context/BrowserQuery'; import { useHistory, useParams } from "react-router-dom"; const unsubscribeFromNewUsersWeeklyEmail = (token) => { - + + + const baseUrl = process.env.REACT_APP_CLIENT_BASE_URL return new Promise((resolve, reject) => { - fetch(`${baseUrl}/unsubscribe_user_match/${token}`, + fetch(`${baseUrl}/unsubscribe_user_match/${token}`, { method: 'POST' } ).then(response => { if (response.ok) { @@ -25,53 +27,61 @@ const unsubscribeFromNewUsersWeeklyEmail = (token) => { const unsubscribe = (token) => { const baseUrl = process.env.REACT_APP_CLIENT_BASE_URL return new Promise((resolve, reject) => { - fetch(`${baseUrl}/unsubscribe/${token}`, { method: 'POST' }) - .then(response => { - if(response.ok){ - resolve(response) - } else { - reject(response) - } - }) + fetch(`${baseUrl}/unsubscribe/${token}`, { method: 'POST', headers: { 'Content-Type': 'application/json', accept: 'application/json' } }) + .then(response => { + if (response.ok) { + resolve(response) + } else { + reject(response) + } + }).catch(error => { + reject(error); + }); }) } function JKUnsubscribe() { - const {t} = useTranslation() - const queryObj = useBrowserQuery(); + const { t } = useTranslation("unsubscribe"); const history = useHistory() + const [loading, setLoading] = useState(true) const [success, setSuccess] = useState(false) + const [error, setError] = useState(null) const { tok } = useParams(); + useEffect(() => { - if(tok){ + if (tok) { unsubscribe(tok) - .then((resp) => { - if(resp.ok){ - setSuccess(true) - } - }) - .catch(error => console.error(error)) - }else{ + .then((resp) => { + if (resp.ok) { + setSuccess(true) + } else { + setSuccess(false) + } + }) + .catch(error => { + setError(error) + }).finally(() => { + setLoading(false) + }); + } else { history.push('/') } }, []) - + return ( - - + - Unsubscribe from JamKazam emails - - { - success? - 'You have been unsubscribed from all JamKazam emails. You will no longer receive any emails from JamKazam.' : - 'Unsubscribing...' - } - + {t('page_title')} + <> + {loading &&
{t('loading')}
} + {success &&
{t('success')}
} + {error &&
{t('error')}
} + +
- + ) } diff --git a/jam-ui/src/i18n/locales/en/account.json b/jam-ui/src/i18n/locales/en/account.json index 5e80c50e6..e0d30b5be 100644 --- a/jam-ui/src/i18n/locales/en/account.json +++ b/jam-ui/src/i18n/locales/en/account.json @@ -26,6 +26,12 @@ "confirmation_email_sent": "A confirmation email has been sent to your email address. Please click the link in the email to confirm your email address." } }, + "changed_email_confirmation": { + "title": "Change Email Confirmation", + "loadding": "Loading...", + "success": "Your email has been successfully changed.", + "error": "An error occurred while confirming your email change. Please try again later." + }, "password_form": { "title": "Password", "help_text": "To update the password associated with your account, enter your current password (for security reasons) and the new password, and click the \"Save Password\" button.", diff --git a/jam-ui/src/i18n/locales/en/unsubscribe.json b/jam-ui/src/i18n/locales/en/unsubscribe.json index 23293c247..3e552d7cb 100644 --- a/jam-ui/src/i18n/locales/en/unsubscribe.json +++ b/jam-ui/src/i18n/locales/en/unsubscribe.json @@ -1,3 +1,6 @@ { - "page_title": "Unsubscribe" + "page_title": "Unsubscribe from JamKazam emails", + "success": "You have successfully unsubscribed from JamKazam emails.", + "error": "An error occurred while unsubscribing. Please try again later.", + "loading": "Loading..." } \ No newline at end of file diff --git a/jam-ui/src/layouts/JKPublicRoutes.js b/jam-ui/src/layouts/JKPublicRoutes.js index f4330eb0c..dd1df5eca 100644 --- a/jam-ui/src/layouts/JKPublicRoutes.js +++ b/jam-ui/src/layouts/JKPublicRoutes.js @@ -23,8 +23,8 @@ const JKPublicRoutes = ({ match: { url } }) => ( - - + {/* + */} diff --git a/ruby/lib/jam_ruby/app/views/layouts/user_mailer_beta.html.erb b/ruby/lib/jam_ruby/app/views/layouts/user_mailer_beta.html.erb index 703b915c4..ca95ba2be 100644 --- a/ruby/lib/jam_ruby/app/views/layouts/user_mailer_beta.html.erb +++ b/ruby/lib/jam_ruby/app/views/layouts/user_mailer_beta.html.erb @@ -30,7 +30,7 @@

<%= I18n.t "mailer_layout.footer.paragraph1" -%> JamKazam.
<%= I18n.t "mailer_layout.footer.you_can" -%>