Commit a9c50433 authored by Natalia Tepluhina's avatar Natalia Tepluhina

Merge branch 'vs-migrate-summary-to-gql' into 'master'

Migrate OrderSummary component to GraphQL

See merge request gitlab-org/gitlab!59963
parents 4de4d351 05d5910e
...@@ -7,11 +7,13 @@ import { ERROR_FETCHING_DATA_HEADER, ERROR_FETCHING_DATA_DESCRIPTION } from '~/e ...@@ -7,11 +7,13 @@ import { ERROR_FETCHING_DATA_HEADER, ERROR_FETCHING_DATA_DESCRIPTION } from '~/e
import plansQuery from '../../graphql/queries/plans.customer.query.graphql'; import plansQuery from '../../graphql/queries/plans.customer.query.graphql';
import { planTags, CUSTOMER_CLIENT } from '../constants'; import { planTags, CUSTOMER_CLIENT } from '../constants';
import Checkout from './checkout.vue'; import Checkout from './checkout.vue';
import OrderSummary from './order_summary.vue';
export default { export default {
components: { components: {
Checkout, Checkout,
GlEmptyState, GlEmptyState,
OrderSummary,
StepOrderApp, StepOrderApp,
}, },
i18n: { i18n: {
...@@ -59,6 +61,8 @@ export default { ...@@ -59,6 +61,8 @@ export default {
<template #checkout> <template #checkout>
<checkout :plans="plans" /> <checkout :plans="plans" />
</template> </template>
<template #order-summary></template> <template #order-summary>
<order-summary :plans="plans" />
</template>
</step-order-app> </step-order-app>
</template> </template>
...@@ -33,12 +33,12 @@ export default { ...@@ -33,12 +33,12 @@ export default {
<template> <template>
<div <div
v-if="!$apollo.loading" v-if="!$apollo.loading"
class="checkout gl-flex gl-flex-column gl-justify-content-between w-100" class="checkout gl-display-flex gl-flex-direction-column gl-justify-content-between w-100"
> >
<div class="full-width"> <div class="full-width">
<progress-bar v-if="isNewUser" :steps="$options.steps" :current-step="$options.currentStep" /> <progress-bar v-if="isNewUser" :steps="$options.steps" :current-step="$options.currentStep" />
<div class="flash-container"></div> <div class="flash-container"></div>
<h2 class="gl-mt-4 gl-mb-3 gl-mb-lg-5">{{ $options.i18n.checkout }}</h2> <h2 class="gl-mt-6 gl-mb-7 gl-mb-lg-5">{{ $options.i18n.checkout }}</h2>
<subscription-details :plans="plans" /> <subscription-details :plans="plans" />
</div> </div>
</div> </div>
......
<script>
import { GlIcon } from '@gitlab/ui';
import STATE_QUERY from 'ee/subscriptions/graphql/queries/state.query.graphql';
import { TAX_RATE, NEW_GROUP } from 'ee/subscriptions/new/constants';
import formattingMixins from 'ee/subscriptions/new/formatting_mixins';
import { sprintf, s__ } from '~/locale';
import SummaryDetails from './order_summary/summary_details.vue';
export default {
components: {
SummaryDetails,
GlIcon,
},
mixins: [formattingMixins],
props: {
plans: {
type: Array,
required: true,
},
},
apollo: {
state: {
query: STATE_QUERY,
update(data) {
return data;
},
result({ data }) {
this.subscription = data.subscription;
this.namespaces = data.namespaces;
this.isSetupForCompany = data.isSetupForCompany;
this.fullName = data.fullName;
this.customer = data.customer;
},
},
},
data() {
return {
subscription: {},
namespaces: [],
isSetupForCompany: false,
collapsed: true,
fullName: null,
customer: {},
};
},
computed: {
selectedPlan() {
return this.plans.find((plan) => plan.code === this.subscription.planId);
},
selectedPlanPrice() {
return this.selectedPlan.pricePerYear;
},
selectedGroup() {
return this.namespaces.find((group) => group.id === this.subscription.namespaceId);
},
totalExVat() {
return this.subscription.quantity * this.selectedPlanPrice;
},
vat() {
return TAX_RATE * this.totalExVat;
},
totalAmount() {
return this.totalExVat + this.vat;
},
usersPresent() {
return this.subscription.quantity > 0;
},
isGroupSelected() {
return this.subscription.namespaceId && this.subscription.namespaceId !== NEW_GROUP;
},
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() {
return sprintf(this.$options.i18n.title, { name: this.name });
},
},
methods: {
toggleCollapse() {
this.collapsed = !this.collapsed;
},
},
i18n: {
title: s__("Checkout|%{name}'s GitLab subscription"),
},
taxRate: TAX_RATE,
};
</script>
<template>
<div
v-if="!$apollo.loading && (!isGroupSelected || isSelectedGroupPresent)"
class="order-summary gl-display-flex gl-flex-direction-column gl-flex-grow-1 gl-mt-2 mt-lg-5"
>
<div class="d-lg-none">
<div @click="toggleCollapse">
<h4 class="d-flex justify-content-between gl-font-lg" :class="{ 'gl-mb-7': !collapsed }">
<div class="d-flex">
<gl-icon v-if="collapsed" name="chevron-right" :size="18" use-deprecated-sizes />
<gl-icon v-else name="chevron-down" :size="18" use-deprecated-sizes />
<div>{{ titleWithName }}</div>
</div>
<div class="gl-ml-3">{{ formatAmount(totalAmount, usersPresent) }}</div>
</h4>
</div>
<summary-details
v-show="!collapsed"
:vat="vat"
:total-ex-vat="totalExVat"
:users-present="usersPresent"
:selected-plan-text="selectedPlan.name"
:selected-plan-price="selectedPlanPrice"
:total-amount="totalAmount"
:number-of-users="subscription.quantity"
:tax-rate="$options.taxRate"
/>
</div>
<div class="d-none d-lg-block">
<div class="append-bottom-20">
<h4>
{{ titleWithName }}
</h4>
</div>
<summary-details
:vat="vat"
:total-ex-vat="totalExVat"
:users-present="usersPresent"
:selected-plan-text="selectedPlan.name"
:selected-plan-price="selectedPlanPrice"
:total-amount="totalAmount"
:number-of-users="subscription.quantity"
:tax-rate="$options.taxRate"
/>
</div>
</div>
</template>
<script>
import formattingMixins from 'ee/subscriptions/new/formatting_mixins';
import { s__ } from '~/locale';
export default {
mixins: [formattingMixins],
props: {
vat: {
type: Number,
required: true,
},
totalExVat: {
type: Number,
required: true,
},
usersPresent: {
type: Boolean,
required: true,
},
selectedPlanText: {
type: String,
required: true,
},
selectedPlanPrice: {
type: Number,
required: true,
},
totalAmount: {
type: Number,
required: true,
},
numberOfUsers: {
type: Number,
required: true,
},
taxRate: {
type: Number,
required: false,
default: null,
},
},
computed: {
startDate() {
return new Date(Date.now());
},
endDate() {
return new Date(this.startDate).setFullYear(this.startDate.getFullYear() + 1);
},
},
i18n: {
selectedPlanText: s__('Checkout|%{selectedPlanText} plan'),
numberOfUsers: s__('Checkout|(x%{numberOfUsers})'),
pricePerUserPerYear: s__('Checkout|$%{selectedPlanPrice} per user per year'),
dates: s__('Checkout|%{startDate} - %{endDate}'),
subtotal: s__('Checkout|Subtotal'),
tax: s__('Checkout|Tax'),
total: s__('Checkout|Total'),
},
};
</script>
<template>
<div>
<div class="d-flex justify-content-between bold gl-mt-3 gl-mb-3">
<div class="js-selected-plan">
{{ sprintf($options.i18n.selectedPlanText, { selectedPlanText }) }}
<span v-if="usersPresent" class="js-number-of-users">{{
sprintf($options.i18n.numberOfUsers, { numberOfUsers })
}}</span>
</div>
<div class="js-amount">{{ formatAmount(totalExVat, usersPresent) }}</div>
</div>
<div class="text-secondary js-per-user">
{{
sprintf($options.i18n.pricePerUserPerYear, {
selectedPlanPrice: selectedPlanPrice.toLocaleString(),
})
}}
</div>
<div class="text-secondary js-dates">
{{
sprintf($options.i18n.dates, {
startDate: formatDate(startDate),
endDate: formatDate(endDate),
})
}}
</div>
<div v-if="taxRate">
<div class="border-bottom gl-mt-3 gl-mb-3"></div>
<div class="d-flex justify-content-between text-secondary">
<div>{{ $options.i18n.subtotal }}</div>
<div class="js-total-ex-vat">{{ formatAmount(totalExVat, usersPresent) }}</div>
</div>
<div class="d-flex justify-content-between text-secondary">
<div>{{ $options.i18n.tax }}</div>
<div class="js-vat">{{ formatAmount(vat, usersPresent) }}</div>
</div>
</div>
<div class="border-bottom gl-mt-3 gl-mb-3"></div>
<div class="d-flex justify-content-between bold gl-font-lg">
<div>{{ $options.i18n.total }}</div>
<div class="js-total-amount">{{ formatAmount(totalAmount, usersPresent) }}</div>
</div>
</div>
</template>
...@@ -34,13 +34,13 @@ export const resolvers = { ...@@ -34,13 +34,13 @@ export const resolvers = {
return SubscriptionsApi.createSubscription(groupId, customer, subscription); return SubscriptionsApi.createSubscription(groupId, customer, subscription);
}, },
updateState: (_, { input }, { cache }) => { updateState: (_, { input }, { cache }) => {
const { state: oldState } = cache.readQuery({ query: STATE_QUERY }); const oldState = cache.readQuery({ query: STATE_QUERY });
const state = produce(oldState, (draftState) => { const state = produce(oldState, (draftState) => {
merge(draftState, input); merge(draftState, input);
}); });
cache.writeQuery({ query: STATE_QUERY, data: { state } }); cache.writeQuery({ query: STATE_QUERY, data: state });
}, },
}, },
}; };
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 = {
subscription: { planId: 'silver' },
};
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: { planId: 'bronze', quantity: 1 },
});
});
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: { planId: 'silver', 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: { planId: 'silver', 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: { planId: 'silver', 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);
});
});
});
});
});
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