From e2953bd3fa9a05cf94c9a89a7df623b755cc5694 Mon Sep 17 00:00:00 2001 From: Oswaldo Ferreira <oswaldo@gitlab.com> Date: Fri, 16 Aug 2019 21:25:37 +0000 Subject: [PATCH] Add an Upgrade button to Group's billings page It adds an upgrade button to the group billings page that redirects to the customers portal upgrade checkout page. If the user is not logged in at the portal he/she will be automatically redirected after the login process. If the user is at the latest tier, the button won't be presented. --- .../javascripts/billings/components/app.vue | 11 +- .../components/subscription_table.vue | 54 +++++-- ee/app/assets/javascripts/billings/index.js | 6 +- .../stores/modules/subscription/state.js | 1 + ee/app/helpers/billing_plans_helper.rb | 16 ++ ee/app/models/gitlab_subscription.rb | 12 ++ ee/app/models/plan.rb | 1 + ee/app/views/groups/billings/index.html.haml | 9 +- .../billings/_billing_plan_header.html.haml | 4 +- .../shared/billings/_trial_status.html.haml | 8 +- .../osw-group-level-upgrade-button.yml | 5 + ee/lib/ee/api/entities.rb | 1 + .../features/billings/billing_plans_spec.rb | 4 +- ee/spec/features/groups/billing_spec.rb | 31 +++- ee/spec/fixtures/gitlab_com_plans.json | 3 + .../components/__snapshots__/app_spec.js.snap | 8 + .../subscription_table_spec.js.snap | 56 +++++++ .../frontend/billings/components/app_spec.js | 63 ++++++++ .../components/subscription_table_spec.js | 125 +++++++++++++++ ee/spec/frontend/billings/mock_data.js | 60 ++++++++ .../__snapshots__/mutations_spec.js.snap | 37 +++++ .../modules/subscription/getters_spec.js | 2 +- .../modules/subscription/mutations_spec.js | 93 +++++++++++ ee/spec/helpers/billing_plans_helper_spec.rb | 38 +++++ .../components/subscription_table_spec.js | 74 --------- ee/spec/javascripts/billings/mock_data.js | 60 +------- .../modules/subscription/mutations_spec.js | 145 ------------------ ee/spec/models/gitlab_subscription_spec.rb | 75 +++++++++ ee/spec/requests/api/namespaces_spec.rb | 3 +- locale/gitlab.pot | 16 +- 30 files changed, 706 insertions(+), 315 deletions(-) create mode 100644 ee/changelogs/unreleased/osw-group-level-upgrade-button.yml create mode 100644 ee/spec/frontend/billings/components/__snapshots__/app_spec.js.snap create mode 100644 ee/spec/frontend/billings/components/__snapshots__/subscription_table_spec.js.snap create mode 100644 ee/spec/frontend/billings/components/app_spec.js create mode 100644 ee/spec/frontend/billings/components/subscription_table_spec.js create mode 100644 ee/spec/frontend/billings/mock_data.js create mode 100644 ee/spec/frontend/billings/store/modules/subscription/__snapshots__/mutations_spec.js.snap rename ee/spec/frontend/billings/{stores => store}/modules/subscription/getters_spec.js (91%) create mode 100644 ee/spec/frontend/billings/store/modules/subscription/mutations_spec.js delete mode 100644 ee/spec/javascripts/billings/components/subscription_table_spec.js delete mode 100644 ee/spec/javascripts/billings/stores/modules/subscription/mutations_spec.js diff --git a/ee/app/assets/javascripts/billings/components/app.vue b/ee/app/assets/javascripts/billings/components/app.vue index 426073861c048..18e2fd6b56a75 100644 --- a/ee/app/assets/javascripts/billings/components/app.vue +++ b/ee/app/assets/javascripts/billings/components/app.vue @@ -8,11 +8,20 @@ export default { SubscriptionTable, }, props: { + planUpgradeHref: { + type: String, + required: false, + default: null, + }, namespaceId: { type: String, required: false, default: null, }, + namespaceName: { + type: String, + required: true, + }, }, created() { this.setNamespaceId(this.namespaceId); @@ -24,5 +33,5 @@ export default { </script> <template> - <subscription-table /> + <subscription-table :namespace-name="namespaceName" :plan-upgrade-href="planUpgradeHref" /> </template> diff --git a/ee/app/assets/javascripts/billings/components/subscription_table.vue b/ee/app/assets/javascripts/billings/components/subscription_table.vue index 5b0913e5fa1e0..d343d76e934ca 100644 --- a/ee/app/assets/javascripts/billings/components/subscription_table.vue +++ b/ee/app/assets/javascripts/billings/components/subscription_table.vue @@ -2,6 +2,7 @@ import _ from 'underscore'; import { mapActions, mapState, mapGetters } from 'vuex'; import { GlLoadingIcon } from '@gitlab/ui'; +import { s__ } from '~/locale'; import SubscriptionTableRow from './subscription_table_row.vue'; import { CUSTOMER_PORTAL_URL, @@ -9,7 +10,6 @@ import { TABLE_TYPE_FREE, TABLE_TYPE_TRIAL, } from '../constants'; -import { s__, sprintf } from '~/locale'; export default { name: 'SubscriptionTable', @@ -17,21 +17,48 @@ export default { SubscriptionTableRow, GlLoadingIcon, }, + props: { + namespaceName: { + type: String, + required: true, + }, + planUpgradeHref: { + type: String, + required: false, + default: null, + }, + }, computed: { ...mapState('subscription', ['isLoading', 'hasError', 'plan', 'tables', 'endpoint']), ...mapGetters('subscription', ['isFreePlan']), subscriptionHeader() { - let suffix = ''; - if (!this.isFreePlan && this.plan.trial) { - suffix = `${s__('SubscriptionTable|Trial')}`; + const planName = this.isFreePlan ? s__('SubscriptionTable|Free') : _.escape(this.plan.name); + const suffix = !this.isFreePlan && this.plan.trial ? s__('SubscriptionTable|Trial') : ''; + + return `${this.namespaceName}: ${planName} ${suffix}`; + }, + upgradeButton() { + if (!this.isFreePlan && !this.plan.upgradable) { + return null; } - return sprintf(s__('SubscriptionTable|GitLab.com %{planName} %{suffix}'), { - planName: this.isFreePlan ? s__('SubscriptionTable|Free') : _.escape(this.plan.name), - suffix, - }); + + return { + text: s__('SubscriptionTable|Upgrade'), + href: !this.isFreePlan && this.planUpgradeHref ? this.planUpgradeHref : CUSTOMER_PORTAL_URL, + }; + }, + manageButton() { + if (this.isFreePlan) { + return null; + } + + return { + text: s__('SubscriptionTable|Manage'), + href: CUSTOMER_PORTAL_URL, + }; }, - actionButtonText() { - return this.isFreePlan ? s__('SubscriptionTable|Upgrade') : s__('SubscriptionTable|Manage'); + buttons() { + return [this.upgradeButton, this.manageButton].filter(Boolean); }, visibleRows() { let tableKey = TABLE_TYPE_DEFAULT; @@ -65,12 +92,15 @@ export default { <strong> {{ subscriptionHeader }} </strong> <div class="controls"> <a - :href="$options.customerPortalUrl" + v-for="(button, index) in buttons" + :key="button.text" + :href="button.href" target="_blank" rel="noopener noreferrer" class="btn btn-inverted-secondary" + :class="{ 'ml-2': index !== 0 }" > - {{ actionButtonText }} + {{ button.text }} </a> </div> </div> diff --git a/ee/app/assets/javascripts/billings/index.js b/ee/app/assets/javascripts/billings/index.js index 485430dd14f18..725e5b3c24960 100644 --- a/ee/app/assets/javascripts/billings/index.js +++ b/ee/app/assets/javascripts/billings/index.js @@ -17,16 +17,20 @@ export default (containerId = 'js-billing-plans') => { }, data() { const { dataset } = this.$options.el; - const { namespaceId } = dataset; + const { namespaceId, namespaceName, planUpgradeHref } = dataset; return { namespaceId, + namespaceName, + planUpgradeHref, }; }, render(createElement) { return createElement('subscription-app', { props: { namespaceId: this.namespaceId, + namespaceName: this.namespaceName, + planUpgradeHref: this.planUpgradeHref, }, }); }, diff --git a/ee/app/assets/javascripts/billings/stores/modules/subscription/state.js b/ee/app/assets/javascripts/billings/stores/modules/subscription/state.js index c31154b1df305..0fad697de684a 100644 --- a/ee/app/assets/javascripts/billings/stores/modules/subscription/state.js +++ b/ee/app/assets/javascripts/billings/stores/modules/subscription/state.js @@ -8,6 +8,7 @@ export default () => ({ code: null, name: null, trial: false, + upgradable: false, }, tables: { free: { diff --git a/ee/app/helpers/billing_plans_helper.rb b/ee/app/helpers/billing_plans_helper.rb index c958bb74dcefa..2de8516bc1f36 100644 --- a/ee/app/helpers/billing_plans_helper.rb +++ b/ee/app/helpers/billing_plans_helper.rb @@ -28,4 +28,20 @@ def plan_purchase_link(href, link_text) def new_gitlab_com_trial_url "#{EE::SUBSCRIPTIONS_URL}/trials/new?gl_com=true" end + + def subscription_plan_data_attributes(group, plan) + return {} unless group + + { + namespace_id: group.id, + namespace_name: group.name, + plan_upgrade_href: plan_upgrade_url(group, plan) + } + end + + def plan_upgrade_url(group, plan) + return unless group && plan&.id + + "#{EE::SUBSCRIPTIONS_URL}/gitlab/namespaces/#{group.id}/upgrade/#{plan.id}" + end end diff --git a/ee/app/models/gitlab_subscription.rb b/ee/app/models/gitlab_subscription.rb index 10ac13a996a32..40b97eb2b7f04 100644 --- a/ee/app/models/gitlab_subscription.rb +++ b/ee/app/models/gitlab_subscription.rb @@ -36,6 +36,18 @@ def has_a_paid_hosted_plan? Plan::PAID_HOSTED_PLANS.include?(plan_name) end + def expired? + return false unless end_date + + end_date < Date.today + end + + def upgradable? + has_a_paid_hosted_plan? && + !expired? && + plan_name != Plan::PAID_HOSTED_PLANS[-1] + end + def plan_code=(code) code ||= Namespace::FREE_PLAN diff --git a/ee/app/models/plan.rb b/ee/app/models/plan.rb index 11b538b0194f4..87831ee019566 100644 --- a/ee/app/models/plan.rb +++ b/ee/app/models/plan.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true class Plan < ApplicationRecord + # This constant must keep ordered by tier. PAID_HOSTED_PLANS = %w[bronze silver gold].freeze ALL_HOSTED_PLANS = (PAID_HOSTED_PLANS + ['early_adopter']).freeze diff --git a/ee/app/views/groups/billings/index.html.haml b/ee/app/views/groups/billings/index.html.haml index 3e9dad916a903..30fa3ac33cd8c 100644 --- a/ee/app/views/groups/billings/index.html.haml +++ b/ee/app/views/groups/billings/index.html.haml @@ -1,5 +1,4 @@ - current_plan = subscription_plan_info(@plans_data, @group.actual_plan_name) - - page_title "Billing" - if @top_most_group @@ -8,4 +7,10 @@ - else = render 'shared/billings/billing_plan_header', namespace: @group, plan: current_plan - #js-billing-plans{ data: { namespace_id: @group.id } } + #js-billing-plans{ data: subscription_plan_data_attributes(@group, current_plan) } + +- if @group.actual_plan + .center + - support_link = link_to s_("BillingPlans|Contact Support"), "https://support.gitlab.com" + + = s_("BillingPlans|If you would like to downgrade your plan please %{support_link}.").html_safe % { support_link: support_link } 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 2b4d3ece9a098..8983d6e9d0417 100644 --- a/ee/app/views/shared/billings/_billing_plan_header.html.haml +++ b/ee/app/views/shared/billings/_billing_plan_header.html.haml @@ -14,11 +14,11 @@ = group_icon(@group, class: 'avatar avatar-tile s96', width: 96, height: 96, alt: @group.name) %h4 - - plan_link = plan.about_page_href ? link_to(plan.name, plan.about_page_href) : plan.name + - plan_link = plan.about_page_href ? link_to(plan.code.titleize, plan.about_page_href) : plan.name - if namespace == current_user.namespace = s_("BillingPlans|@%{user_name} you are currently on the %{plan_link} plan.").html_safe % { user_name: current_user.username, plan_link: plan_link } - else - = s_("BillingPlans|%{group_name} is currently on the %{plan_link} plan.").html_safe % { group_name: namespace.full_name, plan_link: plan_link } + = s_("BillingPlans|%{group_name} is currently using the %{plan_link} plan.").html_safe % { group_name: namespace.full_name, plan_link: plan_link } - if parent_group %p= s_("BillingPlans|This group uses the plan associated with its parent group.") diff --git a/ee/app/views/shared/billings/_trial_status.html.haml b/ee/app/views/shared/billings/_trial_status.html.haml index dd13b1daf562a..afa05fadb9087 100644 --- a/ee/app/views/shared/billings/_trial_status.html.haml +++ b/ee/app/views/shared/billings/_trial_status.html.haml @@ -1,6 +1,6 @@ -- faq_link = link_to s_("BillingPlans|frequently asked questions"), "https://about.gitlab.com/gitlab-com/#faq" -- features_link = link_to s_("BillingPlans|features"), "https://about.gitlab.com/features" -- learn_more_text = s_("BillingPlans|Learn more about each plan by reading our %{faq_link}.").html_safe % { faq_link: faq_link } +- faq_link = link_to s_("BillingPlans|frequently asked questions"), "https://about.gitlab.com/gitlab-com/#faq" +- pricing_page_link = link_to s_("BillingPlans|Pricing page"), "https://about.gitlab.com/pricing" +- features_link = link_to s_("BillingPlans|features"), "https://about.gitlab.com/features" - if namespace.eligible_for_trial? = s_("BillingPlans|Learn more about each plan by reading our %{faq_link}, or start a free 30-day trial of GitLab.com Gold.").html_safe % { faq_link: faq_link } @@ -9,4 +9,4 @@ - elsif namespace.trial_expired? = s_("BillingPlans|Your GitLab.com trial expired on %{expiration_date}. %{learn_more_text}").html_safe % { expiration_date: namespace.trial_ends_on, learn_more_text: learn_more_text } - else - = learn_more_text + = s_("BillingPlans|Learn more about each plan by visiting our %{pricing_page_link}.").html_safe % { pricing_page_link: pricing_page_link } diff --git a/ee/changelogs/unreleased/osw-group-level-upgrade-button.yml b/ee/changelogs/unreleased/osw-group-level-upgrade-button.yml new file mode 100644 index 0000000000000..fb37baf46c097 --- /dev/null +++ b/ee/changelogs/unreleased/osw-group-level-upgrade-button.yml @@ -0,0 +1,5 @@ +--- +title: Add an Upgrade button to Group's billings page +merge_request: 14849 +author: +type: added diff --git a/ee/lib/ee/api/entities.rb b/ee/lib/ee/api/entities.rb index 0af462a241763..65e2a1957ddc7 100644 --- a/ee/lib/ee/api/entities.rb +++ b/ee/lib/ee/api/entities.rb @@ -682,6 +682,7 @@ class GitlabSubscription < Grape::Entity expose :plan_name, as: :code expose :plan_title, as: :name expose :trial + expose :upgradable?, as: :upgradable end expose :usage do diff --git a/ee/spec/features/billings/billing_plans_spec.rb b/ee/spec/features/billings/billing_plans_spec.rb index 63a66f24f633a..869806d4438aa 100644 --- a/ee/spec/features/billings/billing_plans_spec.rb +++ b/ee/spec/features/billings/billing_plans_spec.rb @@ -141,7 +141,7 @@ it 'displays plan header' do page.within('.billing-plan-header') do - expect(page).to have_content("#{group.name} is currently on the Bronze plan") + expect(page).to have_content("#{group.name} is currently using the Bronze plan") expect(page).to have_css('.billing-plan-logo .identicon') end @@ -178,7 +178,7 @@ it 'displays plan header' do page.within('.billing-plan-header') do - expect(page).to have_content("#{subgroup2.full_name} is currently on the Bronze plan") + expect(page).to have_content("#{subgroup2.full_name} is currently using the Bronze plan") expect(page).to have_css('.billing-plan-logo .identicon') expect(page.find('.btn-success')).to have_content('Manage plan') end diff --git a/ee/spec/features/groups/billing_spec.rb b/ee/spec/features/groups/billing_spec.rb index 20d1028bec436..fa173107d9439 100644 --- a/ee/spec/features/groups/billing_spec.rb +++ b/ee/spec/features/groups/billing_spec.rb @@ -7,6 +7,10 @@ let!(:group) { create(:group) } let!(:bronze_plan) { create(:bronze_plan) } + def formatted_date(date) + date.strftime("%B %-d, %Y") + end + before do stub_full_request("https://customers.gitlab.com/gitlab_plans?plan=#{plan}") .to_return(status: 200, body: File.new(Rails.root.join('ee/spec/fixtures/gitlab_com_plans.json'))) @@ -20,28 +24,37 @@ context 'with a free plan' do let(:plan) { 'free' } - before do + let!(:subscription) do create(:gitlab_subscription, namespace: group, hosted_plan: nil, seats: 15) end - it 'shows the proper title for the plan' do + it 'shows the proper title and subscription data' do visit group_billings_path(group) - expect(page).to have_content("#{group.name} is currently on the Free plan") + expect(page).to have_content("#{group.name} is currently using the Free plan") + expect(page).to have_content("start date #{formatted_date(subscription.start_date)}") + expect(page).to have_link("Upgrade", href: "#{EE::SUBSCRIPTIONS_URL}/subscriptions") + expect(page).not_to have_link("Manage") end end context 'with a paid plan' do let(:plan) { 'bronze' } - before do + let!(:subscription) do create(:gitlab_subscription, namespace: group, hosted_plan: bronze_plan, seats: 15) end - it 'shows the proper title for the plan' do + it 'shows the proper title and subscription data' do visit group_billings_path(group) - expect(page).to have_content("#{group.name} is currently on the Bronze plan") + upgrade_url = + "#{EE::SUBSCRIPTIONS_URL}/gitlab/namespaces/#{group.id}/upgrade/bronze-external-id" + + expect(page).to have_content("#{group.name} is currently using the Bronze plan") + expect(page).to have_content("start date #{formatted_date(subscription.start_date)}") + expect(page).to have_link("Upgrade", href: upgrade_url) + expect(page).to have_link("Manage", href: "#{EE::SUBSCRIPTIONS_URL}/subscriptions") end end @@ -52,10 +65,12 @@ group.update_attribute(:plan, bronze_plan) end - it 'shows the proper title for the plan' do + it 'shows the proper title and subscription data' do visit group_billings_path(group) - expect(page).to have_content("#{group.name} is currently on the Bronze plan") + expect(page).to have_content("#{group.name} is currently using the Bronze plan") + expect(page).not_to have_link("Upgrade") + expect(page).to have_link("Manage", href: "#{EE::SUBSCRIPTIONS_URL}/subscriptions") end end end diff --git a/ee/spec/fixtures/gitlab_com_plans.json b/ee/spec/fixtures/gitlab_com_plans.json index 0d574e1717346..4215555c7b713 100644 --- a/ee/spec/fixtures/gitlab_com_plans.json +++ b/ee/spec/fixtures/gitlab_com_plans.json @@ -46,6 +46,7 @@ } }, { + "id": "bronze-external-id", "about_page_href": "https://about.gitlab.com/gitlab-com/", "code": "bronze", "features": [ @@ -92,6 +93,7 @@ } }, { + "id": "silver-external-id", "about_page_href": "https://about.gitlab.com/gitlab-com/", "code": "silver", "features": [ @@ -126,6 +128,7 @@ } }, { + "id": "gold-external-id", "about_page_href": "https://about.gitlab.com/gitlab-com/", "code": "gold", "features": [ diff --git a/ee/spec/frontend/billings/components/__snapshots__/app_spec.js.snap b/ee/spec/frontend/billings/components/__snapshots__/app_spec.js.snap new file mode 100644 index 0000000000000..5658d0b0ebae9 --- /dev/null +++ b/ee/spec/frontend/billings/components/__snapshots__/app_spec.js.snap @@ -0,0 +1,8 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`SubscriptionApp component on creation matches the snapshot 1`] = ` +<subscriptiontable-stub + namespacename="bronze" + planupgradehref="/url" +/> +`; diff --git a/ee/spec/frontend/billings/components/__snapshots__/subscription_table_spec.js.snap b/ee/spec/frontend/billings/components/__snapshots__/subscription_table_spec.js.snap new file mode 100644 index 0000000000000..94004b18e2bfe --- /dev/null +++ b/ee/spec/frontend/billings/components/__snapshots__/subscription_table_spec.js.snap @@ -0,0 +1,56 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`SubscriptionTable component given a bronze plan with state: isFreePlan=false, upgradable=true, isTrialPlan=false has Upgrade and Manage buttons 1`] = ` +Array [ + Object { + "href": "http://test.host/plan/upgrade/bronze", + "text": "Upgrade", + }, + Object { + "href": "https://customers.gitlab.com/subscriptions", + "text": "Manage", + }, +] +`; + +exports[`SubscriptionTable component given a free plan with state: isFreePlan=true, upgradable=true, isTrialPlan=false has Upgrade and Manage buttons 1`] = ` +Array [ + Object { + "href": "http://test.host/plan/upgrade/free", + "text": "Upgrade", + }, + Object { + "href": "https://customers.gitlab.com/subscriptions", + "text": "Manage", + }, +] +`; + +exports[`SubscriptionTable component given a gold plan with state: isFreePlan=false, upgradable=false, isTrialPlan=false has Manage button 1`] = ` +Array [ + Object { + "href": "https://customers.gitlab.com/subscriptions", + "text": "Manage", + }, +] +`; + +exports[`SubscriptionTable component given a trial-gold plan with state: isFreePlan=false, upgradable=false, isTrialPlan=true has Manage button 1`] = ` +Array [ + Object { + "href": "https://customers.gitlab.com/subscriptions", + "text": "Manage", + }, +] +`; + +exports[`SubscriptionTable component when created matches the snapshot 1`] = ` +<div> + <glloadingicon-stub + class="prepend-top-10 append-bottom-10" + color="orange" + label="Loading subscriptions" + size="3" + /> +</div> +`; diff --git a/ee/spec/frontend/billings/components/app_spec.js b/ee/spec/frontend/billings/components/app_spec.js new file mode 100644 index 0000000000000..aeaa603976b29 --- /dev/null +++ b/ee/spec/frontend/billings/components/app_spec.js @@ -0,0 +1,63 @@ +import { createLocalVue, shallowMount } from '@vue/test-utils'; +import createStore from 'ee/billings/stores'; +import SubscriptionApp from 'ee/billings/components/app.vue'; +import SubscriptionTable from 'ee/billings/components/subscription_table.vue'; + +describe('SubscriptionApp component', () => { + let store; + let wrapper; + + const appProps = { + namespaceId: '42', + namespaceName: 'bronze', + planUpgradeHref: '/url', + }; + + const factory = (props = appProps) => { + const localVue = createLocalVue(); + + store = createStore(); + jest.spyOn(store, 'dispatch').mockImplementation(); + + wrapper = shallowMount(SubscriptionApp, { + localVue, + store, + sync: false, + propsData: { ...props }, + }); + }; + + const expectComponentWithProps = (Component, props = {}) => { + const componentWrapper = wrapper.find(Component); + + expect(componentWrapper.isVisible()).toBeTruthy(); + expect(componentWrapper.props()).toEqual(expect.objectContaining(props)); + }; + + afterEach(() => { + wrapper.destroy(); + }); + + describe('on creation', () => { + beforeEach(() => { + factory(); + }); + + it('dispatches the setNamespaceId on mounted', () => { + expect(store.dispatch.mock.calls).toEqual([ + ['subscription/setNamespaceId', appProps.namespaceId], + ]); + }); + + it('matches the snapshot', () => { + expect(wrapper.element).toMatchSnapshot(); + }); + + it('passes the correct props to the subscriptions table', () => { + expectComponentWithProps(SubscriptionTable, { + namespaceName: appProps.namespaceName, + planUpgradeHref: appProps.planUpgradeHref, + }); + }); + }); +}); diff --git a/ee/spec/frontend/billings/components/subscription_table_spec.js b/ee/spec/frontend/billings/components/subscription_table_spec.js new file mode 100644 index 0000000000000..31196068e1f6e --- /dev/null +++ b/ee/spec/frontend/billings/components/subscription_table_spec.js @@ -0,0 +1,125 @@ +import { createLocalVue, shallowMount } from '@vue/test-utils'; +import { GlLoadingIcon } from '@gitlab/ui'; +import { TEST_HOST } from 'helpers/test_constants'; +import createStore from 'ee/billings/stores'; +import * as types from 'ee/billings/stores/modules/subscription/mutation_types'; +import SubscriptionTable from 'ee/billings/components/subscription_table.vue'; +import SubscriptionTableRow from 'ee/billings/components/subscription_table_row.vue'; +import mockDataSubscription from '../mock_data'; + +const TEST_NAMESPACE_NAME = 'GitLab.com'; + +describe('SubscriptionTable component', () => { + let store; + let wrapper; + + const findButtonProps = () => + wrapper.findAll('a').wrappers.map(x => ({ text: x.text(), href: x.attributes('href') })); + + const factory = (options = {}) => { + const localVue = createLocalVue(); + + store = createStore(); + jest.spyOn(store, 'dispatch').mockImplementation(); + + wrapper = shallowMount(localVue.extend(SubscriptionTable), { + ...options, + localVue, + store, + sync: false, + }); + }; + + afterEach(() => { + wrapper.destroy(); + }); + + describe('when created', () => { + beforeEach(() => { + factory({ + propsData: { + namespaceName: TEST_NAMESPACE_NAME, + planUpgradeHref: '/url/', + }, + }); + + Object.assign(store.state.subscription, { isLoading: true }); + + return wrapper.vm.$nextTick(); + }); + + it('shows loading icon', () => { + expect(wrapper.find(GlLoadingIcon).isVisible()).toBeTruthy(); + }); + + it('dispatches the correct actions', () => { + expect(store.dispatch).toHaveBeenCalledWith('subscription/fetchSubscription', undefined); + }); + + it('matches the snapshot', () => { + expect(wrapper.element).toMatchSnapshot(); + }); + }); + + describe('with success', () => { + beforeEach(() => { + factory({ propsData: { namespaceName: TEST_NAMESPACE_NAME } }); + + store.state.subscription.isLoading = false; + store.commit(`subscription/${types.RECEIVE_SUBSCRIPTION_SUCCESS}`, mockDataSubscription.gold); + + return wrapper.vm.$nextTick(); + }); + + it('should render the card title "GitLab.com: Gold"', () => { + expect( + wrapper + .find('.js-subscription-header strong') + .text() + .trim(), + ).toBe('GitLab.com: Gold'); + }); + + it('should render a "Usage" and a "Billing" row', () => { + expect(wrapper.findAll(SubscriptionTableRow).length).toBe(2); + }); + }); + + describe.each` + planName | isFreePlan | upgradable | isTrialPlan | snapshotDesc + ${'free'} | ${true} | ${true} | ${false} | ${'has Upgrade and Manage buttons'} + ${'trial-gold'} | ${false} | ${false} | ${true} | ${'has Manage button'} + ${'gold'} | ${false} | ${false} | ${false} | ${'has Manage button'} + ${'bronze'} | ${false} | ${true} | ${false} | ${'has Upgrade and Manage buttons'} + `( + 'given a $planName plan with state: isFreePlan=$isFreePlan, upgradable=$upgradable, isTrialPlan=$isTrialPlan', + ({ planName, isFreePlan, upgradable, snapshotDesc }) => { + beforeEach(() => { + const planUpgradeHref = `${TEST_HOST}/plan/upgrade/${planName}`; + + factory({ + propsData: { + namespaceName: TEST_NAMESPACE_NAME, + planUpgradeHref, + }, + }); + + Object.assign(store.state.subscription, { + isLoading: false, + isFreePlan, + plan: { + code: planName, + name: planName, + upgradable, + }, + }); + + return wrapper.vm.$nextTick(); + }); + + it(snapshotDesc, () => { + expect(findButtonProps()).toMatchSnapshot(); + }); + }, + ); +}); diff --git a/ee/spec/frontend/billings/mock_data.js b/ee/spec/frontend/billings/mock_data.js new file mode 100644 index 0000000000000..6c418a1a12efa --- /dev/null +++ b/ee/spec/frontend/billings/mock_data.js @@ -0,0 +1,60 @@ +export default { + gold: { + plan: { + name: 'Gold', + code: 'gold', + trial: false, + upgradable: false, + }, + usage: { + seats_in_subscription: 100, + seats_in_use: 98, + max_seats_used: 104, + seats_owed: 4, + }, + billing: { + subscription_start_date: '2018-07-11', + subscription_end_date: '2019-07-11', + last_invoice: '2018-09-01', + next_invoice: '2018-10-01', + }, + }, + free: { + plan: { + name: null, + code: null, + trial: null, + upgradable: null, + }, + usage: { + seats_in_subscription: 0, + seats_in_use: 0, + max_seats_used: 5, + seats_owed: 0, + }, + billing: { + subscription_start_date: '2018-10-30', + subscription_end_date: null, + trial_ends_on: null, + }, + }, + trial: { + plan: { + name: 'Gold', + code: 'gold', + trial: true, + upgradable: false, + }, + usage: { + seats_in_subscription: 100, + seats_in_use: 1, + max_seats_used: 0, + seats_owed: 0, + }, + billing: { + subscription_start_date: '2018-12-13', + subscription_end_date: '2019-12-13', + trial_ends_on: '2019-12-13', + }, + }, +}; diff --git a/ee/spec/frontend/billings/store/modules/subscription/__snapshots__/mutations_spec.js.snap b/ee/spec/frontend/billings/store/modules/subscription/__snapshots__/mutations_spec.js.snap new file mode 100644 index 0000000000000..2d9423325e12f --- /dev/null +++ b/ee/spec/frontend/billings/store/modules/subscription/__snapshots__/mutations_spec.js.snap @@ -0,0 +1,37 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`EE billings subscription module mutations RECEIVE_SUBSCRIPTION_SUCCESS with Free plan it updates table free with subscription plan 1`] = ` +Array [ + Object { + "seatsInUse": 0, + "subscriptionStartDate": "2018-10-30", + }, +] +`; + +exports[`EE billings subscription module mutations RECEIVE_SUBSCRIPTION_SUCCESS with Gold subscription it updates table default with subscription plan 1`] = ` +Array [ + Object { + "maxSeatsUsed": 104, + "seatsInSubscription": 100, + "seatsInUse": 98, + "seatsOwed": 4, + }, + Object { + "lastInvoice": "2018-09-01", + "nextInvoice": "2018-10-01", + "subscriptionEndDate": "2019-07-11", + "subscriptionStartDate": "2018-07-11", + }, +] +`; + +exports[`EE billings subscription module mutations RECEIVE_SUBSCRIPTION_SUCCESS with Gold trial it updates table trial with subscription plan 1`] = ` +Array [ + Object { + "seatsInUse": 1, + "subscriptionEndDate": "2019-12-13", + "subscriptionStartDate": "2018-12-13", + }, +] +`; diff --git a/ee/spec/frontend/billings/stores/modules/subscription/getters_spec.js b/ee/spec/frontend/billings/store/modules/subscription/getters_spec.js similarity index 91% rename from ee/spec/frontend/billings/stores/modules/subscription/getters_spec.js rename to ee/spec/frontend/billings/store/modules/subscription/getters_spec.js index 2f4ce06a8107a..d9db956fc82bf 100644 --- a/ee/spec/frontend/billings/stores/modules/subscription/getters_spec.js +++ b/ee/spec/frontend/billings/store/modules/subscription/getters_spec.js @@ -1,7 +1,7 @@ import State from 'ee/billings/stores/modules/subscription/state'; import * as getters from 'ee/billings/stores/modules/subscription/getters'; -describe('subscription module getters', () => { +describe('EE billings subscription module getters', () => { let state; beforeEach(() => { diff --git a/ee/spec/frontend/billings/store/modules/subscription/mutations_spec.js b/ee/spec/frontend/billings/store/modules/subscription/mutations_spec.js new file mode 100644 index 0000000000000..c9882e2906d71 --- /dev/null +++ b/ee/spec/frontend/billings/store/modules/subscription/mutations_spec.js @@ -0,0 +1,93 @@ +import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils'; +import createState from 'ee/billings/stores/modules/subscription/state'; +import * as types from 'ee/billings/stores/modules/subscription/mutation_types'; +import mutations from 'ee/billings/stores/modules/subscription/mutations'; +import { TABLE_TYPE_DEFAULT, TABLE_TYPE_FREE, TABLE_TYPE_TRIAL } from 'ee/billings/constants'; +import mockSubscriptionData from '../../../mock_data'; + +describe('EE billings subscription module mutations', () => { + let state; + + beforeEach(() => { + state = createState(); + }); + + describe(types.SET_NAMESPACE_ID, () => { + it('sets namespaceId', () => { + const expectedNamespaceId = 'test'; + + expect(state.namespaceId).toBeNull(); + + mutations[types.SET_NAMESPACE_ID](state, expectedNamespaceId); + + expect(state.namespaceId).toEqual(expectedNamespaceId); + }); + }); + + describe(types.REQUEST_SUBSCRIPTION, () => { + beforeEach(() => { + mutations[types.REQUEST_SUBSCRIPTION](state); + }); + + it('sets isLoading to true', () => { + expect(state.isLoading).toBeTruthy(); + }); + + it('sets hasError to false', () => { + expect(state.hasError).toBeFalsy(); + }); + }); + + describe(types.RECEIVE_SUBSCRIPTION_SUCCESS, () => { + const getColumnValues = columns => + columns.reduce( + (acc, { id, value }) => ({ + ...acc, + [id]: value, + }), + {}, + ); + const getStateTableValues = key => + state.tables[key].rows.map(({ columns }) => getColumnValues(columns)); + + describe.each` + desc | subscription | tableKey + ${'with Gold subscription'} | ${mockSubscriptionData.gold} | ${TABLE_TYPE_DEFAULT} + ${'with Free plan'} | ${mockSubscriptionData.free} | ${TABLE_TYPE_FREE} + ${'with Gold trial'} | ${mockSubscriptionData.trial} | ${TABLE_TYPE_TRIAL} + `('$desc', ({ subscription, tableKey }) => { + beforeEach(() => { + state.isLoading = true; + mutations[types.RECEIVE_SUBSCRIPTION_SUCCESS](state, subscription); + }); + + it('sets isLoading to false', () => { + expect(state.isLoading).toBeFalsy(); + }); + + it('sets plan', () => { + const { plan } = convertObjectPropsToCamelCase(subscription, { deep: true }); + + expect(state.plan).toEqual(plan); + }); + + it(`it updates table ${tableKey} with subscription plan`, () => { + expect(getStateTableValues(tableKey)).toMatchSnapshot(); + }); + }); + }); + + describe(types.RECEIVE_SUBSCRIPTION_ERROR, () => { + beforeEach(() => { + mutations[types.RECEIVE_SUBSCRIPTION_ERROR](state); + }); + + it('sets isLoading to false', () => { + expect(state.isLoading).toBeFalsy(); + }); + + it('sets hasError to true', () => { + expect(state.hasError).toBeTruthy(); + }); + }); +}); diff --git a/ee/spec/helpers/billing_plans_helper_spec.rb b/ee/spec/helpers/billing_plans_helper_spec.rb index 56f7a045f879f..2b4445e9da728 100644 --- a/ee/spec/helpers/billing_plans_helper_spec.rb +++ b/ee/spec/helpers/billing_plans_helper_spec.rb @@ -14,4 +14,42 @@ expect(helper.current_plan?(plan)).to be_falsy end end + + describe '#subscription_plan_data_attributes' do + let(:group) { build(:group) } + let(:plan) do + Hashie::Mash.new(id: 'external-paid-plan-hash-code') + end + + context 'when group and plan with ID present' do + it 'returns data attributes' do + upgrade_href = + "#{EE::SUBSCRIPTIONS_URL}/gitlab/namespaces/#{group.id}/upgrade/#{plan.id}" + + expect(helper.subscription_plan_data_attributes(group, plan)) + .to eq(namespace_id: group.id, + namespace_name: group.name, + plan_upgrade_href: upgrade_href) + end + end + + context 'when group not present' do + let(:group) { nil } + + it 'returns empty data attributes' do + expect(helper.subscription_plan_data_attributes(group, plan)).to eq({}) + end + end + + context 'when plan with ID not present' do + let(:plan) { Hashie::Mash.new(id: nil) } + + it 'returns data attributes without upgrade href' do + expect(helper.subscription_plan_data_attributes(group, plan)) + .to eq(namespace_id: group.id, + namespace_name: group.name, + plan_upgrade_href: nil) + end + end + end end diff --git a/ee/spec/javascripts/billings/components/subscription_table_spec.js b/ee/spec/javascripts/billings/components/subscription_table_spec.js deleted file mode 100644 index 2cda5cedaff47..0000000000000 --- a/ee/spec/javascripts/billings/components/subscription_table_spec.js +++ /dev/null @@ -1,74 +0,0 @@ -import Vue from 'vue'; -import component from 'ee/billings/components/subscription_table.vue'; -import createStore from 'ee/billings/stores'; -import * as types from 'ee/billings/stores/modules/subscription/mutation_types'; -import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper'; -import mockDataSubscription from '../mock_data'; -import { resetStore } from '../helpers'; - -describe('Subscription Table', () => { - const Component = Vue.extend(component); - let store; - let vm; - - beforeEach(() => { - store = createStore(); - vm = createComponentWithStore(Component, store, {}); - spyOn(vm.$store, 'dispatch'); - - vm.$mount(); - }); - - afterEach(() => { - vm.$destroy(); - - resetStore(store); - }); - - it('renders loading icon', done => { - vm.$store.state.subscription.isLoading = true; - - vm.$nextTick() - .then(() => { - expect(vm.$el.querySelector('.loading-container')).not.toBe(null); - }) - .then(done) - .catch(done.fail); - }); - - describe('with success', () => { - const namespaceId = 1; - - beforeEach(done => { - vm.$store.state.subscription.namespaceId = namespaceId; - vm.$store.commit( - `subscription/${types.RECEIVE_SUBSCRIPTION_SUCCESS}`, - mockDataSubscription.gold, - ); - vm.$store.state.subscription.isLoading = false; - vm.$nextTick(done); - }); - - it('should render the card title "GitLab.com Gold"', () => { - expect(vm.$el.querySelector('.js-subscription-header strong').textContent.trim()).toBe( - 'GitLab.com Gold', - ); - }); - - it('should render a link labelled "Manage" in the card header', () => { - expect(vm.$el.querySelector('.js-subscription-header .btn').textContent.trim()).toBe( - 'Manage', - ); - }); - - it('should render a link linking to the customer portal', () => { - expect(vm.$el.querySelector('.js-subscription-header .btn').getAttribute('href')).toBe( - 'https://customers.gitlab.com/subscriptions', - ); - }); - - it('should render a "Usage" and a "Billing" row', () => { - expect(vm.$el.querySelectorAll('.grid-row')).toHaveLength(2); - }); - }); -}); diff --git a/ee/spec/javascripts/billings/mock_data.js b/ee/spec/javascripts/billings/mock_data.js index 4e07808940200..43f95eb493f37 100644 --- a/ee/spec/javascripts/billings/mock_data.js +++ b/ee/spec/javascripts/billings/mock_data.js @@ -1,57 +1,3 @@ -export default { - gold: { - plan: { - name: 'Gold', - code: 'gold', - trial: false, - }, - usage: { - seats_in_subscription: 100, - seats_in_use: 98, - max_seats_used: 104, - seats_owed: 4, - }, - billing: { - subscription_start_date: '2018-07-11', - subscription_end_date: '2019-07-11', - last_invoice: '2018-09-01', - next_invoice: '2018-10-01', - }, - }, - free: { - plan: { - name: null, - code: null, - trial: null, - }, - usage: { - seats_in_subscription: 0, - seats_in_use: 0, - max_seats_used: 5, - seats_owed: 0, - }, - billing: { - subscription_start_date: '2018-10-30', - subscription_end_date: null, - trial_ends_on: null, - }, - }, - trial: { - plan: { - name: 'Gold', - code: 'gold', - trial: true, - }, - usage: { - seats_in_subscription: 100, - seats_in_use: 1, - max_seats_used: 0, - seats_owed: 0, - }, - billing: { - subscription_start_date: '2018-12-13', - subscription_end_date: '2019-12-13', - trial_ends_on: '2019-12-13', - }, - }, -}; +import mockData from '../../frontend/billings/mock_data'; + +export default mockData; diff --git a/ee/spec/javascripts/billings/stores/modules/subscription/mutations_spec.js b/ee/spec/javascripts/billings/stores/modules/subscription/mutations_spec.js deleted file mode 100644 index 8a55469ace7a7..0000000000000 --- a/ee/spec/javascripts/billings/stores/modules/subscription/mutations_spec.js +++ /dev/null @@ -1,145 +0,0 @@ -import createState from 'ee/billings/stores/modules/subscription/state'; -import * as types from 'ee/billings/stores/modules/subscription/mutation_types'; -import mutations from 'ee/billings/stores/modules/subscription/mutations'; -import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils'; -import mockData from '../../../mock_data'; - -describe('subscription module mutations', () => { - describe('SET_NAMESPACE_ID', () => { - it('should set "namespaceId" to "1"', () => { - const state = createState(); - const namespaceId = '1'; - - mutations[types.SET_NAMESPACE_ID](state, namespaceId); - - expect(state.namespaceId).toEqual(namespaceId); - }); - }); - - describe('REQUEST_SUBSCRIPTION', () => { - let state; - - beforeEach(() => { - state = createState(); - mutations[types.REQUEST_SUBSCRIPTION](state); - }); - - it('should set "isLoading" to "true", ()', () => { - expect(state.isLoading).toBeTruthy(); - }); - }); - - describe('RECEIVE_SUBSCRIPTION_SUCCESS', () => { - let payload; - let state; - - describe('Gold subscription', () => { - beforeEach(() => { - state = createState(); - payload = mockData.gold; - mutations[types.RECEIVE_SUBSCRIPTION_SUCCESS](state, payload); - }); - - it('should set "isLoading" to "false"', () => { - expect(state.isLoading).toBeFalsy(); - }); - - it('should set "plan" attributes', () => { - expect(state.plan.code).toBe(payload.plan.code); - expect(state.plan.name).toBe(payload.plan.name); - expect(state.plan.trial).toBe(payload.plan.trial); - }); - - it('should set the column values on the "Usage" row', () => { - const usageRow = state.tables.default.rows[0]; - const data = convertObjectPropsToCamelCase(payload, { deep: true }); - usageRow.columns.forEach(column => { - expect(column.value).toBe(data.usage[column.id]); - }); - }); - - it('should set the column values on the "Billing" row', () => { - const billingRow = state.tables.default.rows[1]; - const data = convertObjectPropsToCamelCase(payload, { deep: true }); - billingRow.columns.forEach(column => { - expect(column.value).toBe(data.billing[column.id]); - }); - }); - }); - - describe('Free plan', () => { - beforeEach(() => { - state = createState(); - payload = mockData.free; - mutations[types.RECEIVE_SUBSCRIPTION_SUCCESS](state, payload); - }); - - it('should set "isLoading" to "false"', () => { - expect(state.isLoading).toBeFalsy(); - }); - - it('should set "plan" attributes', () => { - expect(state.plan.code).toBe(payload.plan.code); - expect(state.plan.name).toBe(payload.plan.name); - expect(state.plan.trial).toBe(payload.plan.trial); - }); - - it('should populate "subscriptionStartDate" from "billings row" correctly', () => { - const usageRow = state.tables.free.rows[0]; - const data = convertObjectPropsToCamelCase(payload, { deep: true }); - usageRow.columns.forEach(column => { - if (column.id === 'subscriptionStartDate') { - expect(column.value).toBe(data.billing.subscriptionStartDate); - } - }); - }); - }); - - describe('Gold trial', () => { - beforeEach(() => { - state = createState(); - payload = mockData.trial; - mutations[types.RECEIVE_SUBSCRIPTION_SUCCESS](state, payload); - }); - - it('should set "isLoading" to "false"', () => { - expect(state.isLoading).toBeFalsy(); - }); - - it('should set "plan" attributes', () => { - expect(state.plan.code).toBe(payload.plan.code); - expect(state.plan.name).toBe(payload.plan.name); - expect(state.plan.trial).toBe(payload.plan.trial); - }); - - it('should populate "subscriptionStartDate" and "subscriptionEndDate" from "billings row" correctly', () => { - const usageRow = state.tables.trial.rows[0]; - const data = convertObjectPropsToCamelCase(payload, { deep: true }); - usageRow.columns.forEach(column => { - if (column.id === 'subscriptionStartDate') { - expect(column.value).toBe(data.billing.subscriptionStartDate); - } else if (column.id === 'subscriptionEndDate') { - expect(column.value).toBe(data.billing.subscriptionEndDate); - } - }); - }); - }); - }); - - describe('RECEIVE_SUBSCRIPTION_ERROR', () => { - let state; - - beforeEach(() => { - state = createState(); - mutations[types.RECEIVE_SUBSCRIPTION_ERROR](state); - }); - - it('should set "isLoading" to "false"', () => { - expect(state.isLoading).toBeFalsy(); - }); - - it('should set "hasError" to "true"', () => { - expect(state.hasError).toBeTruthy(); - }); - }); -}); diff --git a/ee/spec/models/gitlab_subscription_spec.rb b/ee/spec/models/gitlab_subscription_spec.rb index ac74629d810e2..3bfe36a54359a 100644 --- a/ee/spec/models/gitlab_subscription_spec.rb +++ b/ee/spec/models/gitlab_subscription_spec.rb @@ -154,4 +154,79 @@ end end end + + describe '#expired?' do + let(:gitlab_subscription) { create(:gitlab_subscription, end_date: end_date) } + subject { gitlab_subscription.expired? } + + context 'when end_date is expired' do + let(:end_date) { Date.yesterday } + + it { is_expected.to be(true) } + end + + context 'when end_date is not expired' do + let(:end_date) { 1.week.from_now } + + it { is_expected.to be(false) } + end + + context 'when end_date is nil' do + let(:end_date) { nil } + + it { is_expected.to be(false) } + end + end + + describe '#has_a_paid_hosted_plan?' do + using RSpec::Parameterized::TableSyntax + + let(:subscription) { build(:gitlab_subscription) } + + where(:plan_name, :seats, :hosted, :result) do + 'bronze' | 0 | true | false + 'bronze' | 1 | true | true + 'bronze' | 1 | false | false + 'silver' | 1 | true | true + 'early_adopter' | 1 | true | false + end + + with_them do + before do + plan = build(:plan, name: plan_name) + allow(subscription).to receive(:hosted?).and_return(hosted) + subscription.assign_attributes(hosted_plan: plan, seats: seats) + end + + it 'returns true if subscription has a paid hosted plan' do + expect(subscription.has_a_paid_hosted_plan?).to eq(result) + end + end + end + + describe '#upgradable?' do + using RSpec::Parameterized::TableSyntax + + let(:subscription) { build(:gitlab_subscription) } + + where(:plan_name, :paid_hosted_plan, :expired, :result) do + 'bronze' | true | false | true + 'bronze' | true | true | false + 'silver' | true | false | true + 'gold' | true | false | false + end + + with_them do + before do + plan = build(:plan, name: plan_name) + allow(subscription).to receive(:expired?) { expired } + allow(subscription).to receive(:has_a_paid_hosted_plan?) { paid_hosted_plan } + subscription.assign_attributes(hosted_plan: plan) + end + + it 'returns true if subscription is upgradable' do + expect(subscription.upgradable?).to eq(result) + end + end + end end diff --git a/ee/spec/requests/api/namespaces_spec.rb b/ee/spec/requests/api/namespaces_spec.rb index edb437eac5618..93737897b652b 100644 --- a/ee/spec/requests/api/namespaces_spec.rb +++ b/ee/spec/requests/api/namespaces_spec.rb @@ -253,10 +253,11 @@ def do_get(current_user) do_get(owner) expect(json_response.keys).to match_array(%w[plan usage billing]) - expect(json_response['plan'].keys).to match_array(%w[name code trial]) + expect(json_response['plan'].keys).to match_array(%w[name code trial upgradable]) expect(json_response['plan']['name']).to eq('Silver') expect(json_response['plan']['code']).to eq('silver') expect(json_response['plan']['trial']).to eq(false) + expect(json_response['plan']['upgradable']).to eq(true) expect(json_response['usage'].keys).to match_array(%w[seats_in_subscription seats_in_use max_seats_used seats_owed]) expect(json_response['billing'].keys).to match_array(%w[subscription_start_date subscription_end_date trial_ends_on]) end diff --git a/locale/gitlab.pot b/locale/gitlab.pot index ef4c33cfc33e4..40eabe05b1ac6 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -2149,7 +2149,7 @@ msgstr "" msgid "Billing" msgstr "" -msgid "BillingPlans|%{group_name} is currently on the %{plan_link} plan." +msgid "BillingPlans|%{group_name} is currently using the %{plan_link} plan." msgstr "" msgid "BillingPlans|@%{user_name} you are currently on the %{plan_link} plan." @@ -2158,6 +2158,9 @@ msgstr "" msgid "BillingPlans|Automatic downgrade and upgrade to some plans is currently not available." msgstr "" +msgid "BillingPlans|Contact Support" +msgstr "" + msgid "BillingPlans|Current plan" msgstr "" @@ -2167,10 +2170,13 @@ msgstr "" msgid "BillingPlans|Downgrade" msgstr "" +msgid "BillingPlans|If you would like to downgrade your plan please %{support_link}." +msgstr "" + msgid "BillingPlans|Learn more about each plan by reading our %{faq_link}, or start a free 30-day trial of GitLab.com Gold." msgstr "" -msgid "BillingPlans|Learn more about each plan by reading our %{faq_link}." +msgid "BillingPlans|Learn more about each plan by visiting our %{pricing_page_link}." msgstr "" msgid "BillingPlans|Manage plan" @@ -2179,6 +2185,9 @@ msgstr "" msgid "BillingPlans|Please contact %{customer_support_link} in that case." msgstr "" +msgid "BillingPlans|Pricing page" +msgstr "" + msgid "BillingPlans|See all %{plan_name} features" msgstr "" @@ -14273,9 +14282,6 @@ msgstr "" msgid "SubscriptionTable|GitLab allows you to continue using your subscription even if you exceed the number of seats you purchased. You will be required to pay for these seats upon renewal." msgstr "" -msgid "SubscriptionTable|GitLab.com %{planName} %{suffix}" -msgstr "" - msgid "SubscriptionTable|Last invoice" msgstr "" -- GitLab