From 6ca7425dc9c76e1a271316096619af05c6adaff2 Mon Sep 17 00:00:00 2001 From: Uku Taht <Uku.taht@gmail.com> Date: Wed, 21 Apr 2021 15:57:38 +0300 Subject: [PATCH] Send renewal notification for annual subscriptions (#949) * Add background job to send renewal notification * Use copy from Marko * Add expiration email in case the user has cancelled * Add queue config * Use correct tag for expiration email --- config/runtime.exs | 5 +- lib/plausible/billing/plans.ex | 4 + lib/plausible_web/email.ex | 28 ++++ .../yearly_expiration_notification.html.eex | 13 ++ .../yearly_renewal_notification.html.eex | 13 ++ lib/workers/notify_annual_renewal.ex | 50 +++++++ ...0075623_add_sent_renewal_notifications.exs | 10 ++ test/workers/notify_annual_renewal_test.exs | 134 ++++++++++++++++++ 8 files changed, 256 insertions(+), 1 deletion(-) create mode 100644 lib/plausible_web/templates/email/yearly_expiration_notification.html.eex create mode 100644 lib/plausible_web/templates/email/yearly_renewal_notification.html.eex create mode 100644 lib/workers/notify_annual_renewal.ex create mode 100644 priv/repo/migrations/20210420075623_add_sent_renewal_notifications.exs create mode 100644 test/workers/notify_annual_renewal_test.exs diff --git a/config/runtime.exs b/config/runtime.exs index 7c9cc142..82aaf85e 100644 --- a/config/runtime.exs +++ b/config/runtime.exs @@ -195,6 +195,8 @@ if config_env() == :prod && !disable_cron do {"0 12 * * *", Plausible.Workers.SendTrialNotifications}, # Daily at 14 {"0 14 * * *", Plausible.Workers.CheckUsage}, + # Daily at 15 + {"0 15 * * *", Plausible.Workers.NotifyAnnualRenewal}, # Every 10 minutes {"*/10 * * * *", Plausible.Workers.ProvisionSslCertificates} ] @@ -213,7 +215,8 @@ if config_env() == :prod && !disable_cron do extra_queues = [ provision_ssl_certificates: 1, trial_notification_emails: 1, - check_usage: 1 + check_usage: 1, + notify_annual_renewal: 1 ] config :plausible, Oban, diff --git a/lib/plausible/billing/plans.ex b/lib/plausible/billing/plans.ex index fe9f9d6c..e10b09d8 100644 --- a/lib/plausible/billing/plans.ex +++ b/lib/plausible/billing/plans.ex @@ -69,6 +69,10 @@ defmodule Plausible.Billing.Plans do } end + def yearly_plan_ids do + Enum.map(@yearly_plans, fn plan -> plan[:product_id] end) + end + def for_product_id(product_id) do Enum.find(@all_plans, fn plan -> plan[:product_id] == product_id end) end diff --git a/lib/plausible_web/email.ex b/lib/plausible_web/email.ex index 6df1d1db..42ca64ba 100644 --- a/lib/plausible_web/email.ex +++ b/lib/plausible_web/email.ex @@ -124,6 +124,34 @@ defmodule PlausibleWeb.Email do }) end + def yearly_renewal_notification(user) do + date = Timex.format!(user.subscription.next_bill_date, "{Mfull} {D}, {YYYY}") + + base_email() + |> to(user) + |> tag("yearly-renewal") + |> subject("Your Plausible subscription is up for renewal") + |> render("yearly_renewal_notification.html", %{ + user: user, + date: date, + next_bill_amount: user.subscription.next_bill_amount + }) + end + + def yearly_expiration_notification(user) do + date = Timex.format!(user.subscription.next_bill_date, "{Mfull} {D}, {YYYY}") + + base_email() + |> to(user) + |> tag("yearly-expiration") + |> subject("Your Plausible subscription is about to expire") + |> render("yearly_expiration_notification.html", %{ + user: user, + date: date, + next_bill_amount: user.subscription.next_bill_amount + }) + end + def cancellation_email(user) do base_email() |> to(user.email) diff --git a/lib/plausible_web/templates/email/yearly_expiration_notification.html.eex b/lib/plausible_web/templates/email/yearly_expiration_notification.html.eex new file mode 100644 index 00000000..8ee223a8 --- /dev/null +++ b/lib/plausible_web/templates/email/yearly_expiration_notification.html.eex @@ -0,0 +1,13 @@ +Hey <%= user_salutation(@user) %>, +<br /><br /> +Time flies! This is a reminder that your annual subscription for Plausible Analytics will expire on <%= @date %>. +<br /><br /> +You need to renew your subscription on <%= link("account settings page", to: "#{plausible_url()}/billing/upgrade") %> if you want to continue using Plausible to count your website stats in a privacy-friendly way. +<br /><br /> +If you don't want to continue your subscription, there's no action required. You will lose access to the stats on <%= @date %>. +<br /><br /> +Have a question, feedback or need some guidance? Just reply to this email to get in touch! +<br /><br /> +<br /><br /> +Thanks for being a Plausible Analytics subscriber!<br /> +<%= plausible_url() %><br /> diff --git a/lib/plausible_web/templates/email/yearly_renewal_notification.html.eex b/lib/plausible_web/templates/email/yearly_renewal_notification.html.eex new file mode 100644 index 00000000..d61a2467 --- /dev/null +++ b/lib/plausible_web/templates/email/yearly_renewal_notification.html.eex @@ -0,0 +1,13 @@ +Hey <%= user_salutation(@user) %>, +<br /><br /> +Time flies! This is a reminder that your annual subscription for Plausible Analytics is due to renew on <%= @date %>. We will automatically charge $<%= @next_bill_amount %> from your preferred billing method. +<br /><br /> +There's no action required if you're happy to continue using Plausible to count your website stats in a privacy-friendly way. +<br /><br /> +If you don't want to continue your subscription, you can cancel it on your <%= link("account settings page", to: "#{plausible_url()}/billing/upgrade") %>. +<br /><br /> +Have a question, feedback or need some guidance? Just reply to this email to get in touch! +<br /><br /> +<br /><br /> +Thanks for being a Plausible Analytics subscriber!<br /> +<%= plausible_url() %><br /> diff --git a/lib/workers/notify_annual_renewal.ex b/lib/workers/notify_annual_renewal.ex new file mode 100644 index 00000000..25666a8b --- /dev/null +++ b/lib/workers/notify_annual_renewal.ex @@ -0,0 +1,50 @@ +defmodule Plausible.Workers.NotifyAnnualRenewal do + use Plausible.Repo + use Oban.Worker, queue: :notify_annual_renewal + + @yearly_plans Plausible.Billing.Plans.yearly_plan_ids() + + @impl Oban.Worker + @doc """ + Sends a notification at most 7 days and at least 1 day before the renewal of an annual subscription + """ + def perform(_args, _job) do + users = + Repo.all( + from u in Plausible.Auth.User, + left_join: sent in "sent_renewal_notifications", + join: s in Plausible.Billing.Subscription, + on: s.user_id == u.id, + where: s.paddle_plan_id in @yearly_plans, + where: + s.next_bill_date > fragment("now()::date") and + s.next_bill_date <= fragment("now()::date + INTERVAL '7 days'"), + where: is_nil(sent.id) or sent.timestamp < fragment("now() - INTERVAL '1 month'"), + preload: [subscription: s] + ) + + for user <- users do + case user.subscription.status do + "active" -> + template = PlausibleWeb.Email.yearly_renewal_notification(user) + Plausible.Mailer.send_email(template) + + "deleted" -> + template = PlausibleWeb.Email.yearly_expiration_notification(user) + Plausible.Mailer.send_email(template) + + _ -> + Sentry.capture_message("Invalid subscription for renewal", user: user) + end + + Repo.insert_all("sent_renewal_notifications", [ + %{ + user_id: user.id, + timestamp: NaiveDateTime.utc_now() + } + ]) + end + + :ok + end +end diff --git a/priv/repo/migrations/20210420075623_add_sent_renewal_notifications.exs b/priv/repo/migrations/20210420075623_add_sent_renewal_notifications.exs new file mode 100644 index 00000000..2174a1fa --- /dev/null +++ b/priv/repo/migrations/20210420075623_add_sent_renewal_notifications.exs @@ -0,0 +1,10 @@ +defmodule Plausible.Repo.Migrations.AddSentRenewalNotifications do + use Ecto.Migration + + def change do + create table(:sent_renewal_notifications) do + add :user_id, references(:users), null: false, on_delete: :delete_all + add :timestamp, :naive_datetime + end + end +end diff --git a/test/workers/notify_annual_renewal_test.exs b/test/workers/notify_annual_renewal_test.exs new file mode 100644 index 00000000..ebb5124d --- /dev/null +++ b/test/workers/notify_annual_renewal_test.exs @@ -0,0 +1,134 @@ +defmodule Plausible.Workers.NotifyAnnualRenewalTest do + use Plausible.DataCase + use Bamboo.Test + import Plausible.TestUtils + alias Plausible.Workers.NotifyAnnualRenewal + alias Plausible.Billing.Plans + + setup [:create_user, :create_site] + @monthly_plan Plans.plans()[:monthly][:"10k"][:product_id] + @yearly_plan Plans.plans()[:yearly][:"10k"][:product_id] + + test "ignores user without subscription" do + NotifyAnnualRenewal.perform(nil, nil) + + assert_no_emails_delivered() + end + + test "ignores user with monthly subscription", %{user: user} do + insert(:subscription, + user: user, + paddle_plan_id: @monthly_plan, + next_bill_date: Timex.shift(Timex.today(), days: 7) + ) + + NotifyAnnualRenewal.perform(nil, nil) + + assert_no_emails_delivered() + end + + test "ignores user with yearly subscription that's not due for renewal in 7 days", %{user: user} do + insert(:subscription, + user: user, + paddle_plan_id: @yearly_plan, + next_bill_date: Timex.shift(Timex.today(), days: 10) + ) + + NotifyAnnualRenewal.perform(nil, nil) + + assert_no_emails_delivered() + end + + test "sends renewal notification to user whose subscription is due for renewal in 7 days", %{ + user: user + } do + insert(:subscription, + user: user, + paddle_plan_id: @yearly_plan, + next_bill_date: Timex.shift(Timex.today(), days: 7) + ) + + NotifyAnnualRenewal.perform(nil, nil) + + assert_email_delivered_with( + to: [{user.name, user.email}], + subject: "Your Plausible subscription is up for renewal" + ) + end + + test "sends renewal notification to user whose subscription is due for renewal in 2 days", %{ + user: user + } do + insert(:subscription, + user: user, + paddle_plan_id: @yearly_plan, + next_bill_date: Timex.shift(Timex.today(), days: 2) + ) + + NotifyAnnualRenewal.perform(nil, nil) + + assert_email_delivered_with( + to: [{user.name, user.email}], + subject: "Your Plausible subscription is up for renewal" + ) + end + + test "does not send renewal notification multiple times", %{user: user} do + insert(:subscription, + user: user, + paddle_plan_id: @yearly_plan, + next_bill_date: Timex.shift(Timex.today(), days: 7) + ) + + NotifyAnnualRenewal.perform(nil, nil) + + assert_email_delivered_with( + to: [{user.name, user.email}], + subject: "Your Plausible subscription is up for renewal" + ) + + NotifyAnnualRenewal.perform(nil, nil) + + assert_no_emails_delivered() + end + + test "sends a renewal notification again a year after the previous one", %{user: user} do + insert(:subscription, + user: user, + paddle_plan_id: @yearly_plan, + next_bill_date: Timex.shift(Timex.today(), days: 7) + ) + + Repo.insert_all("sent_renewal_notifications", [ + %{ + user_id: user.id, + timestamp: Timex.shift(Timex.today(), years: -1) |> Timex.to_naive_datetime() + } + ]) + + NotifyAnnualRenewal.perform(nil, nil) + + assert_email_delivered_with( + to: [{user.name, user.email}], + subject: "Your Plausible subscription is up for renewal" + ) + end + + describe "expiration" do + test "if user subscription is 'deleted', notify them about expiration instead", %{user: user} do + insert(:subscription, + user: user, + paddle_plan_id: @yearly_plan, + next_bill_date: Timex.shift(Timex.today(), days: 7), + status: "deleted" + ) + + NotifyAnnualRenewal.perform(nil, nil) + + assert_email_delivered_with( + to: [{user.name, user.email}], + subject: "Your Plausible subscription is about to expire" + ) + end + end +end -- GitLab