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 {
isAddon: true,
};
},
slotProps() {
return this.plan;
},
totalUnits() {
return this.quantity * this.config.quantityPerPack;
},
......
<script>
import { GlIcon, GlCollapse, GlCollapseToggleDirective } from '@gitlab/ui';
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 { CUSTOMERSDOT_CLIENT } from 'ee/subscriptions/buy_addons_shared/constants';
import formattingMixins from 'ee/subscriptions/new/formatting_mixins';
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';
export default {
......@@ -21,6 +26,9 @@ export default {
plan: {
type: Object,
required: true,
validator(value) {
return Object.prototype.hasOwnProperty.call(value, 'id');
},
},
title: {
type: String,
......@@ -39,6 +47,32 @@ export default {
const id = Number(data.selectedNamespaceId);
this.selectedNamespace = find(data.eligibleNamespaces, { id });
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 {
isBottomSummaryVisible: false,
selectedNamespace: {},
subscription: {},
endDate: '',
proratedAmount: 0,
hasError: false,
};
},
computed: {
......@@ -54,13 +91,15 @@ export default {
return this.plan.pricePerYear;
},
totalExVat() {
return this.subscription.quantity * this.selectedPlanPrice;
return this.isLoading
? 0
: this.proratedAmount || this.subscription.quantity * this.selectedPlanPrice;
},
vat() {
return TAX_RATE * this.totalExVat;
},
totalAmount() {
return this.totalExVat + this.vat;
return this.isLoading ? 0 : this.proratedAmount || this.totalExVat + this.vat;
},
quantityPresent() {
return this.subscription.quantity > 0;
......@@ -74,8 +113,8 @@ export default {
titleWithName() {
return sprintf(this.title, { name: this.namespaceName });
},
isVisible() {
return !this.$apollo.loading;
isLoading() {
return this.$apollo.loading;
},
},
taxRate: TAX_RATE,
......@@ -83,7 +122,6 @@ export default {
</script>
<template>
<div
v-if="isVisible"
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">
......@@ -95,7 +133,7 @@ export default {
<h4 data-testid="title">{{ titleWithName }}</h4>
</div>
<p class="gl-ml-3" data-testid="amount">
{{ formatAmount(totalAmount, quantityPresent) }}
{{ totalAmount ? formatAmount(totalAmount, quantityPresent) : '-' }}
</p>
</div>
</div>
......@@ -109,7 +147,7 @@ export default {
:total-amount="totalAmount"
:quantity="quantity"
:tax-rate="$options.taxRate"
:purchase-has-expiration="purchaseHasExpiration"
:subscription-end-date="endDate"
>
<template #price-per-unit="{ price }">
<slot name="price-per-unit" :price="price"></slot>
......@@ -133,7 +171,7 @@ export default {
:total-amount="totalAmount"
:quantity="quantity"
:tax-rate="$options.taxRate"
:purchase-has-expiration="purchaseHasExpiration"
:subscription-end-date="endDate"
>
<template #price-per-unit="{ price }">
<slot name="price-per-unit" :price="price"></slot>
......
......@@ -47,10 +47,10 @@ export default {
required: false,
default: null,
},
purchaseHasExpiration: {
type: Boolean,
subscriptionEndDate: {
type: String,
required: false,
default: false,
default: '',
},
},
data() {
......@@ -60,7 +60,9 @@ export default {
},
computed: {
endDate() {
return this.startDate.setFullYear(this.startDate.getFullYear() + 1);
return (
this.subscriptionEndDate || this.startDate.setFullYear(this.startDate.getFullYear() + 1)
);
},
hasPositiveQuantity() {
return this.quantity > 0;
......@@ -74,6 +76,9 @@ export default {
formattedPrice() {
return formatNumber(this.selectedPlanPrice);
},
renderedAmount() {
return this.totalExVat ? this.formatAmount(this.totalExVat, this.hasPositiveQuantity) : '-';
},
},
i18n: {
quantity: I18N_SUMMARY_QUANTITY,
......@@ -95,14 +100,14 @@ export default {
}}</span>
</div>
<div>
{{ formatAmount(totalExVat, hasPositiveQuantity) }}
{{ renderedAmount }}
</div>
</div>
<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">
<slot name="price-per-unit" :price="formattedPrice"></slot>
</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, {
startDate: formatDate(startDate),
......@@ -116,7 +121,7 @@ export default {
<div class="gl-display-flex gl-justify-content-space-between gl-text-gray-500">
<div>{{ $options.i18n.subtotal }}</div>
<div data-testid="total-ex-vat">
{{ formatAmount(totalExVat, hasPositiveQuantity) }}
{{ renderedAmount }}
</div>
</div>
<div class="gl-display-flex gl-justify-content-space-between gl-text-gray-500">
......@@ -141,7 +146,7 @@ export default {
>
<div>{{ $options.i18n.total }}</div>
<div data-testid="total-amount">
{{ formatAmount(totalAmount, hasPositiveQuantity) }}
{{ renderedAmount }}
</div>
</div>
</div>
......
......@@ -12,6 +12,7 @@ export const CUSTOMER_TYPE = 'Customer';
export const SUBSCRIPTION_TYPE = 'Subscription';
export const NAMESPACE_TYPE = 'Namespace';
export const PAYMENT_METHOD_TYPE = 'PaymentMethod';
export const ORDER_PREVIEW_TYPE = 'OrderPreview';
export const PLAN_TYPE = 'Plan';
export const STEP_TYPE = 'Step';
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', () => {
describe('when subscription has expiration', () => {
beforeEach(() => {
wrapper = createComponent({ purchaseHasExpiration: true });
wrapper = createComponent({ subscriptionEndDate: '2021-02-06' });
});
it('renders subscription period', () => {
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', () => {
beforeEach(() => {
wrapper = createComponent({ purchaseHasExpiration: false });
wrapper = createComponent({ subscriptionEndDate: '' });
});
it('does not render subscription period', () => {
......
......@@ -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 purchaseFlowResolvers from 'ee/vue_shared/purchase_flow/graphql/resolvers';
import {
mockCiMinutesPlans,
mockStoragePlans,
mockParsedNamespaces,
mockOrderPreview,
stateData as mockStateData,
} 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();
localVue.use(VueApollo);
......@@ -29,24 +33,25 @@ describe('Order Summary', () => {
const findAmount = () => wrapper.findByTestId('amount');
const findTitle = () => wrapper.findByTestId('title');
const createMockApolloProvider = (stateData = {}) => {
const createMockApolloProvider = (stateData = {}, mockRequest = {}) => {
const mockApollo = createMockApollo([], resolvers);
const data = merge({}, mockStateData, initialStateData, stateData);
mockApollo.clients.defaultClient.cache.writeQuery({
query: stateQuery,
data,
});
mockApollo.clients[CUSTOMERSDOT_CLIENT] = createMockClient([[orderPreviewQuery, mockRequest]]);
return mockApollo;
};
const createComponent = (stateData) => {
const apolloProvider = createMockApolloProvider(stateData);
const createComponent = (apolloProvider, props) => {
wrapper = shallowMountExtended(OrderSummary, {
localVue,
apolloProvider,
propsData: {
plan: mockCiMinutesPlans[0],
title: "%{name}'s CI minutes",
plan: mockStoragePlans[0],
title: "%{name}'s storage subscription",
...props,
},
});
};
......@@ -57,37 +62,101 @@ describe('Order Summary', () => {
describe('the default plan', () => {
beforeEach(() => {
createComponent({
subscription: { quantity: 1 },
});
const apolloProvider = createMockApolloProvider({ subscription: { quantity: 1 } });
createComponent(apolloProvider);
});
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', () => {
beforeEach(() => {
createComponent({
subscription: { quantity: 3 },
});
const apolloProvider = createMockApolloProvider({ subscription: { quantity: 3 } });
createComponent(apolloProvider);
});
it('renders amount', () => {
expect(findAmount().text()).toBe('$30');
expect(findAmount().text()).toBe('$180');
});
});
describe('when quantity is less than or equal to zero', () => {
beforeEach(() => {
createComponent({
const apolloProvider = createMockApolloProvider({
subscription: { quantity: 0 },
});
createComponent(apolloProvider);
});
it('does not render amount', () => {
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 {
PAYMENT_METHOD_TYPE,
PLAN_TYPE,
SUBSCRIPTION_TYPE,
ORDER_PREVIEW_TYPE,
} from 'ee/subscriptions/buy_addons_shared/constants';
export const accountId = '111111111111';
......@@ -50,6 +51,15 @@ export const mockDefaultCache = {
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 = {
eligibleNamespaces: [],
subscription: {
......
import VueApollo from 'vue-apollo';
import { writeInitialDataToApolloCache } from 'ee/subscriptions/buy_addons_shared/utils';
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 { 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 = {}) {
const {
plansQueryMock = jest.fn().mockResolvedValue({ data: { plans: mockCiMinutesPlans } }),
orderPreviewQueryMock = jest
.fn()
.mockResolvedValue({ data: { orderPreview: mockOrderPreview } }),
} = mockResponses;
const { quantity } = dataset;
const mockDefaultClient = createMockClient();
const mockCustomersDotClient = createMockClient([[plansQuery, plansQueryMock]]);
const mockCustomersDotClient = createMockClient([
[plansQuery, plansQueryMock],
[orderPreviewQuery, orderPreviewQueryMock],
]);
const apolloProvider = new VueApollo({
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