Commit 11e58268 authored by Kushal Pandya's avatar Kushal Pandya

Merge branch 'vs/fix-summary-addon-purchase' into 'master'

Fix summary section when purchasing an addon

See merge request gitlab-org/gitlab!65657
parents 61123117 eafc8465
...@@ -23,7 +23,6 @@ export default { ...@@ -23,7 +23,6 @@ export default {
emptySvg, emptySvg,
data() { data() {
return { return {
plans: null,
hasError: false, hasError: false,
}; };
}, },
...@@ -59,10 +58,10 @@ export default { ...@@ -59,10 +58,10 @@ export default {
/> />
<step-order-app v-else-if="!$apollo.loading"> <step-order-app v-else-if="!$apollo.loading">
<template #checkout> <template #checkout>
<checkout :plans="plans" /> <checkout :plan="plans[0]" />
</template> </template>
<template #order-summary> <template #order-summary>
<order-summary :plans="plans" /> <order-summary :plan="plans[0]" />
</template> </template>
</step-order-app> </step-order-app>
</template> </template>
<script> <script>
import updateState from 'ee/subscriptions/graphql/mutations/update_state.mutation.graphql';
import { GENERAL_ERROR_MESSAGE } from 'ee/vue_shared/purchase_flow/constants';
import createFlash from '~/flash';
import { s__ } from '~/locale'; import { s__ } from '~/locale';
import AddonPurchaseDetails from './checkout/addon_purchase_details.vue'; import AddonPurchaseDetails from './checkout/addon_purchase_details.vue';
import BillingAddress from './checkout/billing_address.vue'; import BillingAddress from './checkout/billing_address.vue';
...@@ -8,11 +11,28 @@ import PaymentMethod from './checkout/payment_method.vue'; ...@@ -8,11 +11,28 @@ import PaymentMethod from './checkout/payment_method.vue';
export default { export default {
components: { AddonPurchaseDetails, BillingAddress, PaymentMethod, ConfirmOrder }, components: { AddonPurchaseDetails, BillingAddress, PaymentMethod, ConfirmOrder },
props: { props: {
plans: { plan: {
type: Array, type: Object,
required: true, required: true,
}, },
}, },
mounted() {
this.updateSelectedPlanId(this.plan.id);
},
methods: {
updateSelectedPlanId(planId) {
this.$apollo
.mutate({
mutation: updateState,
variables: {
input: { selectedPlanId: planId },
},
})
.catch((error) => {
createFlash({ message: GENERAL_ERROR_MESSAGE, error, captureError: true });
});
},
},
i18n: { i18n: {
checkout: s__('Checkout|Checkout'), checkout: s__('Checkout|Checkout'),
}, },
...@@ -22,7 +42,7 @@ export default { ...@@ -22,7 +42,7 @@ export default {
<div class="checkout gl-display-flex gl-flex-direction-column gl-align-items-center"> <div class="checkout gl-display-flex gl-flex-direction-column gl-align-items-center">
<div class="flash-container"></div> <div class="flash-container"></div>
<h2 class="gl-mt-6 gl-mb-7 gl-mb-lg-5">{{ $options.i18n.checkout }}</h2> <h2 class="gl-mt-6 gl-mb-7 gl-mb-lg-5">{{ $options.i18n.checkout }}</h2>
<addon-purchase-details :plans="plans" /> <addon-purchase-details :plan="plan" />
<billing-address /> <billing-address />
<payment-method /> <payment-method />
<confirm-order /> <confirm-order />
......
<script> <script>
import { GlIcon, GlCollapse, GlCollapseToggleDirective } from '@gitlab/ui'; import { GlIcon, GlCollapse, GlCollapseToggleDirective } from '@gitlab/ui';
import stateQuery from 'ee/subscriptions/graphql/queries/state.query.graphql'; import stateQuery from 'ee/subscriptions/graphql/queries/state.query.graphql';
import { TAX_RATE, NEW_GROUP } from 'ee/subscriptions/new/constants'; import { TAX_RATE } from 'ee/subscriptions/new/constants';
import formattingMixins from 'ee/subscriptions/new/formatting_mixins'; import formattingMixins from 'ee/subscriptions/new/formatting_mixins';
import { sprintf, s__ } from '~/locale'; import { sprintf, s__ } from '~/locale';
import SummaryDetails from './order_summary/summary_details.vue'; import SummaryDetails from './order_summary/summary_details.vue';
...@@ -17,8 +17,8 @@ export default { ...@@ -17,8 +17,8 @@ export default {
}, },
mixins: [formattingMixins], mixins: [formattingMixins],
props: { props: {
plans: { plan: {
type: Array, type: Object,
required: true, required: true,
}, },
}, },
...@@ -27,12 +27,8 @@ export default { ...@@ -27,12 +27,8 @@ export default {
query: stateQuery, query: stateQuery,
manual: true, manual: true,
result({ data }) { result({ data }) {
this.subscription = data.subscription;
this.namespaces = data.namespaces; this.namespaces = data.namespaces;
this.isSetupForCompany = data.isSetupForCompany; this.subscription = data.subscription;
this.fullName = data.fullName;
this.customer = data.customer;
this.selectedPlanId = data.selectedPlanId;
}, },
}, },
}, },
...@@ -40,22 +36,15 @@ export default { ...@@ -40,22 +36,15 @@ export default {
return { return {
subscription: {}, subscription: {},
namespaces: [], namespaces: [],
isSetupForCompany: false,
isBottomSummaryVisible: false, isBottomSummaryVisible: false,
fullName: null,
customer: {},
selectedPlanId: null,
}; };
}, },
computed: { computed: {
selectedPlan() {
return this.plans.find((plan) => plan.id === this.selectedPlanId);
},
selectedPlanPrice() { selectedPlanPrice() {
return this.selectedPlan.pricePerYear; return this.plan.pricePerYear;
}, },
selectedGroup() { selectedGroup() {
return this.namespaces.find((group) => group.id === this.subscription.namespaceId); return this.namespaces.find((group) => group.id === Number(this.subscription.namespaceId));
}, },
totalExVat() { totalExVat() {
return this.subscription.quantity * this.selectedPlanPrice; return this.subscription.quantity * this.selectedPlanPrice;
...@@ -66,46 +55,21 @@ export default { ...@@ -66,46 +55,21 @@ export default {
totalAmount() { totalAmount() {
return this.totalExVat + this.vat; return this.totalExVat + this.vat;
}, },
usersPresent() { quantityPresent() {
return this.subscription.quantity > 0; return this.subscription.quantity > 0;
}, },
isGroupSelected() { namespaceName() {
return this.subscription.namespaceId && this.subscription.namespaceId !== NEW_GROUP; return this.selectedGroup.name;
},
isSelectedGroupPresent() {
return (
this.isGroupSelected &&
this.namespaces.some((namespace) => namespace.id === this.subscription.namespaceId)
);
},
name() {
if (this.isSetupForCompany && this.customer.company) {
return this.customer.company;
}
if (this.isGroupSelected && this.isSelectedGroupPresent) {
return this.selectedGroup.name;
}
if (this.isSetupForCompany) {
return s__('Checkout|Your organization');
}
return this.fullName;
}, },
titleWithName() { titleWithName() {
return sprintf(this.$options.i18n.title, { name: this.name }); return sprintf(this.$options.i18n.title, { name: this.namespaceName });
}, },
isVisible() { isVisible() {
return ( return !this.$apollo.loading;
!this.$apollo.loading &&
(!this.isGroupSelected || this.isSelectedGroupPresent) &&
this.selectedPlan
);
}, },
}, },
i18n: { i18n: {
title: s__("Checkout|%{name}'s GitLab subscription"), title: s__("Checkout|%{name}'s CI minutes"),
}, },
taxRate: TAX_RATE, taxRate: TAX_RATE,
}; };
...@@ -121,20 +85,22 @@ export default { ...@@ -121,20 +85,22 @@ export default {
<div class="d-flex"> <div class="d-flex">
<gl-icon v-if="isBottomSummaryVisible" name="chevron-down" /> <gl-icon v-if="isBottomSummaryVisible" name="chevron-down" />
<gl-icon v-else name="chevron-right" /> <gl-icon v-else name="chevron-right" />
<div>{{ titleWithName }}</div> <div data-testid="title">{{ titleWithName }}</div>
</div>
<div class="gl-ml-3" data-testid="amount">
{{ formatAmount(totalAmount, quantityPresent) }}
</div> </div>
<div class="gl-ml-3">{{ formatAmount(totalAmount, usersPresent) }}</div>
</h4> </h4>
</div> </div>
<gl-collapse id="summary-details" v-model="isBottomSummaryVisible"> <gl-collapse id="summary-details" v-model="isBottomSummaryVisible">
<summary-details <summary-details
:vat="vat" :vat="vat"
:total-ex-vat="totalExVat" :total-ex-vat="totalExVat"
:users-present="usersPresent" :quantity-present="quantityPresent"
:selected-plan-text="selectedPlan.name" :selected-plan-text="plan.name"
:selected-plan-price="selectedPlanPrice" :selected-plan-price="selectedPlanPrice"
:total-amount="totalAmount" :total-amount="totalAmount"
:number-of-users="subscription.quantity" :quantity="subscription.quantity"
:tax-rate="$options.taxRate" :tax-rate="$options.taxRate"
/> />
</gl-collapse> </gl-collapse>
...@@ -148,11 +114,11 @@ export default { ...@@ -148,11 +114,11 @@ export default {
<summary-details <summary-details
:vat="vat" :vat="vat"
:total-ex-vat="totalExVat" :total-ex-vat="totalExVat"
:users-present="usersPresent" :quantity-present="quantityPresent"
:selected-plan-text="selectedPlan.name" :selected-plan-text="plan.name"
:selected-plan-price="selectedPlanPrice" :selected-plan-price="selectedPlanPrice"
:total-amount="totalAmount" :total-amount="totalAmount"
:number-of-users="subscription.quantity" :quantity="subscription.quantity"
:tax-rate="$options.taxRate" :tax-rate="$options.taxRate"
/> />
</div> </div>
......
...@@ -13,10 +13,6 @@ export default { ...@@ -13,10 +13,6 @@ export default {
type: Number, type: Number,
required: true, required: true,
}, },
usersPresent: {
type: Boolean,
required: true,
},
selectedPlanText: { selectedPlanText: {
type: String, type: String,
required: true, required: true,
...@@ -29,7 +25,7 @@ export default { ...@@ -29,7 +25,7 @@ export default {
type: Number, type: Number,
required: true, required: true,
}, },
numberOfUsers: { quantity: {
type: Number, type: Number,
required: true, required: true,
}, },
...@@ -38,6 +34,10 @@ export default { ...@@ -38,6 +34,10 @@ export default {
required: false, required: false,
default: null, default: null,
}, },
purchaseHasExpiration: {
type: Boolean,
required: false,
},
}, },
data() { data() {
return { return {
...@@ -51,8 +51,8 @@ export default { ...@@ -51,8 +51,8 @@ export default {
}, },
i18n: { i18n: {
selectedPlanText: s__('Checkout|%{selectedPlanText} plan'), selectedPlanText: s__('Checkout|%{selectedPlanText} plan'),
numberOfUsers: s__('Checkout|(x%{numberOfUsers})'), quantity: s__('Checkout|(x%{quantity})'),
pricePerUserPerYear: s__('Checkout|$%{selectedPlanPrice} per user per year'), pricePerUnitPerYear: s__('Checkout|$%{selectedPlanPrice} per pack per year'),
dates: s__('Checkout|%{startDate} - %{endDate}'), dates: s__('Checkout|%{startDate} - %{endDate}'),
subtotal: s__('Checkout|Subtotal'), subtotal: s__('Checkout|Subtotal'),
tax: s__('Checkout|Tax'), tax: s__('Checkout|Tax'),
...@@ -63,22 +63,22 @@ export default { ...@@ -63,22 +63,22 @@ export default {
<template> <template>
<div> <div>
<div class="d-flex justify-content-between bold gl-mt-3 gl-mb-3"> <div class="d-flex justify-content-between bold gl-mt-3 gl-mb-3">
<div class="js-selected-plan"> <div data-testid="selected-plan">
{{ sprintf($options.i18n.selectedPlanText, { selectedPlanText }) }} {{ sprintf($options.i18n.selectedPlanText, { selectedPlanText }) }}
<span v-if="usersPresent" class="js-number-of-users">{{ <span v-if="quantity" data-testid="quantity">{{
sprintf($options.i18n.numberOfUsers, { numberOfUsers }) sprintf($options.i18n.quantity, { quantity })
}}</span> }}</span>
</div> </div>
<div class="js-amount">{{ formatAmount(totalExVat, usersPresent) }}</div> <div data-testid="amount">{{ formatAmount(totalExVat, quantity > 0) }}</div>
</div> </div>
<div class="text-secondary js-per-user"> <div class="text-secondary" data-testid="price-per-unit">
{{ {{
sprintf($options.i18n.pricePerUserPerYear, { sprintf($options.i18n.pricePerUnitPerYear, {
selectedPlanPrice: selectedPlanPrice.toLocaleString(), selectedPlanPrice: selectedPlanPrice.toLocaleString(),
}) })
}} }}
</div> </div>
<div class="text-secondary js-dates"> <div v-if="purchaseHasExpiration" class="text-secondary" data-testid="subscription-period">
{{ {{
sprintf($options.i18n.dates, { sprintf($options.i18n.dates, {
startDate: formatDate(startDate), startDate: formatDate(startDate),
...@@ -90,17 +90,17 @@ export default { ...@@ -90,17 +90,17 @@ export default {
<div class="border-bottom gl-mt-3 gl-mb-3"></div> <div class="border-bottom gl-mt-3 gl-mb-3"></div>
<div class="d-flex justify-content-between text-secondary"> <div class="d-flex justify-content-between text-secondary">
<div>{{ $options.i18n.subtotal }}</div> <div>{{ $options.i18n.subtotal }}</div>
<div class="js-total-ex-vat">{{ formatAmount(totalExVat, usersPresent) }}</div> <div data-testid="total-ex-vat">{{ formatAmount(totalExVat, quantity > 0) }}</div>
</div> </div>
<div class="d-flex justify-content-between text-secondary"> <div class="d-flex justify-content-between text-secondary">
<div>{{ $options.i18n.tax }}</div> <div>{{ $options.i18n.tax }}</div>
<div class="js-vat">{{ formatAmount(vat, usersPresent) }}</div> <div data-testid="vat">{{ formatAmount(vat, quantity > 0) }}</div>
</div> </div>
</div> </div>
<div class="border-bottom gl-mt-3 gl-mb-3"></div> <div class="border-bottom gl-mt-3 gl-mb-3"></div>
<div class="d-flex justify-content-between bold gl-font-lg"> <div class="d-flex justify-content-between bold gl-font-lg">
<div>{{ $options.i18n.total }}</div> <div>{{ $options.i18n.total }}</div>
<div class="js-total-amount">{{ formatAmount(totalAmount, usersPresent) }}</div> <div data-itestid="total-amount">{{ formatAmount(totalAmount, quantity > 0) }}</div>
</div> </div>
</div> </div>
</template> </template>
import { STEPS } from 'ee/subscriptions/constants'; import { STEPS } from 'ee/subscriptions/constants';
import stateQuery from 'ee/subscriptions/graphql/queries/state.query.graphql'; import stateQuery from 'ee/subscriptions/graphql/queries/state.query.graphql';
import { convertObjectPropsToCamelCase, parseBoolean } from '~/lib/utils/common_utils'; import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
function arrayToGraphqlArray(arr, typename) { function arrayToGraphqlArray(arr, typename) {
return Array.from(arr, (item) => { return Array.from(arr, (item) => {
...@@ -11,23 +11,21 @@ function arrayToGraphqlArray(arr, typename) { ...@@ -11,23 +11,21 @@ function arrayToGraphqlArray(arr, typename) {
} }
export function writeInitialDataToApolloCache(apolloProvider, dataset) { export function writeInitialDataToApolloCache(apolloProvider, dataset) {
const { groupData, newUser, setupForCompany, fullName, planId = null } = dataset; const { groupData, namespaceId } = dataset;
// eslint-disable-next-line @gitlab/require-i18n-strings // eslint-disable-next-line @gitlab/require-i18n-strings
const namespaces = arrayToGraphqlArray(JSON.parse(groupData), 'Namespace'); const namespaces = arrayToGraphqlArray(JSON.parse(groupData), 'Namespace');
const isNewUser = parseBoolean(newUser);
const isSetupForCompany = parseBoolean(setupForCompany) || !isNewUser;
apolloProvider.clients.defaultClient.cache.writeQuery({ apolloProvider.clients.defaultClient.cache.writeQuery({
query: stateQuery, query: stateQuery,
data: { data: {
isNewUser, isNewUser: false,
isSetupForCompany, fullName: null,
isSetupForCompany: false,
selectedPlanId: null,
namespaces, namespaces,
fullName,
selectedPlanId: planId,
subscription: { subscription: {
quantity: 1, quantity: 1,
namespaceId: null, namespaceId,
// eslint-disable-next-line @gitlab/require-i18n-strings // eslint-disable-next-line @gitlab/require-i18n-strings
__typename: 'Subscription', __typename: 'Subscription',
}, },
......
...@@ -108,7 +108,7 @@ export default { ...@@ -108,7 +108,7 @@ export default {
}; };
</script> </script>
<template> <template>
<div class="mb-3 mb-lg-5"> <div class="mb-3 mb-lg-5 gl-w-full">
<step-header :title="title" :is-finished="isFinished" /> <step-header :title="title" :is-finished="isFinished" />
<div :class="['card', snakeCasedStep]"> <div :class="['card', snakeCasedStep]">
<div v-show="isActive" @keyup.enter="nextStep"> <div v-show="isActive" @keyup.enter="nextStep">
......
...@@ -2,9 +2,9 @@ ...@@ -2,9 +2,9 @@
class SubscriptionsController < ApplicationController class SubscriptionsController < ApplicationController
layout 'checkout' layout 'checkout'
skip_before_action :authenticate_user!, only: [:new, :buy_minutes] skip_before_action :authenticate_user!, only: [:new]
before_action :load_eligible_groups, only: %i[new buy_minutes] before_action :load_eligible_groups, only: :new
feature_category :purchase feature_category :purchase
...@@ -30,8 +30,11 @@ class SubscriptionsController < ApplicationController ...@@ -30,8 +30,11 @@ class SubscriptionsController < ApplicationController
end end
def buy_minutes def buy_minutes
render_404 unless Feature.enabled?(:new_route_ci_minutes_purchase, default_enabled: :yaml) return render_404 unless Feature.enabled?(:new_route_ci_minutes_purchase, default_enabled: :yaml)
redirect_unauthenticated_user
@group = find_group
return render_404 if @group.nil?
end end
def payment_form def payment_form
......
...@@ -16,6 +16,14 @@ module SubscriptionsHelper ...@@ -16,6 +16,14 @@ module SubscriptionsHelper
} }
end end
def addon_data(group)
{
group_data: [present_group(group)].to_json,
namespace_id: params[:selected_group],
source: params[:source]
}
end
def plan_title def plan_title
strong_memoize(:plan_title) do strong_memoize(:plan_title) do
plan = subscription_available_plans.find { |plan| plan[:id] == params[:plan_id] } plan = subscription_available_plans.find { |plan| plan[:id] == params[:plan_id] }
...@@ -45,13 +53,15 @@ module SubscriptionsHelper ...@@ -45,13 +53,15 @@ module SubscriptionsHelper
end end
def present_groups(groups) def present_groups(groups)
groups.map do |namespace| groups.map { |namespace| present_group(namespace) }
{ end
id: namespace.id,
name: namespace.name, def present_group(namespace)
users: namespace.member_count, {
guests: namespace.guest_count id: namespace.id,
} name: namespace.name,
end users: namespace.member_count,
guests: namespace.guest_count
}
end end
end end
- page_title _('Buy CI Minutes') - page_title _('Buy CI Minutes')
#js-buy-minutes{ data: subscription_data(@eligible_groups) } #js-buy-minutes{ data: addon_data(@group) }
...@@ -65,19 +65,18 @@ RSpec.describe SubscriptionsController do ...@@ -65,19 +65,18 @@ RSpec.describe SubscriptionsController do
end end
describe 'GET #buy_minutes' do describe 'GET #buy_minutes' do
subject(:buy_minutes) { get :buy_minutes, params: { plan_id: 'bronze_id' } } let_it_be(:group) { create(:group) }
it_behaves_like 'unauthenticated subscription request', 'buy_minutes' subject(:buy_minutes) { get :buy_minutes, params: { selected_group: group.id } }
context 'with authenticated user' do context 'with authenticated user' do
before do before do
group.add_owner(user)
stub_feature_flags(new_route_ci_minutes_purchase: true)
sign_in(user) sign_in(user)
end end
it { is_expected.to render_template 'layouts/checkout' } context 'when there are groups eligible for the addon' do
it { is_expected.to render_template :buy_minutes }
context 'when there are groups eligible for the subscription' do
let_it_be(:group) { create(:group) } let_it_be(:group) { create(:group) }
before do before do
...@@ -88,30 +87,21 @@ RSpec.describe SubscriptionsController do ...@@ -88,30 +87,21 @@ RSpec.describe SubscriptionsController do
end end
end end
it 'assigns the eligible groups for the subscription' do it { is_expected.to render_template 'layouts/checkout' }
buy_minutes it { is_expected.to render_template :buy_minutes }
expect(assigns(:eligible_groups)).to eq [group]
end
end
context 'when there are no eligible groups for the subscription' do
it 'assigns eligible groups as an empty array' do
allow_next_instance_of(GitlabSubscriptions::FilterPurchaseEligibleNamespacesService, user: user, namespaces: []) do |instance|
allow(instance).to receive(:execute).and_return(instance_double(ServiceResponse, success?: true, payload: []))
end
it 'assigns the group for the addon' do
buy_minutes buy_minutes
expect(assigns(:eligible_groups)).to eq [] expect(assigns(:group)).to eq group
end end
end end
end end
context 'with :new_route_ci_minutes_purchase disabled' do context 'with :new_route_ci_minutes_purchase disabled' do
before do before do
sign_in(user)
stub_feature_flags(new_route_ci_minutes_purchase: false) stub_feature_flags(new_route_ci_minutes_purchase: false)
sign_in(user)
end end
it { is_expected.to have_gitlab_http_status(:not_found) } it { is_expected.to have_gitlab_http_status(:not_found) }
......
...@@ -8,10 +8,7 @@ import subscriptionsResolvers from 'ee/subscriptions/buy_minutes/graphql/resolve ...@@ -8,10 +8,7 @@ import subscriptionsResolvers from 'ee/subscriptions/buy_minutes/graphql/resolve
import stateQuery from 'ee/subscriptions/graphql/queries/state.query.graphql'; import stateQuery from 'ee/subscriptions/graphql/queries/state.query.graphql';
import Step from 'ee/vue_shared/purchase_flow/components/step.vue'; import Step from 'ee/vue_shared/purchase_flow/components/step.vue';
import purchaseFlowResolvers from 'ee/vue_shared/purchase_flow/graphql/resolvers'; import purchaseFlowResolvers from 'ee/vue_shared/purchase_flow/graphql/resolvers';
import { import { stateData as initialStateData } from 'ee_jest/subscriptions/buy_minutes/mock_data';
stateData as initialStateData,
mockCiMinutesPlans,
} from 'ee_jest/subscriptions/buy_minutes/mock_data';
import createMockApollo from 'helpers/mock_apollo_helper'; import createMockApollo from 'helpers/mock_apollo_helper';
const localVue = createLocalVue(); const localVue = createLocalVue();
...@@ -40,9 +37,6 @@ describe('AddonPurchaseDetails', () => { ...@@ -40,9 +37,6 @@ describe('AddonPurchaseDetails', () => {
return mount(AddonPurchaseDetails, { return mount(AddonPurchaseDetails, {
localVue, localVue,
apolloProvider, apolloProvider,
propsData: {
plans: mockCiMinutesPlans,
},
stubs: { stubs: {
Step, Step,
}, },
......
import { mount, createLocalVue } from '@vue/test-utils';
import { merge } from 'lodash';
import VueApollo from 'vue-apollo';
import OrderSummary from 'ee/subscriptions/buy_minutes/components/order_summary.vue';
import subscriptionsResolvers from 'ee/subscriptions/buy_minutes/graphql/resolvers';
import stateQuery from 'ee/subscriptions/graphql/queries/state.query.graphql';
import purchaseFlowResolvers from 'ee/vue_shared/purchase_flow/graphql/resolvers';
import {
mockCiMinutesPlans,
stateData as mockStateData,
} from 'ee_jest/subscriptions/buy_minutes/mock_data';
import createMockApollo from 'helpers/mock_apollo_helper';
const localVue = createLocalVue();
localVue.use(VueApollo);
describe('Order Summary', () => {
const resolvers = { ...purchaseFlowResolvers, ...subscriptionsResolvers };
const initialStateData = {
selectedPlanId: 'secondPlanId',
};
let wrapper;
const createMockApolloProvider = (stateData = {}) => {
const mockApollo = createMockApollo([], resolvers);
const data = merge({}, mockStateData, initialStateData, stateData);
mockApollo.clients.defaultClient.cache.writeQuery({
query: stateQuery,
data,
});
return mockApollo;
};
const createComponent = (stateData) => {
const apolloProvider = createMockApolloProvider(stateData);
wrapper = mount(OrderSummary, {
localVue,
apolloProvider,
propsData: {
plans: mockCiMinutesPlans,
},
});
};
afterEach(() => {
wrapper.destroy();
});
describe('Changing the company name', () => {
describe('When purchasing for a single user', () => {
beforeEach(() => {
createComponent({ isSetupForCompany: false });
});
it('should display the title with the passed name', () => {
expect(wrapper.find('h4').text()).toContain("Full Name's GitLab subscription");
});
});
describe('When purchasing for a company or group', () => {
describe('Without a group name provided', () => {
beforeEach(() => {
createComponent({ isSetupForCompany: true });
});
it('should display the title with the default name', () => {
expect(wrapper.find('h4').text()).toContain("Your organization's GitLab subscription");
});
});
describe('With a group name provided', () => {
beforeEach(() => {
createComponent({
isSetupForCompany: true,
customer: { company: 'My group' },
});
});
it('when given a group name, it should display the title with the group name', () => {
expect(wrapper.find('h4').text()).toContain("My group's GitLab subscription");
});
});
});
});
describe('Changing the plan', () => {
beforeEach(() => {
createComponent();
});
describe('the selected plan', () => {
it('should display the chosen plan', () => {
expect(wrapper.find('.js-selected-plan').text()).toContain('silver plan');
});
it('should display the correct formatted amount price per user', () => {
expect(wrapper.find('.js-per-user').text()).toContain('$228 per user per year');
});
});
describe('the default plan', () => {
beforeEach(() => {
createComponent({
subscription: { quantity: 1 },
selectedPlanId: 'firstPlanId',
});
});
it('should display the chosen plan', () => {
expect(wrapper.find('.js-selected-plan').text()).toContain('bronze plan');
});
it('should display the correct formatted amount price per user', () => {
expect(wrapper.find('.js-per-user').text()).toContain('$48 per user per year');
});
it('should display the correct formatted total amount', () => {
expect(wrapper.find('.js-total-amount').text()).toContain('$48');
});
});
});
describe('Changing the number of users', () => {
beforeEach(() => {
createComponent({
subscription: { quantity: 1 },
});
});
describe('the default of 1 selected user', () => {
it('should display the correct number of users', () => {
expect(wrapper.find('.js-number-of-users').text()).toContain('(x1)');
});
it('should display the correct formatted amount price per user', () => {
expect(wrapper.find('.js-per-user').text()).toContain('$228 per user per year');
});
it('should display the correct multiplied formatted amount of the chosen plan', () => {
expect(wrapper.find('.js-amount').text()).toContain('$228');
});
it('should display the correct formatted total amount', () => {
expect(wrapper.find('.js-total-amount').text()).toContain('$228');
});
});
describe('3 selected users', () => {
beforeEach(() => {
createComponent({
subscription: { quantity: 3 },
});
});
it('should display the correct number of users', () => {
expect(wrapper.find('.js-number-of-users').text()).toContain('(x3)');
});
it('should display the correct formatted amount price per user', () => {
expect(wrapper.find('.js-per-user').text()).toContain('$228 per user per year');
});
it('should display the correct multiplied formatted amount of the chosen plan', () => {
expect(wrapper.find('.js-amount').text()).toContain('$684');
});
it('should display the correct formatted total amount', () => {
expect(wrapper.find('.js-total-amount').text()).toContain('$684');
});
});
describe('no selected users', () => {
beforeEach(() => {
createComponent({
subscription: { quantity: 0 },
});
});
it('should not display the number of users', () => {
expect(wrapper.find('.js-number-of-users').exists()).toBe(false);
});
it('should display the correct formatted amount price per user', () => {
expect(wrapper.find('.js-per-user').text()).toContain('$228 per user per year');
});
it('should not display the amount', () => {
expect(wrapper.find('.js-amount').text()).toContain('-');
});
it('should display the correct formatted total amount', () => {
expect(wrapper.find('.js-total-amount').text()).toContain('-');
});
});
describe('date range', () => {
beforeEach(() => {
createComponent();
});
it('shows the formatted date range from the start date to one year in the future', () => {
expect(wrapper.find('.js-dates').text()).toContain('Jul 6, 2020 - Jul 6, 2021');
});
});
describe('tax rate', () => {
beforeEach(() => {
createComponent();
});
describe('a tax rate of 0', () => {
it('should not display the total amount excluding vat', () => {
expect(wrapper.find('.js-total-ex-vat').exists()).toBe(false);
});
it('should not display the vat amount', () => {
expect(wrapper.find('.js-vat').exists()).toBe(false);
});
});
});
});
});
import { shallowMount } from '@vue/test-utils';
import SummaryDetails from 'ee/subscriptions/buy_minutes/components/order_summary/summary_details.vue';
describe('SummaryDetails', () => {
let wrapper;
const createComponent = (props = {}) => {
return shallowMount(SummaryDetails, {
propsData: {
vat: 8,
totalExVat: 10,
selectedPlanText: 'Test',
selectedPlanPrice: 10,
totalAmount: 10,
quantity: 1,
...props,
},
});
};
const findQuantity = () => wrapper.find('[data-testid="quantity"]');
const findSubscriptionPeriod = () => wrapper.find('[data-testid="subscription-period"]');
const findTotalExVat = () => wrapper.find('[data-testid="total-ex-vat"]');
const findVat = () => wrapper.find('[data-testid="vat"]');
afterEach(() => {
wrapper.destroy();
});
describe('rendering', () => {
beforeEach(() => {
wrapper = createComponent();
});
it('renders the plan name', () => {
expect(wrapper.find('[data-testid="selected-plan"]').text()).toMatchInterpolatedText(
'Test plan (x1)',
);
});
it('renders the price per unit', () => {
expect(wrapper.find('[data-testid="price-per-unit"]').text()).toBe('$10 per pack per year');
});
});
describe('when quantity is greater then zero', () => {
beforeEach(() => {
wrapper = createComponent();
});
it('renders quantity', () => {
expect(findQuantity().isVisible()).toBe(true);
expect(findQuantity().text()).toBe('(x1)');
});
});
describe('when quantity is less or equal to zero', () => {
beforeEach(() => {
wrapper = createComponent({ quantity: 0 });
});
it('does not render quantity', () => {
expect(wrapper.find('[data-testid="quantity"]').exists()).toBe(false);
});
});
describe('when subscription has expiration', () => {
beforeEach(() => {
wrapper = createComponent({ purchaseHasExpiration: true });
});
it('renders subscription period', () => {
expect(findSubscriptionPeriod().isVisible()).toBe(true);
expect(findSubscriptionPeriod().text()).toBe('Jul 6, 2020 - Jul 6, 2021');
});
});
describe('when subscription does not have expiration', () => {
beforeEach(() => {
wrapper = createComponent({ purchaseHasExpiration: false });
});
it('does not render subscription period', () => {
expect(findSubscriptionPeriod().exists()).toBe(false);
});
});
describe('when tax rate is applied', () => {
beforeEach(() => {
wrapper = createComponent({ taxRate: 8 });
});
it('renders tax fields', () => {
expect(findTotalExVat().isVisible()).toBe(true);
expect(findTotalExVat().text()).toBe('$10');
expect(findVat().isVisible()).toBe(true);
expect(findVat().text()).toBe('$8');
});
});
describe('when tax rate is not applied', () => {
beforeEach(() => {
wrapper = createComponent();
});
it('does not render tax fields', () => {
expect(findTotalExVat().exists()).toBe(false);
expect(findVat().exists()).toBe(false);
});
});
});
import { shallowMount, createLocalVue } from '@vue/test-utils';
import { merge } from 'lodash';
import VueApollo from 'vue-apollo';
import OrderSummary from 'ee/subscriptions/buy_minutes/components/order_summary.vue';
import subscriptionsResolvers from 'ee/subscriptions/buy_minutes/graphql/resolvers';
import stateQuery from 'ee/subscriptions/graphql/queries/state.query.graphql';
import purchaseFlowResolvers from 'ee/vue_shared/purchase_flow/graphql/resolvers';
import {
mockCiMinutesPlans,
mockParsedNamespaces,
stateData as mockStateData,
} from 'ee_jest/subscriptions/buy_minutes/mock_data';
import createMockApollo from 'helpers/mock_apollo_helper';
const localVue = createLocalVue();
localVue.use(VueApollo);
describe('Order Summary', () => {
const resolvers = { ...purchaseFlowResolvers, ...subscriptionsResolvers };
const initialStateData = {
selectedPlanId: 'ciMinutesPackPlanId',
namespaces: [mockParsedNamespaces[0]],
subscription: {
namespaceId: mockParsedNamespaces[0].id,
},
};
let wrapper;
const createMockApolloProvider = (stateData = {}) => {
const mockApollo = createMockApollo([], resolvers);
const data = merge({}, mockStateData, initialStateData, stateData);
mockApollo.clients.defaultClient.cache.writeQuery({
query: stateQuery,
data,
});
return mockApollo;
};
const createComponent = (stateData) => {
const apolloProvider = createMockApolloProvider(stateData);
wrapper = shallowMount(OrderSummary, {
localVue,
apolloProvider,
propsData: {
plan: mockCiMinutesPlans[0],
},
});
};
afterEach(() => {
wrapper.destroy();
});
describe('the default plan', () => {
beforeEach(() => {
createComponent({
subscription: { quantity: 1 },
selectedPlanId: 'ciMinutesPackPlanId',
});
});
it('displays the title', () => {
expect(wrapper.find('[data-testid="title"]').text()).toMatchInterpolatedText(
"Gitlab Org's CI minutes",
);
});
});
describe('when quantity is greater than zero', () => {
beforeEach(() => {
createComponent({
subscription: { quantity: 3 },
});
});
it('renders amount', () => {
expect(wrapper.find('[data-testid="amount"]').text()).toBe('$30');
});
});
describe('when quantity is less than or equal to zero', () => {
beforeEach(() => {
createComponent({
subscription: { quantity: 0 },
});
});
it('does not render amount', () => {
expect(wrapper.find('[data-testid="amount"]').text()).toBe('-');
});
});
});
import { STEPS } from 'ee/subscriptions/constants'; import { STEPS } from 'ee/subscriptions/constants';
export const mockCiMinutesPlans = [ export const mockCiMinutesPlans = [
{ id: 'firstPlanId', code: 'bronze', pricePerYear: 48, name: 'bronze', __typename: 'Plan' }, {
{ id: 'secondPlanId', code: 'silver', pricePerYear: 228, name: 'silver', __typename: 'Plan' }, id: 'ciMinutesPackPlanId',
code: 'ci_minutes',
pricePerYear: 10,
name: '1000 CI minutes pack',
__typename: 'Plan',
},
]; ];
export const mockNamespaces = export const mockNamespaces =
'[{"id":132,"name":"Gitlab Org","users":3},{"id":483,"name":"Gnuwget","users":12}]'; '[{"id":132,"name":"Gitlab Org","users":3},{"id":483,"name":"Gnuwget","users":12}]';
......
import apolloProvider from 'ee/subscriptions/buy_minutes/graphql'; import apolloProvider from 'ee/subscriptions/buy_minutes/graphql';
import { writeInitialDataToApolloCache } from 'ee/subscriptions/buy_minutes/utils'; import { writeInitialDataToApolloCache } from 'ee/subscriptions/buy_minutes/utils';
import stateQuery from 'ee/subscriptions/graphql/queries/state.query.graphql'; import stateQuery from 'ee/subscriptions/graphql/queries/state.query.graphql';
import { import { mockNamespaces, mockParsedNamespaces } from './mock_data';
mockNamespaces,
mockParsedNamespaces,
mockNewUser,
mockFullName,
mockSetupForCompany,
} from './mock_data';
const DEFAULT_DATA = { const DEFAULT_DATA = {
groupData: mockNamespaces, groupData: mockNamespaces,
newUser: mockNewUser, namespaceId: mockParsedNamespaces[0].id,
fullName: mockFullName, newUser: false,
setupForCompany: mockSetupForCompany, fullName: null,
setupForCompany: false,
}; };
describe('utils', () => { describe('utils', () => {
...@@ -48,72 +43,5 @@ describe('utils', () => { ...@@ -48,72 +43,5 @@ describe('utils', () => {
}); });
}); });
}); });
describe('newUser', () => {
describe.each`
newUser | parsedNewUser | throws
${'true'} | ${true} | ${false}
${mockNewUser} | ${false} | ${false}
${''} | ${false} | ${true}
`('parameter decoding', ({ newUser, parsedNewUser, throws }) => {
it(`decodes ${newUser} to ${parsedNewUser}`, async () => {
if (throws) {
expect(() => {
writeInitialDataToApolloCache(apolloProvider, { newUser });
}).toThrow();
} else {
writeInitialDataToApolloCache(apolloProvider, { ...DEFAULT_DATA, newUser });
const sourceData = await apolloProvider.clients.defaultClient.query({
query: stateQuery,
});
expect(sourceData.data.isNewUser).toEqual(parsedNewUser);
}
});
});
});
describe('fullName', () => {
describe.each`
fullName | parsedFullName
${mockFullName} | ${mockFullName}
${''} | ${''}
${null} | ${null}
`('parameter decoding', ({ fullName, parsedFullName }) => {
it(`decodes ${fullName} to ${parsedFullName}`, async () => {
writeInitialDataToApolloCache(apolloProvider, { ...DEFAULT_DATA, fullName });
const sourceData = await apolloProvider.clients.defaultClient.query({
query: stateQuery,
});
expect(sourceData.data.fullName).toEqual(parsedFullName);
});
});
});
describe('setupForCompany', () => {
describe.each`
setupForCompany | parsedSetupForCompany | throws
${mockSetupForCompany} | ${true} | ${false}
${'false'} | ${false} | ${false}
${''} | ${false} | ${true}
`('parameter decoding', ({ setupForCompany, parsedSetupForCompany, throws }) => {
it(`decodes ${setupForCompany} to ${parsedSetupForCompany}`, async () => {
if (throws) {
expect(() => {
writeInitialDataToApolloCache(apolloProvider, { setupForCompany });
}).toThrow();
} else {
writeInitialDataToApolloCache(apolloProvider, {
...DEFAULT_DATA,
newUser: 'true',
setupForCompany,
});
const sourceData = await apolloProvider.clients.defaultClient.query({
query: stateQuery,
});
expect(sourceData.data.isSetupForCompany).toEqual(parsedSetupForCompany);
}
});
});
});
}); });
}); });
...@@ -132,4 +132,21 @@ RSpec.describe SubscriptionsHelper do ...@@ -132,4 +132,21 @@ RSpec.describe SubscriptionsHelper do
it { is_expected.to eq(nil) } it { is_expected.to eq(nil) }
end end
end end
describe '#addon_data' do
subject(:addon_data) { helper.addon_data(group) }
let_it_be(:user) { create(:user, name: 'First Last') }
let_it_be(:group) { create(:group, name: 'My Namespace') }
before do
allow(helper).to receive(:current_user).and_return(user)
allow(helper).to receive(:params).and_return({ selected_group: group.id.to_s, source: 'some_source' })
group.add_owner(user)
end
it { is_expected.to include(namespace_id: group.id.to_s) }
it { is_expected.to include(source: 'some_source') }
it { is_expected.to include(group_data: %Q{[{"id":#{group.id},"name":"My Namespace","users":1,"guests":0}]}) }
end
end end
# frozen_string_literal: true # frozen_string_literal: true
RSpec.shared_examples_for 'subscription form data' do |js_selector| RSpec.shared_examples_for 'subscription form data' do |js_selector|
before do before do
allow(view).to receive(:subscription_data).and_return( allow(view).to receive(:subscription_data).and_return(
...@@ -18,3 +19,21 @@ RSpec.shared_examples_for 'subscription form data' do |js_selector| ...@@ -18,3 +19,21 @@ RSpec.shared_examples_for 'subscription form data' do |js_selector|
it { is_expected.to have_selector("#{js_selector}[data-plan-id='bronze_id']") } it { is_expected.to have_selector("#{js_selector}[data-plan-id='bronze_id']") }
it { is_expected.to have_selector("#{js_selector}[data-source='some_source']") } it { is_expected.to have_selector("#{js_selector}[data-source='some_source']") }
end end
RSpec.shared_examples_for 'addon form data' do |js_selector|
before do
allow(view).to receive(:addon_data).and_return(
plan_data: '[{"id":"ci_minutes_plan_id","code":"ci_minutes","price_per_year":10.0}]',
namespace_id: '1',
plan_id: 'ci_minutes_plan_id',
source: 'some_source'
)
end
subject { render }
it { is_expected.to have_selector("#{js_selector}[data-plan-data='[{\"id\":\"ci_minutes_plan_id\",\"code\":\"ci_minutes\",\"price_per_year\":10.0}]']") }
it { is_expected.to have_selector("#{js_selector}[data-plan-id='ci_minutes_plan_id']") }
it { is_expected.to have_selector("#{js_selector}[data-namespace-id='1']") }
it { is_expected.to have_selector("#{js_selector}[data-source='some_source']") }
end
...@@ -3,5 +3,5 @@ ...@@ -3,5 +3,5 @@
require 'spec_helper' require 'spec_helper'
RSpec.describe 'subscriptions/buy_minutes' do RSpec.describe 'subscriptions/buy_minutes' do
it_behaves_like 'subscription form data', '#js-buy-minutes' it_behaves_like 'addon form data', '#js-buy-minutes'
end end
...@@ -6418,12 +6418,18 @@ msgstr "" ...@@ -6418,12 +6418,18 @@ msgstr ""
msgid "Checkout" msgid "Checkout"
msgstr "" msgstr ""
msgid "Checkout|$%{selectedPlanPrice} per pack per year"
msgstr ""
msgid "Checkout|$%{selectedPlanPrice} per user per year" msgid "Checkout|$%{selectedPlanPrice} per user per year"
msgstr "" msgstr ""
msgid "Checkout|%{cardType} ending in %{lastFourDigits}" msgid "Checkout|%{cardType} ending in %{lastFourDigits}"
msgstr "" msgstr ""
msgid "Checkout|%{name}'s CI minutes"
msgstr ""
msgid "Checkout|%{name}'s GitLab subscription" msgid "Checkout|%{name}'s GitLab subscription"
msgstr "" msgstr ""
...@@ -6442,6 +6448,9 @@ msgstr "" ...@@ -6442,6 +6448,9 @@ msgstr ""
msgid "Checkout|(x%{numberOfUsers})" msgid "Checkout|(x%{numberOfUsers})"
msgstr "" msgstr ""
msgid "Checkout|(x%{quantity})"
msgstr ""
msgid "Checkout|Billing address" msgid "Checkout|Billing address"
msgstr "" 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