Commit b2e3539c authored by Bob Van Landuyt's avatar Bob Van Landuyt

Merge branch 'vs/saas-qrtly-recon-alert' into 'master'

Add qrtly reconciliation alert to SaaS [RUN ALL RSPEC] [RUN AS-IF-FOSS]

See merge request gitlab-org/gitlab!63772
parents 989ea960 db0658cd
......@@ -3,15 +3,14 @@
- billable_users_url = help_page_path('subscriptions/self_managed/index', anchor: 'billable-users')
- billable_users_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer nofollow">'.html_safe % { url: billable_users_url }
= render_if_exists 'shared/qrtly_reconciliation_alert'
- if @notices
- @notices.each do |notice|
.js-vue-alert{ 'v-cloak': true, data: { variant: notice[:type],
dismissible: true.to_s } }
= notice[:message].html_safe
- if Gitlab.ee? && display_upcoming_reconciliation_alert?
#js-qrtly-reconciliation-alert{ data: upcoming_reconciliation_hash }
- if @license.present?
.license-panel.gl-mt-5
= render_if_exists 'admin/licenses/summary'
......
......@@ -6,6 +6,8 @@
- if show_thanks_for_purchase_banner?
= render_if_exists 'shared/thanks_for_purchase_banner', plan_title: plan_title, quantity: params[:purchased_quantity].to_i
= render_if_exists 'shared/qrtly_reconciliation_alert', group: @group
- if show_invite_banner?(@group)
= content_for :group_invite_members_banner do
.container-fluid.container-limited{ class: "gl-pb-2! gl-pt-6! #{@content_class}" }
......
......@@ -19,6 +19,7 @@
= render_if_exists "layouts/header/ee_subscribable_banner"
= render_if_exists "shared/namespace_storage_limit_alert"
= render_if_exists "shared/new_user_signups_cap_reached_alert"
= yield :page_level_alert
= yield :customize_homepage_banner
- unless @hide_breadcrumbs
= render "layouts/nav/breadcrumbs"
......
<script>
import { GlAlert, GlSprintf } from '@gitlab/ui';
import Cookie from 'js-cookie';
import { helpPagePath } from '~/helpers/help_page_helper';
import { i18n } from 'ee/billings/qrtly_reconciliation/constants';
import { formatDate, getDayDifference } from '~/lib/utils/datetime_utility';
import { s__, __, sprintf } from '~/locale';
const i18n = {
title: s__('Admin|Quarterly reconcilliation will occur on %{qrtlyDate}'),
description: s__(`Admin|The number of maximum users for your instance
is currently exceeding the number of users in license.
On %{qrtlyDate}, GitLab will process a quarterly reconciliation
and automatically bill you a prorated amount for the overage.
There is no action needed from you. If you have a credit card on file,
it will be charged. Otherwise, you will receive an invoice.`),
learnMore: s__('Admin|Learn more about quarterly reconcilliation'),
contactSupport: __('Contact support'),
};
const CONTACT_SUPPORT_URL = 'https://about.gitlab.com/support/#contact-support';
const qrtlyReconciliationHelpPageUrl = helpPagePath('subscriptions/self_managed/index', {
anchor: 'quarterly-subscription-reconciliation',
});
import { sprintf } from '~/locale';
export default {
name: 'QrtlyReconciliationAlert',
......@@ -38,6 +20,11 @@ export default {
type: String,
required: true,
},
dotCom: {
type: Boolean,
required: false,
default: false,
},
},
computed: {
alertTitle() {
......@@ -46,6 +33,11 @@ export default {
formattedDate() {
return formatDate(this.date, 'isoDate');
},
description() {
return this.dotCom
? this.$options.i18n.description.dotCom
: this.$options.i18n.description.ee;
},
},
methods: {
handleDismiss() {
......@@ -55,8 +47,6 @@ export default {
},
},
i18n,
CONTACT_SUPPORT_URL,
qrtlyReconciliationHelpPageUrl,
};
</script>
......@@ -65,13 +55,13 @@ export default {
data-testid="qrtly-reconciliation-alert"
variant="info"
:title="alertTitle"
:primary-button-text="$options.i18n.learnMore"
:primary-button-link="$options.qrtlyReconciliationHelpPageUrl"
:secondary-button-text="$options.i18n.contactSupport"
:secondary-button-link="$options.CONTACT_SUPPORT_URL"
:primary-button-text="$options.i18n.buttons.primary.text"
:primary-button-link="$options.i18n.buttons.primary.link"
:secondary-button-text="$options.i18n.buttons.secondary.text"
:secondary-button-link="$options.i18n.buttons.secondary.link"
@dismiss="handleDismiss"
>
<gl-sprintf :message="$options.i18n.description">
<gl-sprintf :message="description">
<template #qrtlyDate>
<span>{{ formattedDate }}</span>
</template>
......
import { helpPagePath } from '~/helpers/help_page_helper';
import { s__, __ } from '~/locale';
const qrtlyReconciliationHelpPageUrl = helpPagePath('subscriptions/self_managed/index', {
anchor: 'quarterly-subscription-reconciliation',
});
export const i18n = {
title: s__('Admin|Quarterly reconciliation will occur on %{qrtlyDate}'),
description: {
ee: s__(`Admin|The number of maximum users for your instance
is currently exceeding the number of users in license.
On %{qrtlyDate}, GitLab will process a quarterly reconciliation
and automatically bill you a prorated amount for the overage.
There is no action needed from you. If you have a credit card on file,
it will be charged. Otherwise, you will receive an invoice.`),
dotCom: s__(`Admin|The number of max seats used for your namespace is currently
exceeding the number of seats in your subscription.
On %{qrtlyDate}, GitLab will process a quarterly reconciliation and
automatically bill you a prorated amount for the overage.
There is no action needed from you. If you have a credit card on file, it will be charged.
Otherwise, you will receive an invoice.`),
},
buttons: {
primary: {
text: s__('Admin|Learn more about quarterly reconciliation'),
link: qrtlyReconciliationHelpPageUrl,
},
secondary: {
text: __('Contact support'),
link: 'https://about.gitlab.com/support/#contact-support',
},
},
};
......@@ -8,7 +8,7 @@ export const initQrtlyReconciliationAlert = (selector = '#js-qrtly-reconciliatio
return false;
}
const { reconciliationDate, cookieKey } = el.dataset;
const { reconciliationDate, cookieKey, dotCom } = el.dataset;
return new Vue({
el,
......@@ -17,6 +17,7 @@ export const initQrtlyReconciliationAlert = (selector = '#js-qrtly-reconciliatio
props: {
date: new Date(reconciliationDate),
cookieKey,
dotCom,
},
});
},
......
import { initQrtlyReconciliationAlert } from 'ee/billings/qrtly_reconciliation/init_qrtly_reconciliation_alert';
import initSubscriptions from 'ee/billings/subscriptions';
import PersistentUserCallout from '~/persistent_user_callout';
PersistentUserCallout.factory(document.querySelector('.js-gold-trial-callout'));
initSubscriptions();
initQrtlyReconciliationAlert();
import initGroupAnalytics from 'ee/analytics/group_analytics/group_analytics_bundle';
import { initQrtlyReconciliationAlert } from 'ee/billings/qrtly_reconciliation/init_qrtly_reconciliation_alert';
import leaveByUrl from '~/namespaces/leave_by_url';
import initGroupDetails from '~/pages/groups/shared/group_details';
import initVueAlerts from '~/vue_alerts';
......@@ -7,3 +8,4 @@ leaveByUrl('group');
initGroupDetails();
initGroupAnalytics();
initVueAlerts();
initQrtlyReconciliationAlert();
......@@ -9,7 +9,8 @@ module GitlabSubscriptions
{
reconciliation_date: entity.next_reconciliation_date.to_s,
cookie_key: entity.cookie_key
cookie_key: entity.cookie_key,
dot_com: Gitlab::CurrentSettings.should_check_namespace_plan?
}
end
......
- page_title _('License')
- if display_upcoming_reconciliation_alert?
#js-qrtly-reconciliation-alert{ data: upcoming_reconciliation_hash }
= render_if_exists 'shared/qrtly_reconciliation_alert'
%h3.page-title
= _('Your License')
......
- page_title _("Billing")
- current_plan = subscription_plan_info(@plans_data, @group.actual_plan_name)
= render_if_exists 'shared/qrtly_reconciliation_alert', group: @group
- if @top_most_group
- top_most_group_plan = subscription_plan_info(@plans_data, @top_most_group.actual_plan_name)
= render 'shared/billings/billing_plan_header', namespace: @group, plan: top_most_group_plan, parent_group: @top_most_group
- else
= render 'shared/billings/billing_plans', plans_data: @plans_data, namespace: @group, current_plan: current_plan
#js-billing-plans{ data: subscription_plan_data_attributes(@group, current_plan) }
- group = local_assigns.fetch(:group, nil)
- return unless display_upcoming_reconciliation_alert?(group)
= content_for :page_level_alert do
.container-fluid.container-limited{ class: "gl-pb-2! gl-pt-6! #{@content_class}" }
#js-qrtly-reconciliation-alert{ data: upcoming_reconciliation_hash(group) }
......@@ -74,30 +74,19 @@ RSpec.describe 'Admin Dashboard' do
end
describe 'qrtly reconciliation alert', :js do
shared_examples 'a visible alert' do
it 'displays an alert' do
expect(page).to have_selector('[data-testid="qrtly-reconciliation-alert"]')
end
end
shared_examples 'a hidden alert' do
it 'does not display an alert' do
expect(page).not_to have_selector('[data-testid="qrtly-reconciliation-alert"]')
end
end
context 'on self-managed' do
before do
allow(Gitlab).to receive(:ee?).and_return(true)
stub_ee_application_setting(should_check_namespace_plan: false)
end
context 'when qrtly reconciliation is available' do
let_it_be(:reconciliation) { create(:upcoming_reconciliation, :self_managed) }
before do
create(:upcoming_reconciliation, :self_managed)
visit(admin_root_path)
end
it_behaves_like 'a visible alert'
it_behaves_like 'a visible dismissible qrtly reconciliation alert'
end
context 'when qrtly reconciliation is not available' do
......@@ -105,7 +94,7 @@ RSpec.describe 'Admin Dashboard' do
visit(admin_root_path)
end
it_behaves_like 'a hidden alert'
it_behaves_like 'a hidden qrtly reconciliation alert'
end
end
end
......
......@@ -85,14 +85,13 @@ RSpec.describe "Admin views license" do
license_history = page.find("#license_history")
highlighted_license_row = license_history.find("[data-testid='license-current']")
expect(highlighted_license_row).to have_content(license.licensee[:name])
expect(highlighted_license_row).to have_content(license.licensee[:email])
expect(highlighted_license_row).to have_content(license.licensee[:company])
expect(highlighted_license_row).to have_content(license.licensee.fetch('Name'))
expect(highlighted_license_row).to have_content(license.licensee.fetch('Email'))
expect(highlighted_license_row).to have_content(license.licensee.fetch('Company'))
expect(highlighted_license_row).to have_content(license.plan.capitalize)
expect(highlighted_license_row).to have_content(I18n.l(license.created_at, format: :with_timezone))
expect(highlighted_license_row).to have_content(I18n.l(license.starts_at))
expect(highlighted_license_row).to have_content(I18n.l(license.expires_at))
expect(highlighted_license_row).to have_content(license.restrictions[:active_user_count])
end
end
end
......@@ -139,61 +138,44 @@ RSpec.describe "Admin views license" do
expect(license_history).to have_css('tbody tr', count: 1)
expect(license_history_row).to have_content(license.licensee[:name])
expect(license_history_row).to have_content(license.licensee[:email])
expect(license_history_row).to have_content(license.licensee[:company])
expect(license_history_row).to have_content(license.licensee.fetch('Name'))
expect(license_history_row).to have_content(license.licensee.fetch('Email'))
expect(license_history_row).to have_content(license.licensee.fetch('Company'))
expect(license_history_row).to have_content(license.plan.capitalize)
expect(license_history_row).to have_content(I18n.l(license.created_at, format: :with_timezone))
expect(license_history_row).to have_content(I18n.l(license.starts_at))
expect(license_history_row).to have_content(I18n.l(license.expires_at))
expect(license_history_row).to have_content(license.restrictions[:active_user_count])
end
end
end
describe 'qrtly reconciliation alert', :js do
shared_examples 'a visible alert' do
it 'displays an alert' do
expect(page).to have_selector('[data-testid="qrtly-reconciliation-alert"]')
end
end
shared_examples 'a hidden alert' do
it 'does not display an alert' do
expect(page).not_to have_selector('[data-testid="qrtly-reconciliation-alert"]')
end
end
context 'on self-managed' do
context 'when qrtly reconciliation is available' do
let_it_be(:reconciliation) { create(:upcoming_reconciliation, :self_managed) }
context 'on dotcom' do
before do
allow(Gitlab).to receive(:com?).and_return(true)
visit(admin_license_path)
end
it_behaves_like 'a hidden alert'
end
context 'on self-managed' do
before do
allow(Gitlab).to receive(:ee?).and_return(true)
it_behaves_like 'a visible dismissible qrtly reconciliation alert'
end
context 'when qrtly reconciliation is available' do
context 'when qrtly reconciliation is not available' do
before do
create(:upcoming_reconciliation, :self_managed)
visit(admin_license_path)
end
it_behaves_like 'a visible alert'
it_behaves_like 'a hidden qrtly reconciliation alert'
end
end
context 'when qrtly reconciliation is not available' do
context 'on dotcom' do
before do
visit(admin_license_path)
end
it_behaves_like 'a hidden alert'
end
it_behaves_like 'a hidden qrtly reconciliation alert'
end
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe 'Billings > Qrtly Reconciliation Alert', :js do
include SubscriptionPortalHelpers
let_it_be(:user) { create(:user) }
let_it_be(:namespace) { create(:group) }
let_it_be(:group_member) { create(:group_member, :owner, group: namespace, user: user) }
let_it_be(:plan) { create(:premium_plan) }
let_it_be(:plans_data) do
Gitlab::Json.parse(File.read(Rails.root.join('ee/spec/fixtures/gitlab_com_plans.json'))).map do |data|
data.deep_symbolize_keys
end
end
let_it_be(:subscription) { create(:gitlab_subscription, namespace: namespace, hosted_plan: plan, seats: 15) }
let_it_be(:page_path) { group_billings_path(namespace) }
before do
stub_ee_application_setting(should_check_namespace_plan: 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)
sign_in(user)
end
context 'when qrtly reconciliation is available' do
before do
create(:upcoming_reconciliation, :saas, namespace: namespace)
visit(page_path)
end
it_behaves_like 'a visible dismissible qrtly reconciliation alert'
end
context 'when qrtly reconciliation is not available' do
before do
visit(page_path)
end
it_behaves_like 'a hidden qrtly reconciliation alert'
end
end
......@@ -48,4 +48,38 @@ RSpec.describe 'Group information', :js, :aggregate_failures do
end
end
end
describe 'qrtly reconciliation alert', :js do
context 'on self-managed' do
before do
visit_page
end
it_behaves_like 'a hidden qrtly reconciliation alert'
end
context 'on dotcom' do
before do
stub_ee_application_setting(should_check_namespace_plan: true)
end
context 'when qrtly reconciliation is available' do
let!(:upcoming_reconciliation) { create(:upcoming_reconciliation, :saas, namespace: group) }
before do
visit_page
end
it_behaves_like 'a visible dismissible qrtly reconciliation alert'
end
context 'when qrtly reconciliation is not available' do
before do
visit_page
end
it_behaves_like 'a hidden qrtly reconciliation alert'
end
end
end
end
import { GlAlert, GlSprintf } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import Cookie from 'js-cookie';
import QrtlyReconciliationAlert from 'ee/billings/qrtly_reconciliation/components/qrtly_reconciliation_alert.vue';
import { i18n } from 'ee/billings/qrtly_reconciliation/constants';
jest.mock('js-cookie', () => ({
set: jest.fn(),
}));
describe('Qrtly Reconciliation Alert', () => {
let wrapper;
const reconciliationDate = new Date('2020-07-10');
const createComponent = (props = {}) => {
return shallowMount(QrtlyReconciliationAlert, {
propsData: {
cookieKey: 'key',
date: reconciliationDate,
...props,
},
});
};
const findAlert = () => wrapper.find(GlAlert);
beforeEach(() => {
wrapper = createComponent();
});
afterEach(() => {
wrapper.destroy();
});
describe('Rendering', () => {
it('renders alert title with date', () => {
expect(findAlert().attributes('title')).toContain(`occur on 2020-07-10`);
});
it('has the correct link to the help page', () => {
expect(findAlert().attributes('primarybuttonlink')).toBe(
'/help/subscriptions/self_managed/index#quarterly-subscription-reconciliation',
);
});
it('has the correct link to contact support', () => {
expect(findAlert().attributes('secondarybuttonlink')).toBe(i18n.buttons.secondary.link);
});
it('has the correct description for EE', () => {
expect(wrapper.findComponent(GlSprintf).attributes('message')).toContain(i18n.description.ee);
});
describe('dotcom', () => {
beforeEach(() => {
wrapper = createComponent({ dotCom: true });
});
it('has the correct description', () => {
expect(wrapper.findComponent(GlSprintf).attributes('message')).toContain(
i18n.description.dotCom,
);
});
});
});
describe('methods', () => {
beforeEach(() => {
wrapper = createComponent();
});
it('sets the cookie on dismis', () => {
findAlert().vm.$emit('dismiss');
expect(Cookie.set).toHaveBeenCalledTimes(1);
expect(Cookie.set).toHaveBeenCalledWith('key', true, { expires: 4 });
});
});
});
......@@ -26,7 +26,8 @@ RSpec.describe GitlabSubscriptions::UpcomingReconciliationHelper do
expect(helper.display_upcoming_reconciliation_alert?(namespace)).to eq(true)
expect(helper.upcoming_reconciliation_hash(namespace)).to eq(
reconciliation_date: upcoming_reconciliation.next_reconciliation_date.to_s,
cookie_key: cookie_key
cookie_key: cookie_key,
dot_com: true
)
end
......@@ -46,7 +47,8 @@ RSpec.describe GitlabSubscriptions::UpcomingReconciliationHelper do
expect(helper.display_upcoming_reconciliation_alert?(group)).to eq(true)
expect(helper.upcoming_reconciliation_hash(group)).to eq(
reconciliation_date: upcoming_reconciliation2.next_reconciliation_date.to_s,
cookie_key: cookie_key
cookie_key: cookie_key,
dot_com: true
)
end
end
......@@ -97,7 +99,8 @@ RSpec.describe GitlabSubscriptions::UpcomingReconciliationHelper do
expect(helper.display_upcoming_reconciliation_alert?).to eq(true)
expect(helper.upcoming_reconciliation_hash).to eq(
reconciliation_date: upcoming_reconciliation.next_reconciliation_date.to_s,
cookie_key: cookie_key
cookie_key: cookie_key,
dot_com: false
)
end
......@@ -117,9 +120,7 @@ RSpec.describe GitlabSubscriptions::UpcomingReconciliationHelper do
expect(helper.display_upcoming_reconciliation_alert?).to eq(false)
expect(helper.upcoming_reconciliation_hash).to eq({})
end
end
context 'when instance has paid namespaces (ex: gitlab.com)' do
it 'returns false and empty hash' do
stub_application_setting(check_namespace_plan: true)
enable_admin_mode!(user)
......
......@@ -55,7 +55,7 @@ RSpec.describe GitlabSubscriptions::UpcomingReconciliationEntity do
let(:namespace) { build(:namespace, owner: user) }
before do
allow(::Gitlab).to receive(:com?).and_return(true)
stub_ee_application_setting(should_check_namespace_plan: true)
allow(namespace).to receive(:id).and_return(2)
allow(GitlabSubscriptions::UpcomingReconciliation).to receive(:next).with(namespace.id).and_return(upcoming_reconciliation)
......
# frozen_string_literal: true
RSpec.shared_examples 'a visible dismissible qrtly reconciliation alert' do
shared_examples 'a visible alert' do
it 'displays an alert' do
expect(page).to have_selector('[data-testid="qrtly-reconciliation-alert"]')
end
end
context 'when dismissed' do
before do
within '[data-testid="qrtly-reconciliation-alert"]' do
click_button 'Dismiss'
end
end
it_behaves_like 'a hidden qrtly reconciliation alert'
context 'when visiting again' do
before do
visit current_path
end
it_behaves_like 'a hidden qrtly reconciliation alert'
end
end
end
RSpec.shared_examples 'a hidden qrtly reconciliation alert' do
it 'does not display an alert' do
expect(page).not_to have_selector('[data-testid="qrtly-reconciliation-alert"]')
end
end
......@@ -2806,13 +2806,16 @@ msgstr ""
msgid "Admin|Admin notes"
msgstr ""
msgid "Admin|Learn more about quarterly reconcilliation"
msgid "Admin|Learn more about quarterly reconciliation"
msgstr ""
msgid "Admin|Note"
msgstr ""
msgid "Admin|Quarterly reconcilliation will occur on %{qrtlyDate}"
msgid "Admin|Quarterly reconciliation will occur on %{qrtlyDate}"
msgstr ""
msgid "Admin|The number of max seats used for your namespace is currently exceeding the number of seats in your subscription. On %{qrtlyDate}, GitLab will process a quarterly reconciliation and automatically bill you a prorated amount for the overage. There is no action needed from you. If you have a credit card on file, it will be charged. Otherwise, you will receive an invoice."
msgstr ""
msgid "Admin|The number of maximum users for your instance is currently exceeding the number of users in license. On %{qrtlyDate}, GitLab will process a quarterly reconciliation and automatically bill you a prorated amount for the overage. There is no action needed from you. If you have a credit card on file, it will be charged. Otherwise, you will receive an invoice."
......
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