Commit e2953bd3 authored by Oswaldo Ferreira's avatar Oswaldo Ferreira Committed by Paul Slaughter

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.
parent 56a10f02
......@@ -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>
......@@ -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>
......
......@@ -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,
},
});
},
......
......@@ -8,6 +8,7 @@ export default () => ({
code: null,
name: null,
trial: false,
upgradable: false,
},
tables: {
free: {
......
......@@ -28,4 +28,20 @@ module BillingPlansHelper
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
......@@ -36,6 +36,18 @@ class GitlabSubscription < ApplicationRecord
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
......
# 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
......
- 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 }
......@@ -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.")
......
- 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 }
---
title: Add an Upgrade button to Group's billings page
merge_request: 14849
author:
type: added
......@@ -682,6 +682,7 @@ module EE
expose :plan_name, as: :code
expose :plan_title, as: :name
expose :trial
expose :upgradable?, as: :upgradable
end
expose :usage do
......
......@@ -141,7 +141,7 @@ describe 'Billing plan pages', :feature do
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 @@ describe 'Billing plan pages', :feature do
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
......
......@@ -7,6 +7,10 @@ describe 'Groups > Billing', :js do
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 @@ describe 'Groups > Billing', :js do
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 @@ describe 'Groups > Billing', :js do
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
......@@ -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": [
......
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`SubscriptionApp component on creation matches the snapshot 1`] = `
<subscriptiontable-stub
namespacename="bronze"
planupgradehref="/url"
/>
`;
// 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>
`;
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,
});
});
});
});
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();
});
},
);
});
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',
},
},
};
// 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",
},
]
`;
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(() => {
......
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();
});
});
});
......@@ -14,4 +14,42 @@ describe BillingPlansHelper do
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
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);
});
});
});
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;
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();
});
});
});
......@@ -154,4 +154,79 @@ describe GitlabSubscription do
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
......@@ -253,10 +253,11 @@ describe API::Namespaces do
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
......
......@@ -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 ""
......
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