From 0c6d62df34eb817afb9d68feafc4f0e2aa3aa109 Mon Sep 17 00:00:00 2001 From: Andrei Stoicescu <astoicescu@gitlab.com> Date: Mon, 25 Jan 2021 23:20:31 +0000 Subject: [PATCH] Add graphQL connection Add service to get plan upgrade info Add specs for new helper methods --- ee/app/assets/stylesheets/pages/billings.scss | 115 +++++++++++++++++- ee/app/helpers/billing_plans_helper.rb | 49 +++++++- .../plan_upgrade_service.rb | 26 ++++ .../shared/billings/_billing_plan.html.haml | 105 ++++++++++------ .../billings/_billing_plan_header.html.haml | 2 +- .../shared/billings/_billing_plans.html.haml | 6 +- ee/lib/gitlab/subscription_portal/client.rb | 29 +++++ .../features/billings/billing_plans_spec.rb | 6 +- ee/spec/features/groups/billing_spec.rb | 2 + ee/spec/helpers/billing_plans_helper_spec.rb | 110 ++++++++++++++--- .../gitlab/subscription_portal/client_spec.rb | 96 +++++++++++++++ .../fetch_subscription_plans_service_spec.rb | 2 +- .../plan_upgrade_service_spec.rb | 63 ++++++++++ .../helpers/subscription_portal_helpers.rb | 37 ++++++ locale/gitlab.pot | 18 ++- 15 files changed, 592 insertions(+), 74 deletions(-) create mode 100644 ee/app/services/gitlab_subscriptions/plan_upgrade_service.rb create mode 100644 ee/spec/services/gitlab_subscriptions/plan_upgrade_service_spec.rb create mode 100644 ee/spec/support/helpers/subscription_portal_helpers.rb diff --git a/ee/app/assets/stylesheets/pages/billings.scss b/ee/app/assets/stylesheets/pages/billings.scss index bf784cee1d44..0a3a72f7776d 100644 --- a/ee/app/assets/stylesheets/pages/billings.scss +++ b/ee/app/assets/stylesheets/pages/billings.scss @@ -19,21 +19,81 @@ } } +$gutter-small: $gl-spacing-scale-6; +$gutter: $gl-spacing-scale-7; +$badge-height: $gl-spacing-scale-7; + .billing-plans { + // This color is not part of the GitLab-UI/Pajamas specifications. + // We're using it only for marketing purposes + $highlight-color: #6e49cb; + + margin-bottom: $gutter-small; + + > * + * { + margin-top: $gutter-small; + } + + .card-wrapper-has-badge { + .card { + @include gl-border-1; + @include gl-border-solid; + @include gl-rounded-top-left-none; + @include gl-rounded-top-right-none; + + border-color: $highlight-color; + } + } + + .card-badge { + @include gl-rounded-top-left-base; + @include gl-rounded-top-right-base; + @include gl-font-weight-bold; + @include gl-px-5; + @include gl-text-white; + + background-color: $highlight-color; + + // These border radii values are not defined in gitlab-ui, + // but they are consistent with the startup-*.scss .card overrides + border-top-left-radius: 0.25rem; + border-top-right-radius: 0.25rem; + + line-height: $badge-height; + + &-text { + @include gl-display-block; + @include gl-text-truncate; + } + } + .card { + @include gl-mb-0; + &-active { background-color: $gray-light; } .card-body { - .price-per-month { + .price-description { + align-items: center; display: flex; flex-direction: row; color: $blue-500; - font-size: 48px; + font-size: 45px; font-weight: $gl-font-weight-bold; line-height: 1; + .price-rebate { + color: $blue-400; + font-size: 20px; + text-decoration: line-through; + } + + .price-cut { + text-decoration: line-through; + } + .conditions { list-style: none; font-size: $gl-font-size-large; @@ -42,15 +102,62 @@ } } - .price-per-year { + .price-conclusion { + @include gl-font-base; color: $blue-500; - font-size: $gl-font-size-small; font-weight: $gl-font-weight-bold; } } } } +@media (min-width: $breakpoint-md) { + .billing-plans { + @include gl-display-flex; + @include gl-flex-wrap; + @include gl-justify-content-space-between; + + > * + * { + @include gl-mt-0; + } + + .card-wrapper { + margin-bottom: $gutter-small; + padding-top: $badge-height; + width: calc(50% - #{$gutter-small} / 2); + + &-has-badge { + @include gl-pt-0; + + .card { + height: calc(100% - #{$badge-height}); + } + } + } + + .card { + @include gl-h-full; + } + } +} + +@media (min-width: $breakpoint-lg) { + .billing-plans { + flex-wrap: nowrap; + + > * + * { + margin-left: $gutter; + } + + .card-wrapper { + @include gl-flex-fill-1; + @include gl-mb-0; + @include gl-overflow-hidden; + @include gl-w-auto; + } + } +} + .subscription-table { .flex-grid { .grid-cell { diff --git a/ee/app/helpers/billing_plans_helper.rb b/ee/app/helpers/billing_plans_helper.rb index b879dc61a4fd..365908e6a20c 100644 --- a/ee/app/helpers/billing_plans_helper.rb +++ b/ee/app/helpers/billing_plans_helper.rb @@ -1,6 +1,8 @@ # frozen_string_literal: true module BillingPlansHelper + include Gitlab::Utils::StrongMemoize + def subscription_plan_info(plans_data, current_plan_code) current_plan = plans_data.find { |plan| plan.code == current_plan_code && plan.current_subscription_plan? } current_plan || plans_data.find { |plan| plan.code == current_plan_code } @@ -10,6 +12,28 @@ def number_to_plan_currency(value) number_to_currency(value, unit: '$', strip_insignificant_zeros: true, format: "%u%n") end + def upgrade_offer_type(namespace, plan) + return :no_offer if namespace.actual_plan_name != Plan::BRONZE || !offer_from_previous_tier?(namespace.id, plan.id) + + upgrade_for_free?(namespace.id) ? :upgrade_for_free : :upgrade_for_offer + end + + def has_upgrade?(upgrade_offer) + upgrade_offer == :upgrade_for_free || upgrade_offer == :upgrade_for_offer + end + + def show_contact_sales_button?(purchase_link_action, upgrade_offer) + return false unless purchase_link_action == 'upgrade' + + upgrade_offer == :upgrade_for_offer || + (experiment_enabled?(:contact_sales_btn_in_app) && upgrade_offer == :no_offer) + end + + def show_upgrade_button?(purchase_link_action, upgrade_offer) + purchase_link_action == 'upgrade' && + (upgrade_offer == :no_offer || upgrade_offer == :upgrade_for_free) + end + def subscription_plan_data_attributes(namespace, plan) return {} unless namespace @@ -34,11 +58,6 @@ def use_new_purchase_flow?(namespace) namespace.group? && (namespace.actual_plan_name == Plan::FREE || namespace.trial_active?) end - def show_contact_sales_button?(purchase_link_action) - experiment_enabled?(:contact_sales_btn_in_app) && - purchase_link_action == 'upgrade' - end - def experiment_tracking_data_for_button_click(button_label) return {} unless Gitlab::Experimentation.active?(:contact_sales_btn_in_app) @@ -139,4 +158,24 @@ def plan_renew_url(group) def billable_seats_href(group) group_seat_usage_path(group) end + + def offer_from_previous_tier?(namespace_id, plan_id) + upgrade_plan_id = upgrade_plan_data(namespace_id)[:upgrade_plan_id] + + return false unless upgrade_plan_id + + upgrade_plan_id == plan_id + end + + def upgrade_for_free?(namespace_id) + !!upgrade_plan_data(namespace_id)[:upgrade_for_free] + end + + def upgrade_plan_data(namespace_id) + strong_memoize(:upgrade_plan_data) do + GitlabSubscriptions::PlanUpgradeService + .new(namespace_id: namespace_id) + .execute + end + end end diff --git a/ee/app/services/gitlab_subscriptions/plan_upgrade_service.rb b/ee/app/services/gitlab_subscriptions/plan_upgrade_service.rb new file mode 100644 index 000000000000..9a8fde941515 --- /dev/null +++ b/ee/app/services/gitlab_subscriptions/plan_upgrade_service.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +module GitlabSubscriptions + class PlanUpgradeService + def initialize(namespace_id:) + @namespace_id = namespace_id + end + + def execute + result = client.plan_upgrade_offer(@namespace_id) + + plan_id = result[:assisted_upgrade_plan_id] || result[:free_upgrade_plan_id] unless result[:eligible_for_free_upgrade].nil? + + { + upgrade_for_free: result[:eligible_for_free_upgrade], + upgrade_plan_id: plan_id + } + end + + private + + def client + Gitlab::SubscriptionPortal::Client + end + end +end diff --git a/ee/app/views/shared/billings/_billing_plan.html.haml b/ee/app/views/shared/billings/_billing_plan.html.haml index 0f6bb10323fc..1ef842eac907 100644 --- a/ee/app/views/shared/billings/_billing_plan.html.haml +++ b/ee/app/views/shared/billings/_billing_plan.html.haml @@ -1,52 +1,79 @@ - purchase_link = plan.purchase_link - plan_name = plan.name - show_deprecated_plan = ::Feature.enabled?(:hide_deprecated_billing_plans) && plan.deprecated? +- has_upgrade = has_upgrade?(plan_offer_type) - if show_deprecated_plan - plan_name += ' (Legacy)' - faq_link_url = 'https://about.gitlab.com/gitlab-com/#faq' - faq_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: faq_link_url } -.card.h-100{ class: ("card-active" if is_current || show_deprecated_plan) } - .card-header.gl-font-weight-bold.d-flex.flex-row.justify-content-between.flex-wrap - %div - = plan_name - - if is_current - .text-muted - = _("Current Plan") +.card-wrapper{ class: ("card-wrapper-has-badge" if has_upgrade) } + - if has_upgrade + .card-badge + %span.card-badge-text + - case plan_offer_type + - when :upgrade_for_free + = s_("BillingPlans|Free upgrade!") + - else + = _("Upgrade offers available!") + .card{ class: ("card-active" if is_current || show_deprecated_plan) } + .card-header.gl-font-weight-bold.d-flex.flex-row.justify-content-between.flex-wrap + %div + = plan_name + - if is_current + .text-muted + = s_("BillingPlans|Current Plan") - .card-body - - if show_deprecated_plan - = s_("The %{plan_name} is no longer available to purchase. For more information about how this will impact you, check our %{faq_link_start}frequently asked questions%{faq_link_end}.").html_safe % { plan_name: plan.name, faq_link_start: faq_link_start, faq_link_end: '</a>'.html_safe } - - else - .price-per-month - .gl-mr-2 - = number_to_plan_currency(plan.price_per_month) + .card-body + - if show_deprecated_plan + = s_("The %{plan_name} is no longer available to purchase. For more information about how this will impact you, check our %{faq_link_start}frequently asked questions%{faq_link_end}.").html_safe % { plan_name: plan.name, faq_link_start: faq_link_start, faq_link_end: '</a>'.html_safe } + - else + .price-description + .gl-mr-2.gl-display-flex.gl-align-items-center + - case plan_offer_type + - when :upgrade_for_free + %span.gl-mr-3.price-rebate + = number_to_plan_currency(plan.price_per_month) + %span + = number_to_plan_currency(plan.upgrade_price_per_month) + - when :upgrade_for_offer + %span.price-cut + = number_to_plan_currency(plan.price_per_month) + - else + %span + = number_to_plan_currency(plan.price_per_month) - %ul.conditions.gl-p-0.gl-my-auto - %li= s_("BillingPlans|per user") - %li= s_("BillingPlans|monthly") - .price-per-year.text-left{ class: ("invisible" unless plan.price_per_year > 0) } - - price_per_year = number_to_plan_currency(plan.price_per_year) - = s_("BillingPlans|billed annually at %{price_per_year}") % { price_per_year: price_per_year } + %ul.conditions.gl-p-0.gl-my-auto + %li= s_("BillingPlans|per user") + %li= s_("BillingPlans|monthly") + .price-conclusion{ class: ("invisible" unless plan.price_per_year > 0) } + - case plan_offer_type + - when :upgrade_for_free + = s_("BillingPlans|for the remainder of your subscription") + - else + - price_per_year = number_to_plan_currency(plan.price_per_year) + = s_("BillingPlans|billed annually at %{price_per_year}") % { price_per_year: price_per_year } + %hr.gl-my-3 - %hr.gl-my-3 + %ul.unstyled-list + - plan_feature_list(plan).each do |feature| + - feature_class = "gl-p-0!" + - if feature.highlight + - feature_class += " gl-font-weight-bold" + %li{ class: "#{feature_class}" } + = feature.title + %li.gl-p-0.gl-pt-3 + - if plan.about_page_href + = link_to s_("BillingPlans|See all %{plan_name} features") % { plan_name: plan.name }, EE::SUBSCRIPTIONS_COMPARISON_URL - %ul.unstyled-list - - plan_feature_list(plan).each do |feature| - - feature_class = "gl-p-0!" - - if feature.highlight - - feature_class += " gl-font-weight-bold" - %li{ class: "#{feature_class}" } - = feature.title - %li.gl-p-0.gl-pt-3 - - if plan.about_page_href - = link_to s_("BillingPlans|See all %{plan_name} features") % { plan_name: plan.name }, EE::SUBSCRIPTIONS_COMPARISON_URL - - - if purchase_link .card-footer - .float-right{ class: ("invisible" unless purchase_link.action == 'upgrade' || is_current) } - - if show_contact_sales_button?(purchase_link.action) - = link_to s_('BillingPlan|Contact sales'), "#{contact_sales_url}?test=inappcontactsales#{plan.code}", class: "btn btn-success-secondary gl-button", data: { **experiment_tracking_data_for_button_click('contact_sales') } - - cta_class = '-new' if use_new_purchase_flow?(namespace) - - upgrade_button_classes = upgrade_button_css_classes(namespace, plan, is_current) - = link_to s_('BillingPlan|Upgrade'), plan_purchase_or_upgrade_url(namespace, plan), class: "#{upgrade_button_classes} billing-cta-purchase#{cta_class}", data: { **experiment_tracking_data_for_button_click('upgrade') } + - cta_class = '-new' if use_new_purchase_flow?(namespace) + - upgrade_button_classes = upgrade_button_css_classes(namespace, plan, is_current) + - upgrade_button_text = plan_offer_type === :upgrade_for_free ? s_('BillingPlan|Upgrade for free') : s_('BillingPlan|Upgrade') + - show_upgrade_button = show_upgrade_button?(purchase_link.action, plan_offer_type) + - show_contact_sales_button = show_contact_sales_button?(purchase_link.action, plan_offer_type) + .gl-min-h-7.gl-display-flex.gl-flex-wrap.gl-justify-content-end + - if show_contact_sales_button + = link_to s_('BillingPlan|Contact sales'), "#{contact_sales_url}?test=inappcontactsales#{plan.code}", class: [("btn gl-button"), (show_upgrade_button ? "btn-success-secondary" : "btn-success")], data: { **experiment_tracking_data_for_button_click('contact_sales') } + - if show_upgrade_button + = link_to upgrade_button_text, plan_purchase_or_upgrade_url(namespace, plan), class: "#{upgrade_button_classes} billing-cta-purchase#{cta_class} gl-ml-3", data: { **experiment_tracking_data_for_button_click('upgrade') } diff --git a/ee/app/views/shared/billings/_billing_plan_header.html.haml b/ee/app/views/shared/billings/_billing_plan_header.html.haml index c1df7a44b06a..c610a5ff1d8f 100644 --- a/ee/app/views/shared/billings/_billing_plan_header.html.haml +++ b/ee/app/views/shared/billings/_billing_plan_header.html.haml @@ -3,7 +3,7 @@ - if namespace_for_user = render_if_exists 'trials/banner', namespace: namespace -.billing-plan-header.content-block.center +.billing-plan-header.content-block.center.gl-mb-5 .billing-plan-logo - if namespace_for_user .avatar-container.s96.home-panel-avatar.gl-mr-3.float-none.mx-auto.mb-4.mt-1 diff --git a/ee/app/views/shared/billings/_billing_plans.html.haml b/ee/app/views/shared/billings/_billing_plans.html.haml index 4dbf3c712694..1bcac338f91f 100644 --- a/ee/app/views/shared/billings/_billing_plans.html.haml +++ b/ee/app/views/shared/billings/_billing_plans.html.haml @@ -7,12 +7,12 @@ - if show_plans?(namespace) - plans = billing_available_plans(plans_data, current_plan) - .billing-plans.gl-mt-5.row.justify-content-center + .billing-plans - plans.each do |plan| - is_default_plan = current_plan.nil? && plan.default? - is_current = plan.code == current_plan&.code || is_default_plan - .col-md-6.col-lg-3.gl-mb-5 - = render 'shared/billings/billing_plan', namespace: namespace, plan: plan, is_current: is_current + = render 'shared/billings/billing_plan', namespace: namespace, plan: plan, is_current: is_current, + plan_offer_type: upgrade_offer_type(namespace, plan) - if namespace.gitlab_subscription&.has_a_paid_hosted_plan? .center.gl-mb-7 diff --git a/ee/lib/gitlab/subscription_portal/client.rb b/ee/lib/gitlab/subscription_portal/client.rb index 4f358a936c9d..89f936ddd8b6 100644 --- a/ee/lib/gitlab/subscription_portal/client.rb +++ b/ee/lib/gitlab/subscription_portal/client.rb @@ -45,6 +45,35 @@ def activate(activation_code) end end + def plan_upgrade_offer(namespace_id) + query = <<~GQL + { + subscription(namespaceId: "#{namespace_id}") { + eoaStarterBronzeEligible + assistedUpgradePlanId + freeUpgradePlanId + } + } + GQL + + response = http_post("graphql", admin_headers, { query: query }).dig(:data) + + if response['errors'].blank? + eligible = response.dig('data', 'subscription', 'eoaStarterBronzeEligible') + assisted_upgrade = response.dig('data', 'subscription', 'assistedUpgradePlanId') + free_upgrade = response.dig('data', 'subscription', 'freeUpgradePlanId') + + { + success: true, + eligible_for_free_upgrade: eligible, + assisted_upgrade_plan_id: assisted_upgrade, + free_upgrade_plan_id: free_upgrade + } + else + { success: false } + end + end + private def http_get(path, headers) diff --git a/ee/spec/features/billings/billing_plans_spec.rb b/ee/spec/features/billings/billing_plans_spec.rb index 01d522ff91e7..53bfcafa1f9f 100644 --- a/ee/spec/features/billings/billing_plans_spec.rb +++ b/ee/spec/features/billings/billing_plans_spec.rb @@ -4,6 +4,7 @@ RSpec.describe 'Billing plan pages', :feature do include StubRequests + include SubscriptionPortalHelpers let(:user) { create(:user) } let(:namespace) { user.namespace } @@ -22,6 +23,7 @@ stub_experiment_for_subject(contact_sales_btn_in_app: true) stub_full_request("#{EE::SUBSCRIPTIONS_URL}/gitlab_plans?plan=#{plan.name}&namespace_id=#{namespace.id}") .to_return(status: 200, body: plans_data.to_json) + stub_eoa_eligibility_request(namespace.id) stub_application_setting(check_namespace_plan: true) allow(Gitlab).to receive(:com?) { true } gitlab_sign_in(user) @@ -159,8 +161,7 @@ def external_upgrade_url(namespace, plan) expect(action).not_to have_link('Upgrade') expect(action).not_to have_css('.disabled') when 'current_plan' - expect(action).to have_link('Upgrade') - expect(action).to have_css('.disabled') + expect(action).not_to have_link('Upgrade') when 'upgrade' expect(action).to have_link('Upgrade') expect(action).not_to have_css('.disabled') @@ -254,7 +255,6 @@ def external_upgrade_url(namespace, plan) expect(action).not_to have_link('Upgrade') expect(action).not_to have_css('.disabled') when 'current_plan' - expect(action).to have_link('Upgrade') expect(action).not_to have_css('.disabled') when 'upgrade' expect(action).to have_link('Upgrade') diff --git a/ee/spec/features/groups/billing_spec.rb b/ee/spec/features/groups/billing_spec.rb index bef142e59399..226603e167cc 100644 --- a/ee/spec/features/groups/billing_spec.rb +++ b/ee/spec/features/groups/billing_spec.rb @@ -4,6 +4,7 @@ RSpec.describe 'Groups > Billing', :js do include StubRequests + include SubscriptionPortalHelpers let_it_be(:user) { create(:user) } let_it_be(:group) { create(:group) } @@ -18,6 +19,7 @@ def subscription_table end before do + stub_eoa_eligibility_request(group.id) stub_full_request("#{EE::SUBSCRIPTIONS_URL}/gitlab_plans?plan=#{plan}&namespace_id=#{group.id}") .with(headers: { 'Accept' => 'application/json' }) .to_return(status: 200, body: File.new(Rails.root.join('ee/spec/fixtures/gitlab_com_plans.json'))) diff --git a/ee/spec/helpers/billing_plans_helper_spec.rb b/ee/spec/helpers/billing_plans_helper_spec.rb index 4e71fe280412..931510dd9930 100644 --- a/ee/spec/helpers/billing_plans_helper_spec.rb +++ b/ee/spec/helpers/billing_plans_helper_spec.rb @@ -8,7 +8,7 @@ let(:group) { build(:group) } let(:plan) do - Hashie::Mash.new(id: 'external-paid-plan-hash-code', name: 'Bronze Plan') + OpenStruct.new(id: 'external-paid-plan-hash-code', name: 'Bronze Plan') end context 'when group and plan with ID present' do @@ -61,7 +61,7 @@ end context 'when plan with ID not present' do - let(:plan) { Hashie::Mash.new(id: nil, name: 'Bronze Plan') } + let(:plan) { OpenStruct.new(id: nil, name: 'Bronze Plan') } it 'returns data attributes without upgrade href' do add_seats_href = "#{EE::SUBSCRIPTIONS_URL}/gitlab/namespaces/#{group.id}/extra_seats" @@ -149,16 +149,77 @@ end end + describe '#upgrade_offer_type' do + using RSpec::Parameterized::TableSyntax + + let(:plan) { OpenStruct.new({ id: '123456789' }) } + + context 'when plan has a valid property' do + where(:plan_name, :for_free, :plan_id, :result) do + Plan::BRONZE | true | '123456789' | :upgrade_for_free + Plan::BRONZE | true | '987654321' | :no_offer + Plan::BRONZE | true | nil | :no_offer + Plan::BRONZE | false | '123456789' | :upgrade_for_offer + Plan::BRONZE | false | nil | :no_offer + Plan::BRONZE | nil | nil | :no_offer + Plan::SILVER | nil | nil | :no_offer + nil | true | nil | :no_offer + end + + with_them do + let(:namespace) do + OpenStruct.new( + { + actual_plan_name: plan_name, + id: '000000000' + } + ) + end + + before do + allow_next_instance_of(GitlabSubscriptions::PlanUpgradeService) do |instance| + expect(instance).to receive(:execute).once.and_return({ + upgrade_for_free: for_free, + upgrade_plan_id: plan_id + }) + end + end + + subject { helper.upgrade_offer_type(namespace, plan) } + + it { is_expected.to eq(result) } + end + end + end + + describe '#has_upgrade?' do + using RSpec::Parameterized::TableSyntax + + where(:offer_type, :result) do + :no_offer | false + :upgrade_for_free | true + :upgrade_for_offer | true + end + + with_them do + subject { helper.has_upgrade?(offer_type) } + + it { is_expected.to eq(result) } + end + end + describe '#show_contact_sales_button?' do using RSpec::Parameterized::TableSyntax - where(:experiment_enabled, :link_action, :result) do - true | 'downgrade' | false - true | 'current' | false - true | 'upgrade' | true - false | 'downgrade' | false - false | 'current' | false - false | 'upgrade' | false + where(:experiment_enabled, :link_action, :upgrade_offer, :result) do + true | 'upgrade' | :no_offer | true + true | 'upgrade' | :upgrade_for_offer | true + true | 'no_upgrade' | :no_offer | false + true | 'no_upgrade' | :upgrade_for_offer | false + false | 'upgrade' | :no_offer | false + false | 'upgrade' | :upgrade_for_offer | true + false | 'no_upgrade' | :no_offer | false + false | 'no_upgrade' | :upgrade_for_offer | false end with_them do @@ -166,7 +227,26 @@ allow(helper).to receive(:experiment_enabled?).with(:contact_sales_btn_in_app).and_return(experiment_enabled) end - subject { helper.show_contact_sales_button?(link_action) } + subject { helper.show_contact_sales_button?(link_action, upgrade_offer) } + + it { is_expected.to eq(result) } + end + end + + describe '#show_upgrade_button?' do + using RSpec::Parameterized::TableSyntax + + where(:link_action, :upgrade_offer, :result) do + 'upgrade' | :no_offer | true + 'upgrade' | :upgrade_for_free | true + 'upgrade' | :upgrade_for_offer | false + 'no_upgrade' | :no_offer | false + 'no_upgrade' | :upgrade_for_free | false + 'no_upgrade' | :upgrade_for_offer | false + end + + with_them do + subject { helper.show_upgrade_button?(link_action, upgrade_offer) } it { is_expected.to eq(result) } end @@ -277,7 +357,7 @@ end with_them do - let(:namespace) { Hashie::Mash.new(trial_active: trial_active) } + let(:namespace) { OpenStruct.new(trial_active: trial_active) } subject { helper.upgrade_button_css_classes(namespace, plan, is_current_plan) } @@ -305,7 +385,7 @@ end context 'when namespace is on an active plan' do - let(:current_plan) { Hashie::Mash.new(code: 'silver') } + let(:current_plan) { OpenStruct.new(code: 'silver') } it 'returns plans without deprecated' do expect(helper.billing_available_plans(plans_data, nil)).to eq([plan]) @@ -313,7 +393,7 @@ end context 'when namespace is on a deprecated plan' do - let(:current_plan) { Hashie::Mash.new(code: 'bronze') } + let(:current_plan) { OpenStruct.new(code: 'bronze') } it 'returns plans with a deprecated plan' do expect(helper.billing_available_plans(plans_data, current_plan)).to eq(plans_data) @@ -321,7 +401,7 @@ end context 'when namespace is on a deprecated plan that has hide_deprecated_card set to true' do - let(:current_plan) { Hashie::Mash.new(code: 'bronze') } + let(:current_plan) { OpenStruct.new(code: 'bronze') } let(:deprecated_plan) { double('Plan', deprecated?: true, code: 'bronze', hide_deprecated_card?: true) } it 'returns plans without the deprecated plan' do @@ -330,7 +410,7 @@ end context 'when namespace is on a plan that has hide_deprecated_card set to true, but deprecated? is false' do - let(:current_plan) { Hashie::Mash.new(code: 'silver') } + let(:current_plan) { OpenStruct.new(code: 'silver') } let(:plan) { double('Plan', deprecated?: false, code: 'silver', hide_deprecated_card?: true) } it 'returns plans with the deprecated plan' do diff --git a/ee/spec/lib/gitlab/subscription_portal/client_spec.rb b/ee/spec/lib/gitlab/subscription_portal/client_spec.rb index 0fa9edfe09b2..cc3aa7a9ee12 100644 --- a/ee/spec/lib/gitlab/subscription_portal/client_spec.rb +++ b/ee/spec/lib/gitlab/subscription_portal/client_spec.rb @@ -137,4 +137,100 @@ expect(result).to eq({ errors: ["invalid activation code"], success: false }) end end + + describe '#plan_upgrade_offer' do + let(:namespace_id) { 111 } + let(:headers) do + { + "Accept" => "application/json", + "Content-Type" => "application/json", + "X-Admin-Email" => "gl_com_api@gitlab.com", + "X-Admin-Token" => "customer_admin_token" + } + end + + let(:params) do + { query: <<~GQL + { + subscription(namespaceId: "{:namespace_id=>#{namespace_id}}") { + eoaStarterBronzeEligible + assistedUpgradePlanId + freeUpgradePlanId + } + } + GQL + } + end + + subject(:plan_upgrade_offer) { described_class.plan_upgrade_offer(namespace_id: namespace_id) } + + context 'when the response contains errors' do + before do + expect(described_class).to receive(:http_post).with('graphql', headers, params).and_return(response) + end + + let(:response) do + { + success: true, + data: { + 'errors' => [{ 'message' => 'this will be ignored' }] + } + } + end + + it 'returns a failure' do + expect(plan_upgrade_offer).to eq({ success: false }) + end + end + + context 'when the response does not contain errors' do + using RSpec::Parameterized::TableSyntax + + where(:eligible, :assisted_plan_id, :free_plan_id) do + true | '111111' | '111111' + true | '111111' | nil + true | nil | '111111' + end + + with_them do + before do + allow(described_class).to receive(:http_post).and_return({ + success: true, + data: { "data" => { "subscription" => { + "eoaStarterBronzeEligible" => eligible, + "assistedUpgradePlanId" => assisted_plan_id, + "freeUpgradePlanId" => free_plan_id + } } } + }) + end + + it 'returns the correct response' do + expect(plan_upgrade_offer).to eq({ + success: true, + eligible_for_free_upgrade: eligible, + assisted_upgrade_plan_id: assisted_plan_id, + free_upgrade_plan_id: free_plan_id + }) + end + end + + context 'when subscription is nil' do + before do + allow(described_class).to receive(:http_post).and_return({ + success: true, + data: { "data" => { "subscription" => nil } } + }) + end + + it 'returns the correct response' do + expect(plan_upgrade_offer).to eq({ + success: true, + eligible_for_free_upgrade: nil, + assisted_upgrade_plan_id: nil, + free_upgrade_plan_id: nil + }) + end + end + end + end end diff --git a/ee/spec/services/fetch_subscription_plans_service_spec.rb b/ee/spec/services/fetch_subscription_plans_service_spec.rb index 97a8065d93cb..2e0549d62785 100644 --- a/ee/spec/services/fetch_subscription_plans_service_spec.rb +++ b/ee/spec/services/fetch_subscription_plans_service_spec.rb @@ -10,7 +10,7 @@ let(:plan) { 'bronze' } let(:response_mock) { double(body: [{ 'foo' => 'bar' }].to_json) } - context 'when successully fetching plans data' do + context 'when successfully fetching plans data' do it 'returns parsed JSON' do expect(Gitlab::HTTP).to receive(:get) .with( diff --git a/ee/spec/services/gitlab_subscriptions/plan_upgrade_service_spec.rb b/ee/spec/services/gitlab_subscriptions/plan_upgrade_service_spec.rb new file mode 100644 index 000000000000..0a7d2e1c8f33 --- /dev/null +++ b/ee/spec/services/gitlab_subscriptions/plan_upgrade_service_spec.rb @@ -0,0 +1,63 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe GitlabSubscriptions::PlanUpgradeService do + subject(:execute) { described_class.new(namespace_id: namespace_id).execute } + + let(:namespace_id) { '111' } + + describe '#execute' do + using RSpec::Parameterized::TableSyntax + + before do + allow(Gitlab::SubscriptionPortal::Client).to receive(:plan_upgrade_offer).and_return(response) + end + + context 'when the response is a failure' do + let(:response) { { success: false } } + + it 'returns nil values' do + expect(execute).to eq({ + upgrade_for_free: nil, + upgrade_plan_id: nil + }) + end + end + + context 'when the response is successful' do + where(:eligible, :assisted_id, :free_id, :plan_id) do + true | '111' | '222' | '111' + true | nil | '222' | '222' + true | '111' | nil | '111' + true | nil | nil | nil + false | '111' | '222' | '111' + false | '111' | nil | '111' + false | nil | '222' | '222' + nil | '111' | '222' | nil + end + + with_them do + let(:response) do + { + success: true, + eligible_for_free_upgrade: eligible, + assisted_upgrade_plan_id: assisted_id, + free_upgrade_plan_id: free_id + } + end + + before do + expect(Gitlab::SubscriptionPortal::Client).to receive(:plan_upgrade_offer).once.and_return(response) + end + + it 'returns the correct values' do + expect(execute).to eq({ + upgrade_for_free: eligible, + upgrade_plan_id: plan_id + }) + end + end + end + end +end diff --git a/ee/spec/support/helpers/subscription_portal_helpers.rb b/ee/spec/support/helpers/subscription_portal_helpers.rb new file mode 100644 index 000000000000..e950f6014408 --- /dev/null +++ b/ee/spec/support/helpers/subscription_portal_helpers.rb @@ -0,0 +1,37 @@ +# frozen_string_literal: true + +module SubscriptionPortalHelpers + include StubRequests + + def stub_eoa_eligibility_request(namespace_id) + stub_full_request("#{EE::SUBSCRIPTIONS_URL}/graphql", method: :post) + .with( + body: "{\"query\":\"{\\n subscription(namespaceId: \\\"#{namespace_id}\\\") {\\n eoaStarterBronzeEligible\\n assistedUpgradePlanId\\n freeUpgradePlanId\\n }\\n}\\n\"}", + headers: { + 'Accept' => 'application/json', + 'Content-Type' => 'application/json', + 'X-Admin-Email' => EE::SUBSCRIPTION_PORTAL_ADMIN_EMAIL, + 'X-Admin-Token' => EE::SUBSCRIPTION_PORTAL_ADMIN_TOKEN + } + ) + .to_return( + status: 200, + headers: { 'Content-Type' => 'application/json' }, + body: stubbed_eoa_eligibility_response_body + ) + end + + private + + def stubbed_eoa_eligibility_response_body + { + "data": { + "subscription": { + "eoaStarterBronzeEligible": false, + "assistedUpgradePlanId": nil, + "freeUpgradePlanId": nil + } + } + }.to_json + end +end diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 287ac10d08ac..4c05ce63bf76 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -4457,6 +4457,12 @@ msgstr "" msgid "BillingPlans|Congratulations, your free trial is activated." msgstr "" +msgid "BillingPlans|Current Plan" +msgstr "" + +msgid "BillingPlans|Free upgrade!" +msgstr "" + msgid "BillingPlans|If you would like to downgrade your plan please contact %{support_link_start}Customer Support%{support_link_end}." msgstr "" @@ -4490,6 +4496,9 @@ msgstr "" msgid "BillingPlans|billed annually at %{price_per_year}" msgstr "" +msgid "BillingPlans|for the remainder of your subscription" +msgstr "" + msgid "BillingPlans|frequently asked questions" msgstr "" @@ -4505,6 +4514,9 @@ msgstr "" msgid "BillingPlan|Upgrade" msgstr "" +msgid "BillingPlan|Upgrade for free" +msgstr "" + msgid "Billing|An email address is only visible for users with public emails." msgstr "" @@ -8510,9 +8522,6 @@ msgstr "" msgid "Current Branch" msgstr "" -msgid "Current Plan" -msgstr "" - msgid "Current Project" msgstr "" @@ -30638,6 +30647,9 @@ msgstr "" msgid "Updating" msgstr "" +msgid "Upgrade offers available!" +msgstr "" + msgid "Upgrade your plan" msgstr "" -- GitLab