Commit c2c0014e authored by Jay Swain's avatar Jay Swain

Expired subscriptions have a grace period

Provide users who have expired subscriptions a grace period
to renew their subscriptions before we begin downgrading them.

part of: https://gitlab.com/gitlab-org/growth/product/-/issues/1513
parent 42f861cc
...@@ -27,6 +27,8 @@ class SubscriptionPresenter < Gitlab::View::Presenter::Delegated ...@@ -27,6 +27,8 @@ class SubscriptionPresenter < Gitlab::View::Presenter::Delegated
def remaining_days def remaining_days
return unless end_date return unless end_date
return 0 if expired?
(end_date - Date.today).to_i (end_date - Date.today).to_i
end end
......
---
title: Introduce new messaging to include a 30 day grace period for subscription expiration
merge_request: 35897
author:
type: changed
...@@ -2,6 +2,9 @@ ...@@ -2,6 +2,9 @@
module Gitlab module Gitlab
class ExpiringSubscriptionMessage class ExpiringSubscriptionMessage
GRACE_PERIOD_EXTENSION_DAYS = 30.days
include Gitlab::Utils::StrongMemoize
include ActionView::Helpers::TextHelper include ActionView::Helpers::TextHelper
attr_reader :subscribable, :signed_in, :is_admin, :namespace attr_reader :subscribable, :signed_in, :is_admin, :namespace
...@@ -26,7 +29,7 @@ module Gitlab ...@@ -26,7 +29,7 @@ module Gitlab
private private
def license_message_subject def license_message_subject
message = subscribable.expired? ? expired_subject : expiring_subject message = expired_but_within_cutoff? ? expired_subject : expiring_subject
message = content_tag(:strong, message) message = content_tag(:strong, message)
...@@ -46,19 +49,17 @@ module Gitlab ...@@ -46,19 +49,17 @@ module Gitlab
end end
def expiring_subject def expiring_subject
remaining_days = pluralize(subscribable.remaining_days, 'day')
if auto_renew? if auto_renew?
_('Your subscription will automatically renew in %{remaining_days}.') % { remaining_days: remaining_days } _('Your subscription will automatically renew in %{remaining_days}.') % { remaining_days: remaining_days_formatted }
else else
_('Your subscription will expire in %{remaining_days}.') % { remaining_days: remaining_days } _('Your subscription will expire in %{remaining_days}.') % { remaining_days: remaining_days_formatted }
end end
end end
def expiration_blocking_message def expiration_blocking_message
return '' unless subscribable.will_block_changes? return '' unless subscribable.will_block_changes?
message = subscribable.expired? ? expired_message : expiring_message message = expired_but_within_cutoff? ? expired_message : expiring_message
content_tag(:p, message.html_safe) content_tag(:p, message.html_safe)
end end
...@@ -66,9 +67,7 @@ module Gitlab ...@@ -66,9 +67,7 @@ module Gitlab
def expired_message def expired_message
return block_changes_message if subscribable.block_changes? return block_changes_message if subscribable.block_changes?
remaining_days = pluralize((subscribable.block_changes_at - Date.today).to_i, 'day') _('No worries, you can still use all the %{strong}%{plan_name}%{strong_close} features for now. You have %{remaining_days} to renew your subscription.') % { plan_name: plan_name, remaining_days: remaining_days_formatted, strong: strong, strong_close: strong_close }
_('No worries, you can still use all the %{strong}%{plan_name}%{strong_close} features for now. You have %{remaining_days} to renew your subscription.') % { plan_name: plan_name, remaining_days: remaining_days, strong: strong, strong_close: strong_close }
end end
def block_changes_message def block_changes_message
...@@ -81,7 +80,7 @@ module Gitlab ...@@ -81,7 +80,7 @@ module Gitlab
if auto_renew? if auto_renew?
support_link = '<a href="mailto:support@gitlab.com">support@gitlab.com</a>'.html_safe support_link = '<a href="mailto:support@gitlab.com">support@gitlab.com</a>'.html_safe
_('We tried to automatically renew your %{strong}%{plan_name}%{strong_close} subscription for %{strong}%{namespace_name}%{strong_close} on %{expires_on} but something went wrong so your subscription was downgraded to the free plan. Don\'t worry, your data is safe. We suggest you check your payment method and get in touch with our support team (%{support_link}). They\'ll gladly help with your subscription renewal.') % { plan_name: plan_name, strong: strong, strong_close: strong_close, namespace_name: namespace.name, support_link: support_link, expires_on: subscribable.expires_at.strftime("%Y-%m-%d") } _('We tried to automatically renew your %{strong}%{plan_name}%{strong_close} subscription for %{strong}%{namespace_name}%{strong_close} on %{expires_on} but something went wrong so your subscription was downgraded to the free plan. Don\'t worry, your data is safe. We suggest you check your payment method and get in touch with our support team (%{support_link}). They\'ll gladly help with your subscription renewal.') % { plan_name: plan_name, strong: strong, strong_close: strong_close, namespace_name: namespace.name, support_link: support_link, expires_on: expires_at_or_cutoff_at.strftime("%Y-%m-%d") }
else else
_('You didn\'t renew your %{strong}%{plan_name}%{strong_close} subscription for %{strong}%{namespace_name}%{strong_close} so it was downgraded to the free plan.') % { plan_name: plan_name, strong: strong, strong_close: strong_close, namespace_name: namespace.name } _('You didn\'t renew your %{strong}%{plan_name}%{strong_close} subscription for %{strong}%{namespace_name}%{strong_close} so it was downgraded to the free plan.') % { plan_name: plan_name, strong: strong, strong_close: strong_close, namespace_name: namespace.name }
end end
...@@ -90,16 +89,16 @@ module Gitlab ...@@ -90,16 +89,16 @@ module Gitlab
def expiring_message def expiring_message
return namespace_expiring_message if namespace return namespace_expiring_message if namespace
_('Your %{strong}%{plan_name}%{strong_close} subscription will expire on %{strong}%{expires_on}%{strong_close}. After that, you will not to be able to create issues or merge requests as well as many other features.') % { expires_on: subscribable.expires_at.strftime("%Y-%m-%d"), plan_name: plan_name, strong: strong, strong_close: strong_close } _('Your %{strong}%{plan_name}%{strong_close} subscription will expire on %{strong}%{expires_on}%{strong_close}. After that, you will not to be able to create issues or merge requests as well as many other features.') % { expires_on: expires_at_or_cutoff_at.strftime("%Y-%m-%d"), plan_name: plan_name, strong: strong, strong_close: strong_close }
end end
def namespace_expiring_message def namespace_expiring_message
if auto_renew? if auto_renew?
_('We will automatically renew your %{strong}%{plan_name}%{strong_close} subscription for %{strong}%{namespace_name}%{strong_close} on %{strong}%{expires_on}%{strong_close}. There\'s nothing that you need to do, we\'ll let you know when the renewal is complete. Need more seats, a higher plan or just want to review your payment method?') % { expires_on: subscribable.expires_at.strftime("%Y-%m-%d"), plan_name: plan_name, strong: strong, strong_close: strong_close, namespace_name: namespace.name } _('We will automatically renew your %{strong}%{plan_name}%{strong_close} subscription for %{strong}%{namespace_name}%{strong_close} on %{strong}%{expires_on}%{strong_close}. There\'s nothing that you need to do, we\'ll let you know when the renewal is complete. Need more seats, a higher plan or just want to review your payment method?') % { expires_on: expires_at_or_cutoff_at.strftime("%Y-%m-%d"), plan_name: plan_name, strong: strong, strong_close: strong_close, namespace_name: namespace.name }
else else
message = [] message = []
message << _('Your %{strong}%{plan_name}%{strong_close} subscription for %{strong}%{namespace_name}%{strong_close} will expire on %{strong}%{expires_on}%{strong_close}.') % { expires_on: subscribable.expires_at.strftime("%Y-%m-%d"), plan_name: plan_name, strong: strong, strong_close: strong_close, namespace_name: namespace.name } message << _('Your %{strong}%{plan_name}%{strong_close} subscription for %{strong}%{namespace_name}%{strong_close} will expire on %{strong}%{expires_on}%{strong_close}.') % { expires_on: expires_at_or_cutoff_at.strftime("%Y-%m-%d"), plan_name: plan_name, strong: strong, strong_close: strong_close, namespace_name: namespace.name }
message << expiring_features_message message << expiring_features_message
...@@ -135,10 +134,9 @@ module Gitlab ...@@ -135,10 +134,9 @@ module Gitlab
end end
def expired_subscribable_within_notification_window? def expired_subscribable_within_notification_window?
return true unless subscribable.expired? return true unless expired_but_within_cutoff?
expired_at = subscribable.expires_at (expires_at_or_cutoff_at + GRACE_PERIOD_EXTENSION_DAYS) > Date.today
(expired_at..(expired_at + 30.days)).cover?(Date.today)
end end
def plan_name def plan_name
...@@ -156,5 +154,44 @@ module Gitlab ...@@ -156,5 +154,44 @@ module Gitlab
def auto_renew? def auto_renew?
subscribable.auto_renew? subscribable.auto_renew?
end end
def grace_period_effective_from
Date.parse('2020-07-22')
end
def self_managed?
subscribable.is_a?(::License)
end
def expires_at_or_cutoff_at
strong_memoize(:expires_at_or_cutoff_at) do
# self-managed licenses are unconcerned of our announcement.
if self_managed?
subscribable.expires_at
else
cutoff_at = grace_period_effective_from + GRACE_PERIOD_EXTENSION_DAYS
[subscribable.expires_at, cutoff_at].max
end
end
end
def expired_but_within_cutoff?
strong_memoize(:expired) do
subscribable.expired? && expires_at_or_cutoff_at < Date.today
end
end
def remaining_days_formatted
strong_memoize(:remaining_days_formatted) do
days = if expired_but_within_cutoff?
(subscribable.block_changes_at - Date.today).to_i
else
(expires_at_or_cutoff_at - Date.today).to_i
end
pluralize(days, 'day')
end
end
end end
end end
...@@ -8,6 +8,7 @@ RSpec.describe "Admin views license" do ...@@ -8,6 +8,7 @@ RSpec.describe "Admin views license" do
before do before do
stub_feature_flags(licenses_app: false) stub_feature_flags(licenses_app: false)
sign_in(admin) sign_in(admin)
allow_any_instance_of(Gitlab::ExpiringSubscriptionMessage).to receive(:grace_period_effective_from).and_return(Date.today - 45.days)
end end
context "when license is valid" do context "when license is valid" do
......
...@@ -18,10 +18,15 @@ RSpec.describe Gitlab::ExpiringSubscriptionMessage do ...@@ -18,10 +18,15 @@ RSpec.describe Gitlab::ExpiringSubscriptionMessage do
namespace: namespace namespace: namespace
).message ).message
end end
let(:grace_period_effective_from) { expired_date - 35.days }
let(:today) { Time.utc(2020, 3, 7, 10) }
let(:expired_date) { Time.utc(2020, 3, 9, 10).to_date }
before do
allow_any_instance_of(Gitlab::ExpiringSubscriptionMessage).to receive(:grace_period_effective_from).and_return(grace_period_effective_from)
end
context 'subscribable installed' do context 'subscribable installed' do
let(:expired_date) { Time.utc(2020, 3, 9, 10) }
let(:today) { Time.utc(2020, 3, 7, 10) }
let(:auto_renew) { false } let(:auto_renew) { false }
before do before do
...@@ -123,6 +128,7 @@ RSpec.describe Gitlab::ExpiringSubscriptionMessage do ...@@ -123,6 +128,7 @@ RSpec.describe Gitlab::ExpiringSubscriptionMessage do
it 'has an expiration blocking message' do it 'has an expiration blocking message' do
allow(subscribable).to receive(:block_changes_at).and_return(Time.utc(2020, 3, 9, 10).to_date) allow(subscribable).to receive(:block_changes_at).and_return(Time.utc(2020, 3, 9, 10).to_date)
allow(subscribable).to receive(:is_a?).with(::License).and_return(true)
Timecop.freeze(today) do Timecop.freeze(today) do
expect(subject).to include('No worries, you can still use all the Ultimate features for now. You have 2 days to renew your subscription.') expect(subject).to include('No worries, you can still use all the Ultimate features for now. You have 2 days to renew your subscription.')
...@@ -135,14 +141,13 @@ RSpec.describe Gitlab::ExpiringSubscriptionMessage do ...@@ -135,14 +141,13 @@ RSpec.describe Gitlab::ExpiringSubscriptionMessage do
context 'subscribable is expiring soon' do context 'subscribable is expiring soon' do
before do before do
allow(subscribable).to receive(:expired?).and_return(false) allow(subscribable).to receive(:expired?).and_return(false)
allow(subscribable).to receive(:remaining_days).and_return(4)
allow(subscribable).to receive(:will_block_changes?).and_return(true) allow(subscribable).to receive(:will_block_changes?).and_return(true)
allow(subscribable).to receive(:block_changes_at).and_return(expired_date) allow(subscribable).to receive(:block_changes_at).and_return(expired_date)
end end
it 'has a nice subject' do it 'has a nice subject' do
Timecop.freeze(today) do Timecop.freeze(today) do
expect(subject).to include('Your subscription will expire in 4 days') expect(subject).to include('Your subscription will expire in 2 days')
end end
end end
...@@ -193,7 +198,9 @@ RSpec.describe Gitlab::ExpiringSubscriptionMessage do ...@@ -193,7 +198,9 @@ RSpec.describe Gitlab::ExpiringSubscriptionMessage do
let(:auto_renew) { true } let(:auto_renew) { true }
it 'has a nice subject' do it 'has a nice subject' do
expect(subject).to include('Your subscription will automatically renew in 4 days.') Timecop.freeze(today) do
expect(subject).to include('Your subscription will automatically renew in 2 days.')
end
end end
it 'has an expiration blocking message' do it 'has an expiration blocking message' do
...@@ -204,6 +211,37 @@ RSpec.describe Gitlab::ExpiringSubscriptionMessage do ...@@ -204,6 +211,37 @@ RSpec.describe Gitlab::ExpiringSubscriptionMessage do
end end
end end
end end
context 'subscribable expired a long time ago' do
let(:expired_date) { today.to_date - 1.year }
let(:grace_period_effective_from) { today.to_date - 25.days }
before do
allow(subscribable).to receive(:expires_at).and_return(expired_date)
allow(subscribable).to receive(:block_changes_at).and_return(expired_date)
allow(subscribable).to receive(:expired?).and_return(true)
allow(subscribable).to receive(:will_block_changes?).and_return(true)
allow(subscribable).to receive(:block_changes?).and_return(true)
end
context 'and is past the cutoff date' do
let(:grace_period_effective_from) { today.to_date - 40.days }
it 'has a nice subject' do
Timecop.freeze(today) do
expect(subject).to include('Your subscription has been downgraded')
end
end
end
context 'and not past the cutoff date' do
it 'has a nice subject' do
Timecop.freeze(today) do
expect(subject).to include('Your subscription will expire in 5 days')
end
end
end
end
end end
end end
end end
......
...@@ -88,5 +88,13 @@ RSpec.describe SubscriptionPresenter do ...@@ -88,5 +88,13 @@ RSpec.describe SubscriptionPresenter do
expect(subject).to eq(2) expect(subject).to eq(2)
end end
end end
it 'is 0 if expired' do
allow(subscription).to receive(:end_date).and_return(Time.utc(2020, 3, 1, 10).to_date)
Timecop.freeze(today) do
expect(subject).to eq(0)
end
end
end end
end end
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment