Merged in 5662-trial_end_reminder_emails (pull request #68)

trail end reminderd

Approved-by: Seth Call
This commit is contained in:
Nuwan Chaturanga 2025-08-19 16:07:57 +00:00 committed by Seth Call
commit 4f837ae67f
16 changed files with 469 additions and 2 deletions

View File

@ -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

View File

@ -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"

View File

@ -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')

View File

@ -0,0 +1,14 @@
<p><%=I18n.t('user_mailer.trial_expires_reminder1.greeting') -%> <%= @user.first_name -%> -</p>
<p>
<%=I18n.t('user_mailer.trial_expires_reminder1.paragraph1').html_safe -%>
</p>
<p>
<%=I18n.t('user_mailer.trial_expires_reminder1.paragraph2').html_safe -%>
</p>
<p>
<%=I18n.t('user_mailer.trial_expires_reminder1.regards') -%>,<br />
<%=I18n.t('user_mailer.trial_expires_reminder1.signature') -%>
</p>

View File

@ -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') -%>

View File

@ -0,0 +1,18 @@
<p><%=I18n.t('user_mailer.trial_expires_reminder2.greeting') -%> <%= @user.first_name -%> -</p>
<p>
<%=I18n.t('user_mailer.trial_expires_reminder2.paragraph1').html_safe -%>
</p>
<p>
<%=I18n.t('user_mailer.trial_expires_reminder2.paragraph2').html_safe -%>
</p>
<p>
<%=I18n.t('user_mailer.trial_expires_reminder2.paragraph3').html_safe -%>
</p>
<p>
<%=I18n.t('user_mailer.trial_expires_reminder2.regards') -%>,<br />
<%=I18n.t('user_mailer.trial_expires_reminder2.signature') -%>
</p>

View File

@ -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') -%>

View File

@ -0,0 +1,18 @@
<p><%=I18n.t('user_mailer.trial_expires_reminder3.greeting') -%> <%= @user.first_name -%> -</p>
<p>
<%=I18n.t('user_mailer.trial_expires_reminder3.paragraph1').html_safe -%>
</p>
<p>
<%=I18n.t('user_mailer.trial_expires_reminder3.paragraph2').html_safe -%>
</p>
<p>
<%=I18n.t('user_mailer.trial_expires_reminder3.paragraph3').html_safe -%>
</p>
<p>
<%=I18n.t('user_mailer.trial_expires_reminder3.regards') -%>,<br />
<%=I18n.t('user_mailer.trial_expires_reminder3.signature') -%>
</p>

View File

@ -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') -%>

View File

@ -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

View File

@ -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")

View File

@ -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("Dont 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

View File

@ -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 youve 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 youve 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("Dont 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

View File

@ -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

View File

@ -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

View File

@ -247,6 +247,64 @@ en:
And if you get stuck, dont hesitate to reach out to us to ask for help at <a href="mailto:support@jamkazam.com">support@jamkazam.com</a>.
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 youve enjoyed your 30-day free gold trial with JamKazam. If youve used it to play music in online sessions with others, below is a summary of your options to keep playing. If you havent finished setting up your gear or havent played in sessions with others yet, please send us an email at <a href="mailto:support@jamkazam.com">support@jamkazam.com</a> to let us know where youre stuck. We have a team that would love to help you!<br />
<ul>
<li>
<strong>Free Plan </strong> You can keep playing free on JamKazam with your current account. Our free plan lets you play 4 hours per month.
</li>
<li>
<strong>Premium Plans </strong> You can also subscribe to our silver, gold, or platinum plans, starting at $4.99/month, with upgrades to unlimited play time, higher audio quality, HD video, and access to premium features like audio and video recording, session broadcasting, and much more. <a href="https://jamkazam.freshdesk.com/support/solutions/articles/66000122535">Read our help article on plans</a> to get all the details and see whats right for you.
</li>
</ul><br />
paragraph2: |
If youd like to take advantage of one of our premium plans, <a href="https://jamkazam.freshdesk.com/support/solutions/articles/66000529807">check this help article</a> that explains how to set up a payment method. Then <a href="https://jamkazam.freshdesk.com/support/solutions/articles/66000529117">review this help article</a> that explains how to select your premium plan.
regards: "Best Regards,"
signature: "JamKazam Team"
trial_expires_reminder2:
subject: "Dont 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. <br />
<ul>
<li>
<strong>Free Plan </strong> You can keep playing free on JamKazam with your current account. Our free plan lets you play 4 hours per month.
</li>
<li>
<strong>Premium Plans </strong> You can also subscribe to our silver, gold, or platinum plans, starting at $4.99/month, with upgrades to unlimited play time, higher audio quality, HD video, and access to premium features like audio and video recording, session broadcasting, and much more. <a href="https://jamkazam.freshdesk.com/support/solutions/articles/66000122535">Read our help article on plans</a> to get all the details and see whats right for you.
</li>
</ul><br />
paragraph2: |
If youd like to take advantage of one of our premium plans, <a href="https://jamkazam.freshdesk.com/support/solutions/articles/66000529807">check this help article</a> that explains how to set up a payment method. Then <a href="https://jamkazam.freshdesk.com/support/solutions/articles/66000529117">review this help article</a> that explains how to select your premium plan.
paragraph3: |
If you havent finished setting up your gear or havent played in sessions with others yet, please send us an email at <a href="mailto:support@jamkazam.com">support@jamkazam.com</a> to let us know where youre 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.
<ul>
<li>
<strong>Free Plan </strong> You can keep playing free on JamKazam with your current account. Our free plan lets you play 4 hours per month.
</li>
<li>
<strong>Premium Plans </strong> You can also subscribe to our silver, gold, or platinum plans, starting at $4.99/month, with upgrades to unlimited play time, higher audio quality, HD video, and access to premium features like audio and video recording, session broadcasting, and much more. <a href="https://jamkazam.freshdesk.com/support/solutions/articles/66000122535">Read our help article on plans</a> to get all the details and see whats right for you.
</li>
</ul>
paragraph2: |
If youd like to take advantage of one of our premium plans, <a href="https://jamkazam.freshdesk.com/support/solutions/articles/66000529807">check this help article</a> that explains how to set up a payment method. Then <a href="https://jamkazam.freshdesk.com/support/solutions/articles/66000529117">review this help article</a> that explains how to select your premium plan.
paragraph3: |
If you havent finished setting up your gear or havent played in sessions with others yet, please send us an email at support@jamkazam.com to let us know where youre 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"