Commit 4d084983 authored by Vitaly Slobodin's avatar Vitaly Slobodin

Merge branch 'dz/347393-call-order-preview-endpoint-on-storage-purchase' into 'master'

Add request to orderPreview for storage purchase

See merge request gitlab-org/gitlab!76952
parents b66377fa 3b4e19fe
...@@ -70,9 +70,6 @@ export default { ...@@ -70,9 +70,6 @@ export default {
isAddon: true, isAddon: true,
}; };
}, },
slotProps() {
return this.plan;
},
totalUnits() { totalUnits() {
return this.quantity * this.config.quantityPerPack; return this.quantity * this.config.quantityPerPack;
}, },
......
<script> <script>
import { GlIcon, GlCollapse, GlCollapseToggleDirective } from '@gitlab/ui'; import { GlIcon, GlCollapse, GlCollapseToggleDirective } from '@gitlab/ui';
import find from 'lodash/find'; import find from 'lodash/find';
import stateQuery from 'ee/subscriptions/graphql/queries/state.query.graphql'; import { logError } from '~/lib/logger';
import { TAX_RATE } from 'ee/subscriptions/new/constants'; import { TAX_RATE } from 'ee/subscriptions/new/constants';
import { CUSTOMERSDOT_CLIENT } from 'ee/subscriptions/buy_addons_shared/constants';
import formattingMixins from 'ee/subscriptions/new/formatting_mixins'; import formattingMixins from 'ee/subscriptions/new/formatting_mixins';
import { sprintf } from '~/locale'; import { sprintf } from '~/locale';
import stateQuery from 'ee/subscriptions/graphql/queries/state.query.graphql';
import orderPreviewQuery from 'ee/subscriptions/graphql/queries/order_preview.customer.query.graphql';
import SummaryDetails from './order_summary/summary_details.vue'; import SummaryDetails from './order_summary/summary_details.vue';
export default { export default {
...@@ -21,6 +26,9 @@ export default { ...@@ -21,6 +26,9 @@ export default {
plan: { plan: {
type: Object, type: Object,
required: true, required: true,
validator(value) {
return Object.prototype.hasOwnProperty.call(value, 'id');
},
}, },
title: { title: {
type: String, type: String,
...@@ -39,6 +47,32 @@ export default { ...@@ -39,6 +47,32 @@ export default {
const id = Number(data.selectedNamespaceId); const id = Number(data.selectedNamespaceId);
this.selectedNamespace = find(data.eligibleNamespaces, { id }); this.selectedNamespace = find(data.eligibleNamespaces, { id });
this.subscription = data.subscription; this.subscription = data.subscription;
this.selectedNamespaceId = data.selectedNamespaceId;
},
},
orderPreview: {
client: CUSTOMERSDOT_CLIENT,
query: orderPreviewQuery,
variables() {
return {
namespaceId: this.selectedNamespaceId,
newProductId: this.plan.id,
newProductQuantity: this.subscription.quantity,
};
},
manual: true,
result({ data }) {
if (data.orderPreview) {
this.endDate = data.orderPreview.targetDate;
this.proratedAmount = data.orderPreview.amount;
}
},
error(error) {
this.hasError = true;
logError(error);
},
skip() {
return !this.purchaseHasExpiration;
}, },
}, },
}, },
...@@ -47,6 +81,9 @@ export default { ...@@ -47,6 +81,9 @@ export default {
isBottomSummaryVisible: false, isBottomSummaryVisible: false,
selectedNamespace: {}, selectedNamespace: {},
subscription: {}, subscription: {},
endDate: '',
proratedAmount: 0,
hasError: false,
}; };
}, },
computed: { computed: {
...@@ -54,13 +91,15 @@ export default { ...@@ -54,13 +91,15 @@ export default {
return this.plan.pricePerYear; return this.plan.pricePerYear;
}, },
totalExVat() { totalExVat() {
return this.subscription.quantity * this.selectedPlanPrice; return this.isLoading
? 0
: this.proratedAmount || this.subscription.quantity * this.selectedPlanPrice;
}, },
vat() { vat() {
return TAX_RATE * this.totalExVat; return TAX_RATE * this.totalExVat;
}, },
totalAmount() { totalAmount() {
return this.totalExVat + this.vat; return this.isLoading ? 0 : this.proratedAmount || this.totalExVat + this.vat;
}, },
quantityPresent() { quantityPresent() {
return this.subscription.quantity > 0; return this.subscription.quantity > 0;
...@@ -74,8 +113,8 @@ export default { ...@@ -74,8 +113,8 @@ export default {
titleWithName() { titleWithName() {
return sprintf(this.title, { name: this.namespaceName }); return sprintf(this.title, { name: this.namespaceName });
}, },
isVisible() { isLoading() {
return !this.$apollo.loading; return this.$apollo.loading;
}, },
}, },
taxRate: TAX_RATE, taxRate: TAX_RATE,
...@@ -83,7 +122,6 @@ export default { ...@@ -83,7 +122,6 @@ export default {
</script> </script>
<template> <template>
<div <div
v-if="isVisible"
class="order-summary gl-display-flex gl-flex-direction-column gl-flex-grow-1 gl-mt-2 mt-lg-5" class="order-summary gl-display-flex gl-flex-direction-column gl-flex-grow-1 gl-mt-2 mt-lg-5"
> >
<div class="gl-lg-display-none"> <div class="gl-lg-display-none">
...@@ -95,7 +133,7 @@ export default { ...@@ -95,7 +133,7 @@ export default {
<h4 data-testid="title">{{ titleWithName }}</h4> <h4 data-testid="title">{{ titleWithName }}</h4>
</div> </div>
<p class="gl-ml-3" data-testid="amount"> <p class="gl-ml-3" data-testid="amount">
{{ formatAmount(totalAmount, quantityPresent) }} {{ totalAmount ? formatAmount(totalAmount, quantityPresent) : '-' }}
</p> </p>
</div> </div>
</div> </div>
...@@ -109,7 +147,7 @@ export default { ...@@ -109,7 +147,7 @@ export default {
:total-amount="totalAmount" :total-amount="totalAmount"
:quantity="quantity" :quantity="quantity"
:tax-rate="$options.taxRate" :tax-rate="$options.taxRate"
:purchase-has-expiration="purchaseHasExpiration" :subscription-end-date="endDate"
> >
<template #price-per-unit="{ price }"> <template #price-per-unit="{ price }">
<slot name="price-per-unit" :price="price"></slot> <slot name="price-per-unit" :price="price"></slot>
...@@ -133,7 +171,7 @@ export default { ...@@ -133,7 +171,7 @@ export default {
:total-amount="totalAmount" :total-amount="totalAmount"
:quantity="quantity" :quantity="quantity"
:tax-rate="$options.taxRate" :tax-rate="$options.taxRate"
:purchase-has-expiration="purchaseHasExpiration" :subscription-end-date="endDate"
> >
<template #price-per-unit="{ price }"> <template #price-per-unit="{ price }">
<slot name="price-per-unit" :price="price"></slot> <slot name="price-per-unit" :price="price"></slot>
......
...@@ -47,10 +47,10 @@ export default { ...@@ -47,10 +47,10 @@ export default {
required: false, required: false,
default: null, default: null,
}, },
purchaseHasExpiration: { subscriptionEndDate: {
type: Boolean, type: String,
required: false, required: false,
default: false, default: '',
}, },
}, },
data() { data() {
...@@ -60,7 +60,9 @@ export default { ...@@ -60,7 +60,9 @@ export default {
}, },
computed: { computed: {
endDate() { endDate() {
return this.startDate.setFullYear(this.startDate.getFullYear() + 1); return (
this.subscriptionEndDate || this.startDate.setFullYear(this.startDate.getFullYear() + 1)
);
}, },
hasPositiveQuantity() { hasPositiveQuantity() {
return this.quantity > 0; return this.quantity > 0;
...@@ -74,6 +76,9 @@ export default { ...@@ -74,6 +76,9 @@ export default {
formattedPrice() { formattedPrice() {
return formatNumber(this.selectedPlanPrice); return formatNumber(this.selectedPlanPrice);
}, },
renderedAmount() {
return this.totalExVat ? this.formatAmount(this.totalExVat, this.hasPositiveQuantity) : '-';
},
}, },
i18n: { i18n: {
quantity: I18N_SUMMARY_QUANTITY, quantity: I18N_SUMMARY_QUANTITY,
...@@ -95,14 +100,14 @@ export default { ...@@ -95,14 +100,14 @@ export default {
}}</span> }}</span>
</div> </div>
<div> <div>
{{ formatAmount(totalExVat, hasPositiveQuantity) }} {{ renderedAmount }}
</div> </div>
</div> </div>
<div class="gl-border-b-1 gl-border-b-gray-100 gl-border-b-solid gl-py-3"> <div class="gl-border-b-1 gl-border-b-gray-100 gl-border-b-solid gl-py-3">
<div class="gl-text-gray-500" data-testid="price-per-unit"> <div class="gl-text-gray-500" data-testid="price-per-unit">
<slot name="price-per-unit" :price="formattedPrice"></slot> <slot name="price-per-unit" :price="formattedPrice"></slot>
</div> </div>
<div v-if="purchaseHasExpiration" class="gl-text-gray-500" data-testid="subscription-period"> <div v-if="subscriptionEndDate" class="gl-text-gray-500" data-testid="subscription-period">
{{ {{
sprintf($options.i18n.dates, { sprintf($options.i18n.dates, {
startDate: formatDate(startDate), startDate: formatDate(startDate),
...@@ -116,7 +121,7 @@ export default { ...@@ -116,7 +121,7 @@ export default {
<div class="gl-display-flex gl-justify-content-space-between gl-text-gray-500"> <div class="gl-display-flex gl-justify-content-space-between gl-text-gray-500">
<div>{{ $options.i18n.subtotal }}</div> <div>{{ $options.i18n.subtotal }}</div>
<div data-testid="total-ex-vat"> <div data-testid="total-ex-vat">
{{ formatAmount(totalExVat, hasPositiveQuantity) }} {{ renderedAmount }}
</div> </div>
</div> </div>
<div class="gl-display-flex gl-justify-content-space-between gl-text-gray-500"> <div class="gl-display-flex gl-justify-content-space-between gl-text-gray-500">
...@@ -141,7 +146,7 @@ export default { ...@@ -141,7 +146,7 @@ export default {
> >
<div>{{ $options.i18n.total }}</div> <div>{{ $options.i18n.total }}</div>
<div data-testid="total-amount"> <div data-testid="total-amount">
{{ formatAmount(totalAmount, hasPositiveQuantity) }} {{ renderedAmount }}
</div> </div>
</div> </div>
</div> </div>
......
...@@ -12,6 +12,7 @@ export const CUSTOMER_TYPE = 'Customer'; ...@@ -12,6 +12,7 @@ export const CUSTOMER_TYPE = 'Customer';
export const SUBSCRIPTION_TYPE = 'Subscription'; export const SUBSCRIPTION_TYPE = 'Subscription';
export const NAMESPACE_TYPE = 'Namespace'; export const NAMESPACE_TYPE = 'Namespace';
export const PAYMENT_METHOD_TYPE = 'PaymentMethod'; export const PAYMENT_METHOD_TYPE = 'PaymentMethod';
export const ORDER_PREVIEW_TYPE = 'OrderPreview';
export const PLAN_TYPE = 'Plan'; export const PLAN_TYPE = 'Plan';
export const STEP_TYPE = 'Step'; export const STEP_TYPE = 'Step';
export const COUNTRY_TYPE = 'Country'; export const COUNTRY_TYPE = 'Country';
......
query orderPreview($namespaceId: ID!, $newProductId: String!, $newProductQuantity: Int!) {
orderPreview(
namespaceId: $namespaceId
newProductId: $newProductId
newProductQuantity: $newProductQuantity
) {
targetDate
amount
amountWithoutTax
prorationCredit
priceBeforeProration
}
}
...@@ -83,18 +83,18 @@ describe('SummaryDetails', () => { ...@@ -83,18 +83,18 @@ describe('SummaryDetails', () => {
describe('when subscription has expiration', () => { describe('when subscription has expiration', () => {
beforeEach(() => { beforeEach(() => {
wrapper = createComponent({ purchaseHasExpiration: true }); wrapper = createComponent({ subscriptionEndDate: '2021-02-06' });
}); });
it('renders subscription period', () => { it('renders subscription period', () => {
expect(findSubscriptionPeriod().isVisible()).toBe(true); expect(findSubscriptionPeriod().isVisible()).toBe(true);
expect(findSubscriptionPeriod().text()).toBe('Jul 6, 2020 - Jul 6, 2021'); expect(findSubscriptionPeriod().text()).toBe('Jul 6, 2020 - Feb 6, 2021');
}); });
}); });
describe('when subscription does not have expiration', () => { describe('when subscription does not have expiration', () => {
beforeEach(() => { beforeEach(() => {
wrapper = createComponent({ purchaseHasExpiration: false }); wrapper = createComponent({ subscriptionEndDate: '' });
}); });
it('does not render subscription period', () => { it('does not render subscription period', () => {
......
...@@ -7,11 +7,15 @@ import subscriptionsResolvers from 'ee/subscriptions/buy_addons_shared/graphql/r ...@@ -7,11 +7,15 @@ import subscriptionsResolvers from 'ee/subscriptions/buy_addons_shared/graphql/r
import stateQuery from 'ee/subscriptions/graphql/queries/state.query.graphql'; import stateQuery from 'ee/subscriptions/graphql/queries/state.query.graphql';
import purchaseFlowResolvers from 'ee/vue_shared/purchase_flow/graphql/resolvers'; import purchaseFlowResolvers from 'ee/vue_shared/purchase_flow/graphql/resolvers';
import { import {
mockCiMinutesPlans, mockStoragePlans,
mockParsedNamespaces, mockParsedNamespaces,
mockOrderPreview,
stateData as mockStateData, stateData as mockStateData,
} from 'ee_jest/subscriptions/mock_data'; } from 'ee_jest/subscriptions/mock_data';
import createMockApollo from 'helpers/mock_apollo_helper'; import createMockApollo, { createMockClient } from 'helpers/mock_apollo_helper';
import orderPreviewQuery from 'ee/subscriptions/graphql/queries/order_preview.customer.query.graphql';
import { CUSTOMERSDOT_CLIENT } from 'ee/subscriptions/buy_addons_shared/constants';
const localVue = createLocalVue(); const localVue = createLocalVue();
localVue.use(VueApollo); localVue.use(VueApollo);
...@@ -29,24 +33,25 @@ describe('Order Summary', () => { ...@@ -29,24 +33,25 @@ describe('Order Summary', () => {
const findAmount = () => wrapper.findByTestId('amount'); const findAmount = () => wrapper.findByTestId('amount');
const findTitle = () => wrapper.findByTestId('title'); const findTitle = () => wrapper.findByTestId('title');
const createMockApolloProvider = (stateData = {}) => { const createMockApolloProvider = (stateData = {}, mockRequest = {}) => {
const mockApollo = createMockApollo([], resolvers); const mockApollo = createMockApollo([], resolvers);
const data = merge({}, mockStateData, initialStateData, stateData); const data = merge({}, mockStateData, initialStateData, stateData);
mockApollo.clients.defaultClient.cache.writeQuery({ mockApollo.clients.defaultClient.cache.writeQuery({
query: stateQuery, query: stateQuery,
data, data,
}); });
mockApollo.clients[CUSTOMERSDOT_CLIENT] = createMockClient([[orderPreviewQuery, mockRequest]]);
return mockApollo; return mockApollo;
}; };
const createComponent = (stateData) => { const createComponent = (apolloProvider, props) => {
const apolloProvider = createMockApolloProvider(stateData);
wrapper = shallowMountExtended(OrderSummary, { wrapper = shallowMountExtended(OrderSummary, {
localVue, localVue,
apolloProvider, apolloProvider,
propsData: { propsData: {
plan: mockCiMinutesPlans[0], plan: mockStoragePlans[0],
title: "%{name}'s CI minutes", title: "%{name}'s storage subscription",
...props,
}, },
}); });
}; };
...@@ -57,37 +62,101 @@ describe('Order Summary', () => { ...@@ -57,37 +62,101 @@ describe('Order Summary', () => {
describe('the default plan', () => { describe('the default plan', () => {
beforeEach(() => { beforeEach(() => {
createComponent({ const apolloProvider = createMockApolloProvider({ subscription: { quantity: 1 } });
subscription: { quantity: 1 }, createComponent(apolloProvider);
});
}); });
it('displays the title', () => { it('displays the title', () => {
expect(findTitle().text()).toMatchInterpolatedText("Gitlab Org's CI minutes"); expect(findTitle().text()).toMatchInterpolatedText("Gitlab Org's storage subscription");
}); });
}); });
describe('when quantity is greater than zero', () => { describe('when quantity is greater than zero', () => {
beforeEach(() => { beforeEach(() => {
createComponent({ const apolloProvider = createMockApolloProvider({ subscription: { quantity: 3 } });
subscription: { quantity: 3 }, createComponent(apolloProvider);
});
}); });
it('renders amount', () => { it('renders amount', () => {
expect(findAmount().text()).toBe('$30'); expect(findAmount().text()).toBe('$180');
}); });
}); });
describe('when quantity is less than or equal to zero', () => { describe('when quantity is less than or equal to zero', () => {
beforeEach(() => { beforeEach(() => {
createComponent({ const apolloProvider = createMockApolloProvider({
subscription: { quantity: 0 }, subscription: { quantity: 0 },
}); });
createComponent(apolloProvider);
}); });
it('does not render amount', () => { it('does not render amount', () => {
expect(findAmount().text()).toBe('-'); expect(findAmount().text()).toBe('-');
}); });
}); });
describe('when subscription has expiration date', () => {
describe('calls api that returns prorated amount', () => {
beforeEach(() => {
const orderPreviewQueryMock = jest
.fn()
.mockResolvedValue({ data: { orderPreview: mockOrderPreview } });
const apolloProvider = createMockApolloProvider(
{ subscription: { quantity: 1 } },
orderPreviewQueryMock,
);
createComponent(apolloProvider, { purchaseHasExpiration: true });
});
it('renders prorated amount', () => {
expect(findAmount().text()).toBe('$59.67');
});
});
describe('calls api that returns empty value', () => {
beforeEach(() => {
const orderPreviewQueryMock = jest.fn().mockResolvedValue({ data: { orderPreview: null } });
const apolloProvider = createMockApolloProvider(
{ subscription: { quantity: 1 } },
orderPreviewQueryMock,
);
createComponent(apolloProvider, { purchaseHasExpiration: true });
});
it('renders amount from the state', () => {
expect(findAmount().text()).toBe('$60');
});
});
describe('calls api that returns no data', () => {
beforeEach(() => {
jest.spyOn(console, 'error').mockImplementation(() => {});
const orderPreviewQueryMock = jest.fn().mockResolvedValue({ data: null });
const apolloProvider = createMockApolloProvider(
{ subscription: { quantity: 1 } },
orderPreviewQueryMock,
);
createComponent(apolloProvider, { purchaseHasExpiration: true });
});
it('renders amount from the state', () => {
expect(findAmount().text()).toBe('$60');
});
});
describe('when api is loading', () => {
beforeEach(() => {
const orderPreviewQueryMock = jest.fn().mockResolvedValue(new Promise(() => {}));
const apolloProvider = createMockApolloProvider(
{ subscription: { quantity: 1 } },
orderPreviewQueryMock,
);
createComponent(apolloProvider, { purchaseHasExpiration: true });
});
it('does not render amount when api is loading', () => {
expect(findAmount().text()).toBe('-');
});
});
});
}); });
...@@ -5,6 +5,7 @@ import { ...@@ -5,6 +5,7 @@ import {
PAYMENT_METHOD_TYPE, PAYMENT_METHOD_TYPE,
PLAN_TYPE, PLAN_TYPE,
SUBSCRIPTION_TYPE, SUBSCRIPTION_TYPE,
ORDER_PREVIEW_TYPE,
} from 'ee/subscriptions/buy_addons_shared/constants'; } from 'ee/subscriptions/buy_addons_shared/constants';
export const accountId = '111111111111'; export const accountId = '111111111111';
...@@ -50,6 +51,15 @@ export const mockDefaultCache = { ...@@ -50,6 +51,15 @@ export const mockDefaultCache = {
redirectAfterSuccess: '/', redirectAfterSuccess: '/',
}; };
export const mockOrderPreview = {
targetDate: '2022-12-15',
amount: 59.67,
amountWithoutTax: 60.0,
prorationCredit: 0.33,
priceBeforeProration: 60.0,
__typename: ORDER_PREVIEW_TYPE,
};
export const stateData = { export const stateData = {
eligibleNamespaces: [], eligibleNamespaces: [],
subscription: { subscription: {
......
import VueApollo from 'vue-apollo'; import VueApollo from 'vue-apollo';
import { writeInitialDataToApolloCache } from 'ee/subscriptions/buy_addons_shared/utils'; import { writeInitialDataToApolloCache } from 'ee/subscriptions/buy_addons_shared/utils';
import plansQuery from 'ee/subscriptions/graphql/queries/plans.customer.query.graphql'; import plansQuery from 'ee/subscriptions/graphql/queries/plans.customer.query.graphql';
import orderPreviewQuery from 'ee/subscriptions/graphql/queries/order_preview.customer.query.graphql';
import { createMockClient } from 'helpers/mock_apollo_helper'; import { createMockClient } from 'helpers/mock_apollo_helper';
import { CUSTOMERSDOT_CLIENT } from 'ee/subscriptions/buy_addons_shared/constants'; import { CUSTOMERSDOT_CLIENT } from 'ee/subscriptions/buy_addons_shared/constants';
import { mockCiMinutesPlans, mockDefaultCache } from 'ee_jest/subscriptions/mock_data'; import {
mockCiMinutesPlans,
mockDefaultCache,
mockOrderPreview,
} from 'ee_jest/subscriptions/mock_data';
export function createMockApolloProvider(mockResponses = {}, dataset = {}) { export function createMockApolloProvider(mockResponses = {}, dataset = {}) {
const { const {
plansQueryMock = jest.fn().mockResolvedValue({ data: { plans: mockCiMinutesPlans } }), plansQueryMock = jest.fn().mockResolvedValue({ data: { plans: mockCiMinutesPlans } }),
orderPreviewQueryMock = jest
.fn()
.mockResolvedValue({ data: { orderPreview: mockOrderPreview } }),
} = mockResponses; } = mockResponses;
const { quantity } = dataset; const { quantity } = dataset;
const mockDefaultClient = createMockClient(); const mockDefaultClient = createMockClient();
const mockCustomersDotClient = createMockClient([[plansQuery, plansQueryMock]]); const mockCustomersDotClient = createMockClient([
[plansQuery, plansQueryMock],
[orderPreviewQuery, orderPreviewQueryMock],
]);
const apolloProvider = new VueApollo({ const apolloProvider = new VueApollo({
defaultClient: mockDefaultClient, defaultClient: mockDefaultClient,
......
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