Commit d6dbe34f authored by Douglas Barbosa Alexandre's avatar Douglas Barbosa Alexandre

Merge branch 'ag/341564-subs-name' into 'master'

Retrieve and use (current) subscription name for finalising the purchase

See merge request gitlab-org/gitlab!71932
parents c4c7aa03 34a4563e
...@@ -16,15 +16,15 @@ export default { ...@@ -16,15 +16,15 @@ export default {
}, },
}, },
mounted() { mounted() {
this.updateSelectedPlanId(this.plan.id); this.updateSelectedPlan(this.plan);
}, },
methods: { methods: {
updateSelectedPlanId(planId) { updateSelectedPlan({ id, isAddon }) {
this.$apollo this.$apollo
.mutate({ .mutate({
mutation: updateState, mutation: updateState,
variables: { variables: {
input: { selectedPlanId: planId }, input: { selectedPlan: { id, isAddon } },
}, },
}) })
.catch((error) => { .catch((error) => {
......
...@@ -8,6 +8,11 @@ export const planTags = { ...@@ -8,6 +8,11 @@ export const planTags = {
/* eslint-enable @gitlab/require-i18n-strings */ /* eslint-enable @gitlab/require-i18n-strings */
export const CUSTOMERSDOT_CLIENT = 'customersDotClient'; export const CUSTOMERSDOT_CLIENT = 'customersDotClient';
export const GITLAB_CLIENT = 'gitlabClient'; export const GITLAB_CLIENT = 'gitlabClient';
export const CUSTOMER_TYPE = 'Customer';
export const SUBSCRIPTION_TYPE = 'Subscription';
export const NAMESPACE_TYPE = 'Namespace';
export const PAYMENT_METHOD_TYPE = 'PaymentMethod';
export const PLAN_TYPE = 'Plan';
export const CI_MINUTES_PER_PACK = 1000; export const CI_MINUTES_PER_PACK = 1000;
export const STORAGE_PER_PACK = 10; export const STORAGE_PER_PACK = 10;
......
import {
CUSTOMER_TYPE,
NAMESPACE_TYPE,
SUBSCRIPTION_TYPE,
PAYMENT_METHOD_TYPE,
PLAN_TYPE,
} from 'ee/subscriptions/buy_addons_shared/constants';
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 } from '~/lib/utils/common_utils'; import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
...@@ -11,26 +18,38 @@ function arrayToGraphqlArray(arr, typename) { ...@@ -11,26 +18,38 @@ function arrayToGraphqlArray(arr, typename) {
} }
export function writeInitialDataToApolloCache(apolloProvider, dataset) { export function writeInitialDataToApolloCache(apolloProvider, dataset) {
const { groupData, namespaceId, redirectAfterSuccess, subscriptionQuantity } = dataset; const {
activeSubscriptionName = '',
groupData,
namespaceId,
redirectAfterSuccess,
subscriptionQuantity,
} = dataset;
// eslint-disable-next-line @gitlab/require-i18n-strings const eligibleNamespaces = arrayToGraphqlArray(JSON.parse(groupData), NAMESPACE_TYPE);
const eligibleNamespaces = arrayToGraphqlArray(JSON.parse(groupData), 'Namespace');
const quantity = subscriptionQuantity || 1; const quantity = subscriptionQuantity || 1;
apolloProvider.clients.defaultClient.cache.writeQuery({ apolloProvider.clients.defaultClient.cache.writeQuery({
query: stateQuery, query: stateQuery,
data: { data: {
activeSubscription: {
name: activeSubscriptionName,
__typename: SUBSCRIPTION_TYPE,
},
isNewUser: false, isNewUser: false,
fullName: null, fullName: null,
isSetupForCompany: false, isSetupForCompany: false,
selectedPlanId: null, selectedPlan: {
id: null,
isAddon: false,
__typename: PLAN_TYPE,
},
eligibleNamespaces, eligibleNamespaces,
redirectAfterSuccess, redirectAfterSuccess,
selectedNamespaceId: namespaceId, selectedNamespaceId: namespaceId,
subscription: { subscription: {
quantity, quantity,
// eslint-disable-next-line @gitlab/require-i18n-strings __typename: SUBSCRIPTION_TYPE,
__typename: 'Subscription',
}, },
paymentMethod: { paymentMethod: {
id: null, id: null,
...@@ -38,7 +57,7 @@ export function writeInitialDataToApolloCache(apolloProvider, dataset) { ...@@ -38,7 +57,7 @@ export function writeInitialDataToApolloCache(apolloProvider, dataset) {
creditCardExpirationYear: null, creditCardExpirationYear: null,
creditCardType: null, creditCardType: null,
creditCardMaskNumber: null, creditCardMaskNumber: null,
__typename: 'PaymentMethod', __typename: PAYMENT_METHOD_TYPE,
}, },
customer: { customer: {
country: null, country: null,
...@@ -48,8 +67,7 @@ export function writeInitialDataToApolloCache(apolloProvider, dataset) { ...@@ -48,8 +67,7 @@ export function writeInitialDataToApolloCache(apolloProvider, dataset) {
state: null, state: null,
zipCode: null, zipCode: null,
company: null, company: null,
// eslint-disable-next-line @gitlab/require-i18n-strings __typename: CUSTOMER_TYPE,
__typename: 'Customer',
}, },
activeStep: STEPS[0], activeStep: STEPS[0],
stepList: STEPS, stepList: STEPS,
......
...@@ -55,6 +55,14 @@ export default { ...@@ -55,6 +55,14 @@ export default {
hasError: false, hasError: false,
}; };
}, },
computed: {
plan() {
return {
...this.plans[0],
isAddon: true,
};
},
},
methods: { methods: {
isQuantityValid(quantity) { isQuantityValid(quantity) {
return Number.isFinite(quantity) && quantity > 0; return Number.isFinite(quantity) && quantity > 0;
...@@ -102,7 +110,6 @@ export default { ...@@ -102,7 +110,6 @@ export default {
this.hasError = true; this.hasError = true;
return null; return null;
} }
return data.plans; return data.plans;
}, },
error(error) { error(error) {
...@@ -122,7 +129,7 @@ export default { ...@@ -122,7 +129,7 @@ export default {
/> />
<step-order-app v-else-if="!$apollo.loading"> <step-order-app v-else-if="!$apollo.loading">
<template #checkout> <template #checkout>
<checkout :plan="plans[0]"> <checkout :plan="plan">
<template #purchase-details> <template #purchase-details>
<addon-purchase-details <addon-purchase-details
:product-label="$options.i18n.productLabel" :product-label="$options.i18n.productLabel"
...@@ -145,7 +152,7 @@ export default { ...@@ -145,7 +152,7 @@ export default {
</checkout> </checkout>
</template> </template>
<template #order-summary> <template #order-summary>
<order-summary :plan="plans[0]" :title="$options.i18n.title"> <order-summary :plan="plan" :title="$options.i18n.title">
<template #price-per-unit="{ price }"> <template #price-per-unit="{ price }">
{{ pricePerUnitLabel(price) }} {{ pricePerUnitLabel(price) }}
</template> </template>
......
...@@ -5,10 +5,16 @@ query State { ...@@ -5,10 +5,16 @@ query State {
name name
users users
} }
activeSubscription @client {
name
}
isNewUser @client isNewUser @client
fullName @client fullName @client
isSetupForCompany @client isSetupForCompany @client
selectedPlanId @client selectedPlan @client {
id
isAddon
}
selectedNamespaceId @client selectedNamespaceId @client
redirectAfterSuccess @client redirectAfterSuccess @client
customer @client { customer @client {
......
...@@ -35,11 +35,13 @@ export default { ...@@ -35,11 +35,13 @@ export default {
}, },
update(data) { update(data) {
const { customer } = data; const { customer } = data;
const { name } = data.activeSubscription;
return { return {
setup_for_company: data.isSetupForCompany, setup_for_company: data.isSetupForCompany,
selected_group: data.selectedNamespaceId, selected_group: data.selectedNamespaceId,
new_user: data.isNewUser, new_user: data.isNewUser,
redirect_after_success: data.redirectAfterSuccess, redirect_after_success: data.redirectAfterSuccess,
active_subscription: name,
customer: { customer: {
country: customer.country, country: customer.country,
address_1: customer.address1, address_1: customer.address1,
...@@ -50,9 +52,10 @@ export default { ...@@ -50,9 +52,10 @@ export default {
company: customer.company, company: customer.company,
}, },
subscription: { subscription: {
plan_id: data.selectedPlanId,
payment_method_id: data.paymentMethod.id,
quantity: data.subscription.quantity, quantity: data.subscription.quantity,
is_addon: data.selectedPlan.isAddon,
plan_id: data.selectedPlan.id,
payment_method_id: data.paymentMethod.id,
}, },
}; };
}, },
......
...@@ -36,11 +36,12 @@ class SubscriptionsController < ApplicationController ...@@ -36,11 +36,12 @@ class SubscriptionsController < ApplicationController
return render_404 unless ci_minutes_plan_data.present? return render_404 unless ci_minutes_plan_data.present?
# At the moment of this comment the account id is directly available to the view. # At the moment of this comment the account id is directly available to the view.
# This might change in the future given the intention to associate the account id to the namespace. # This might change in the future given the intention to associate the account id to the namespace.
# See: https://gitlab.com/gitlab-org/gitlab/-/issues/338546#note_684762160 # See: https://gitlab.com/gitlab-org/gitlab/-/issues/338546#note_684762160
result = find_group(plan_id: ci_minutes_plan_data["id"]) result = find_group(plan_id: ci_minutes_plan_data["id"])
@group = result[:namespace] @group = result[:namespace]
@account_id = result[:account_id] @account_id = result[:account_id]
@active_subscription = result[:active_subscription]
return render_404 if @group.nil? return render_404 if @group.nil?
...@@ -51,11 +52,12 @@ class SubscriptionsController < ApplicationController ...@@ -51,11 +52,12 @@ class SubscriptionsController < ApplicationController
return render_404 unless storage_plan_data.present? return render_404 unless storage_plan_data.present?
# At the moment of this comment the account id is directly available to the view. # At the moment of this comment the account id is directly available to the view.
# This might change in the future given the intention to associate the account id to the namespace. # This might change in the future given the intention to associate the account id to the namespace.
# See: https://gitlab.com/gitlab-org/gitlab/-/issues/338546#note_684762160 # See: https://gitlab.com/gitlab-org/gitlab/-/issues/338546#note_684762160
result = find_group(plan_id: storage_plan_data["id"]) result = find_group(plan_id: storage_plan_data["id"])
@group = result[:namespace] @group = result[:namespace]
@account_id = result[:account_id] @account_id = result[:account_id]
@active_subscription = result[:active_subscription]
return render_404 if @group.nil? return render_404 if @group.nil?
...@@ -110,7 +112,7 @@ class SubscriptionsController < ApplicationController ...@@ -110,7 +112,7 @@ class SubscriptionsController < ApplicationController
end end
def subscription_params def subscription_params
params.require(:subscription).permit(:plan_id, :payment_method_id, :quantity, :source) params.require(:subscription).permit(:plan_id, :is_addon, :payment_method_id, :quantity, :source).merge(params.permit(:active_subscription))
end end
def find_group(plan_id:) def find_group(plan_id:)
......
...@@ -16,8 +16,9 @@ module SubscriptionsHelper ...@@ -16,8 +16,9 @@ module SubscriptionsHelper
} }
end end
def buy_addon_data(group, account_id, anchor, purchased_product) def buy_addon_data(group, account_id, active_subscription, anchor, purchased_product)
{ {
active_subscription: active_subscription,
group_data: [present_group(group, account_id)].to_json, group_data: [present_group(group, account_id)].to_json,
namespace_id: params[:selected_group], namespace_id: params[:selected_group],
redirect_after_success: group_usage_quotas_path(group, anchor: anchor, purchased_product: purchased_product), redirect_after_success: group_usage_quotas_path(group, anchor: anchor, purchased_product: purchased_product),
......
...@@ -25,10 +25,14 @@ module GitlabSubscriptions ...@@ -25,10 +25,14 @@ module GitlabSubscriptions
return missing_plan_error if plan_id.nil? && any_self_service_plan.nil? return missing_plan_error if plan_id.nil? && any_self_service_plan.nil?
if response[:success] && response[:data] if response[:success] && response[:data]
eligible_namespaces = response[:data].to_h { |data| [data["id"], data["accountId"]] } eligible_namespaces = response[:data].to_h { |data| [data["id"], [data["accountId"], data['subscription']]] }
data = namespaces.each_with_object([]) do |namespace, acc| data = namespaces.each_with_object([]) do |namespace, acc|
if eligible_namespaces.include?(namespace.id) if eligible_namespaces.include?(namespace.id)
acc << { namespace: namespace, account_id: eligible_namespaces[namespace.id] } acc << {
namespace: namespace,
account_id: eligible_namespaces[namespace.id][0],
active_subscription: eligible_namespaces[namespace.id][1]
}
end end
end end
......
...@@ -25,10 +25,7 @@ module Subscriptions ...@@ -25,10 +25,7 @@ module Subscriptions
# We can't use an email from GL.com because it may differ from the billing email. # We can't use an email from GL.com because it may differ from the billing email.
# Instead we use the email received from the CustomersDot as a billing email. # Instead we use the email received from the CustomersDot as a billing email.
customer_data = response.with_indifferent_access[:data][:customer] customer_data = response.with_indifferent_access[:data][:customer]
billing_email = customer_data[:email] response = create_subscription(customer_data)
token = customer_data[:authentication_token]
response = client.create_subscription(create_subscription_params, billing_email, token)
OnboardingProgressService.new(@group).execute(action: :subscription_created) if response[:success] OnboardingProgressService.new(@group).execute(action: :subscription_created) if response[:success]
...@@ -73,15 +70,20 @@ module Subscriptions ...@@ -73,15 +70,20 @@ module Subscriptions
} }
end end
def create_subscription_params def create_subscription(customer_data)
# When purchasing an add on, we don't want to send create_subscription_params
# in order to avoid amending the main product. Note that this will go away
# when fully transitioning the flow to GraphQL
create_params = add_on? ? create_addon_params : create_subscription_params
billing_email, token = customer_data.values_at(:email, :authentication_token)
client.create_subscription(create_params, billing_email, token)
end
def create_params
{ {
plan_id: subscription_params[:plan_id], plan_id: subscription_params[:plan_id],
payment_method_id: subscription_params[:payment_method_id], payment_method_id: subscription_params[:payment_method_id],
products: {
main: {
quantity: subscription_params[:quantity]
}
},
gl_namespace_id: @group.id, gl_namespace_id: @group.id,
gl_namespace_name: @group.name, gl_namespace_name: @group.name,
preview: 'false', preview: 'false',
...@@ -89,6 +91,27 @@ module Subscriptions ...@@ -89,6 +91,27 @@ module Subscriptions
} }
end end
def create_addon_params
{
active_subscription: subscription_params[:active_subscription],
quantity: subscription_params[:quantity]
}.merge(create_params)
end
def create_subscription_params
{
products: {
main: {
quantity: subscription_params[:quantity]
}
}
}.merge(create_params)
end
def add_on?
Gitlab::Utils.to_boolean(subscription_params[:is_addon], default: false)
end
def country_code(country) def country_code(country)
World.alpha3_from_alpha2(country) World.alpha3_from_alpha2(country)
end end
......
...@@ -2,4 +2,4 @@ ...@@ -2,4 +2,4 @@
- content_for :page_specific_javascripts do - content_for :page_specific_javascripts do
= render "layouts/one_trust" = render "layouts/one_trust"
#js-buy-minutes{ data: buy_addon_data(@group, @account_id, 'pipelines-quota-tab', s_('Checkout|CI minutes')) } #js-buy-minutes{ data: buy_addon_data(@group, @account_id, @active_subscription, 'pipelines-quota-tab', s_('Checkout|CI minutes')) }
...@@ -2,4 +2,4 @@ ...@@ -2,4 +2,4 @@
- content_for :page_specific_javascripts do - content_for :page_specific_javascripts do
= render "layouts/one_trust" = render "layouts/one_trust"
#js-buy-storage{ data: buy_addon_data(@group, @account_id, 'storage-quota-tab', s_('Checkout|a storage subscription')) } #js-buy-storage{ data: buy_addon_data(@group, @account_id, @active_subscription, 'storage-quota-tab', s_('Checkout|a storage subscription')) }
...@@ -124,6 +124,7 @@ module Gitlab ...@@ -124,6 +124,7 @@ module Gitlab
namespaceEligibility(customerUid: $customerUid, namespaces: $namespaces, planId: $planId, eligibleForPurchase: $eligibleForPurchase) { namespaceEligibility(customerUid: $customerUid, namespaces: $namespaces, planId: $planId, eligibleForPurchase: $eligibleForPurchase) {
id id
accountId: zuoraAccountId accountId: zuoraAccountId
subscription { name }
} }
} }
GQL GQL
......
{ [
"customer": { {
"provider": "gitlab",
"uid": 111,
"credentials": {
"token": "foo_token"
},
"customer": { "customer": {
"country": "NLD", "provider": "gitlab",
"address_1": "Address line 1", "uid": 111,
"address_2": "Address line 2", "credentials": {
"city": "City", "token": "foo_token"
"state": "State", },
"zip_code": "Zip code", "customer": {
"company": "My organization" "country": "NLD",
"address_1": "Address line 1",
"address_2": "Address line 2",
"city": "City",
"state": "State",
"zip_code": "Zip code",
"company": "My organization"
},
"info": {
"first_name": "First name",
"last_name": "Last name",
"email": "first.last@gitlab.com"
}
}, },
"info": { "subscription": {
"first_name": "First name", "plan_id": "Plan ID",
"last_name": "Last name", "payment_method_id": "Payment method ID",
"email": "first.last@gitlab.com" "products": {
"main": {
"quantity": 123
}
},
"gl_namespace_id": 222,
"gl_namespace_name": "Group name",
"preview": "false",
"source": "some_source"
} }
}, },
"subscription": { {
"plan_id": "Plan ID", "customer": {
"payment_method_id": "Payment method ID", "provider": "gitlab",
"products": { "uid": 111,
"main": { "credentials": {
"quantity": 123 "token": "foo_token"
},
"customer": {
"country": "NLD",
"address_1": "Address line 1",
"address_2": "Address line 2",
"city": "City",
"state": "State",
"zip_code": "Zip code",
"company": "My organization"
},
"info": {
"first_name": "First name",
"last_name": "Last name",
"email": "first.last@gitlab.com"
} }
}, },
"gl_namespace_id": 222, "subscription": {
"gl_namespace_name": "Group name", "plan_id": "Add-on Plan ID",
"preview": "false", "payment_method_id": "Payment method ID",
"source": "some_source" "quantity": 111,
"active_subscription": "A-000000",
"gl_namespace_id": 222,
"gl_namespace_name": "Group name",
"preview": "false",
"source": "some_source"
}
} }
} ]
...@@ -59,7 +59,6 @@ describe('Order Summary', () => { ...@@ -59,7 +59,6 @@ describe('Order Summary', () => {
beforeEach(() => { beforeEach(() => {
createComponent({ createComponent({
subscription: { quantity: 1 }, subscription: { quantity: 1 },
selectedPlanId: 'ciMinutesPackPlanId',
}); });
}); });
......
import { GlEmptyState } from '@gitlab/ui'; import { GlEmptyState } from '@gitlab/ui';
import { createLocalVue } from '@vue/test-utils'; import { createLocalVue } from '@vue/test-utils';
import VueApollo from 'vue-apollo'; import VueApollo from 'vue-apollo';
import { pick } from 'lodash';
import { I18N_CI_MINUTES_TITLE, planTags } from 'ee/subscriptions/buy_addons_shared/constants';
import Checkout from 'ee/subscriptions/buy_addons_shared/components/checkout.vue'; import Checkout from 'ee/subscriptions/buy_addons_shared/components/checkout.vue';
import AddonPurchaseDetails from 'ee/subscriptions/buy_addons_shared/components/checkout/addon_purchase_details.vue'; import AddonPurchaseDetails from 'ee/subscriptions/buy_addons_shared/components/checkout/addon_purchase_details.vue';
import OrderSummary from 'ee/subscriptions/buy_addons_shared/components/order_summary.vue'; import OrderSummary from 'ee/subscriptions/buy_addons_shared/components/order_summary.vue';
...@@ -9,8 +11,8 @@ import App from 'ee/subscriptions/buy_minutes/components/app.vue'; ...@@ -9,8 +11,8 @@ import App from 'ee/subscriptions/buy_minutes/components/app.vue';
import StepOrderApp from 'ee/vue_shared/purchase_flow/components/step_order_app.vue'; import StepOrderApp from 'ee/vue_shared/purchase_flow/components/step_order_app.vue';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises'; import waitForPromises from 'helpers/wait_for_promises';
import { planTags } from '../../../../../app/assets/javascripts/subscriptions/buy_addons_shared/constants';
import { createMockApolloProvider } from '../spec_helper'; import { createMockApolloProvider } from '../spec_helper';
import { mockCiMinutesPlans } from '../mock_data';
const localVue = createLocalVue(); const localVue = createLocalVue();
localVue.use(VueApollo); localVue.use(VueApollo);
...@@ -32,24 +34,42 @@ describe('App', () => { ...@@ -32,24 +34,42 @@ describe('App', () => {
}); });
} }
const getCiMinutePlan = () => pick(mockCiMinutesPlans[0], ['id', 'code', 'pricePerYear', 'name']);
const findCheckout = () => wrapper.findComponent(Checkout);
const findOrderSummary = () => wrapper.findComponent(OrderSummary);
const findPriceLabel = () => wrapper.findByTestId('price-per-unit');
const findQuantityText = () => wrapper.findByTestId('addon-quantity-text'); const findQuantityText = () => wrapper.findByTestId('addon-quantity-text');
const findSummaryLabel = () => wrapper.findByTestId('summary-label'); const findSummaryLabel = () => wrapper.findByTestId('summary-label');
const findSummaryTotal = () => wrapper.findByTestId('summary-total'); const findSummaryTotal = () => wrapper.findByTestId('summary-total');
const findPriceLabel = () => wrapper.findByTestId('price-per-unit');
afterEach(() => { afterEach(() => {
wrapper.destroy(); wrapper.destroy();
}); });
describe('when data is received', () => { describe('when data is received', () => {
it('should display the StepOrderApp', async () => { beforeEach(() => {
const mockApollo = createMockApolloProvider(); const mockApollo = createMockApolloProvider();
wrapper = createComponent(mockApollo); wrapper = createComponent(mockApollo);
await waitForPromises(); return waitForPromises();
});
it('should display the StepOrderApp', () => {
expect(wrapper.findComponent(StepOrderApp).exists()).toBe(true); expect(wrapper.findComponent(StepOrderApp).exists()).toBe(true);
expect(wrapper.findComponent(GlEmptyState).exists()).toBe(false); expect(wrapper.findComponent(GlEmptyState).exists()).toBe(false);
}); });
it('provides the correct props to checkout', () => {
expect(findCheckout().props()).toMatchObject({
plan: { ...getCiMinutePlan, isAddon: true },
});
});
it('provides the correct props to order summary', () => {
expect(findOrderSummary().props()).toMatchObject({
plan: { ...getCiMinutePlan, isAddon: true },
title: I18N_CI_MINUTES_TITLE,
});
});
}); });
describe('when data is not received', () => { describe('when data is not received', () => {
......
...@@ -6,7 +6,10 @@ import { STEPS } from 'ee/subscriptions/constants'; ...@@ -6,7 +6,10 @@ 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 ConfirmOrder from 'ee/vue_shared/purchase_flow/components/checkout/confirm_order.vue'; import ConfirmOrder from 'ee/vue_shared/purchase_flow/components/checkout/confirm_order.vue';
import { GENERAL_ERROR_MESSAGE } from 'ee/vue_shared/purchase_flow/constants'; import { GENERAL_ERROR_MESSAGE } from 'ee/vue_shared/purchase_flow/constants';
import { stateData as initialStateData } from 'ee_jest/subscriptions/buy_minutes/mock_data'; import {
stateData as initialStateData,
subscriptionName,
} from 'ee_jest/subscriptions/buy_minutes/mock_data';
import { createMockApolloProvider } from 'ee_jest/vue_shared/purchase_flow/spec_helper'; import { createMockApolloProvider } from 'ee_jest/vue_shared/purchase_flow/spec_helper';
import { extendedWrapper } from 'helpers/vue_test_utils_helper'; import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import flash from '~/flash'; import flash from '~/flash';
...@@ -40,17 +43,13 @@ describe('Confirm Order', () => { ...@@ -40,17 +43,13 @@ describe('Confirm Order', () => {
wrapper.destroy(); wrapper.destroy();
}); });
describe('Active', () => { describe('when rendering', () => {
describe('when receiving proper step data', () => { describe('when receiving proper step data', () => {
beforeEach(() => { beforeEach(() => {
mockApolloProvider = createMockApolloProvider(STEPS, 3); mockApolloProvider = createMockApolloProvider(STEPS, 3);
createComponent({ apolloProvider: mockApolloProvider }); createComponent({ apolloProvider: mockApolloProvider });
}); });
it('button should be visible', () => {
expect(findConfirmButton().exists()).toBe(true);
});
it('shows the text "Confirm purchase"', () => { it('shows the text "Confirm purchase"', () => {
expect(findConfirmButton().text()).toBe('Confirm purchase'); expect(findConfirmButton().text()).toBe('Confirm purchase');
}); });
...@@ -60,7 +59,7 @@ describe('Confirm Order', () => { ...@@ -60,7 +59,7 @@ describe('Confirm Order', () => {
}); });
}); });
describe('Clicking the button', () => { describe('when confirming the order', () => {
beforeEach(() => { beforeEach(() => {
mockApolloProvider = createMockApolloProvider([]); mockApolloProvider = createMockApolloProvider([]);
mockApolloProvider.clients.defaultClient.cache.writeQuery({ mockApolloProvider.clients.defaultClient.cache.writeQuery({
...@@ -74,9 +73,10 @@ describe('Confirm Order', () => { ...@@ -74,9 +73,10 @@ describe('Confirm Order', () => {
it('calls the confirmOrder API method with the correct params', () => { it('calls the confirmOrder API method with the correct params', () => {
expect(Api.confirmOrder).toHaveBeenCalledTimes(1); expect(Api.confirmOrder).toHaveBeenCalledTimes(1);
expect(Api.confirmOrder.mock.calls[0][0]).toMatchObject({ expect(Api.confirmOrder.mock.calls[0][0]).toEqual({
setup_for_company: true, setup_for_company: true,
selected_group: '30', selected_group: '30',
active_subscription: subscriptionName,
new_user: false, new_user: false,
redirect_after_success: '/path/to/redirect/', redirect_after_success: '/path/to/redirect/',
customer: { customer: {
...@@ -90,6 +90,7 @@ describe('Confirm Order', () => { ...@@ -90,6 +90,7 @@ describe('Confirm Order', () => {
}, },
subscription: { subscription: {
plan_id: null, plan_id: null,
is_addon: true,
payment_method_id: null, payment_method_id: null,
quantity: 1, quantity: 1,
}, },
...@@ -105,35 +106,33 @@ describe('Confirm Order', () => { ...@@ -105,35 +106,33 @@ describe('Confirm Order', () => {
}); });
}); });
describe('Order confirmation', () => { describe('when confirming the purchase', () => {
describe('when the confirmation succeeds', () => { const location = 'group/location/path';
const location = 'group/location/path';
beforeEach(() => { beforeEach(() => {
mockApolloProvider = createMockApolloProvider(STEPS, 3); mockApolloProvider = createMockApolloProvider(STEPS, 3);
createComponent({ apolloProvider: mockApolloProvider }); createComponent({ apolloProvider: mockApolloProvider });
}); });
it('should redirect to the location', async () => { it('redirects to the location if it succeeds', async () => {
Api.confirmOrder = jest.fn().mockResolvedValueOnce({ data: { location } }); Api.confirmOrder = jest.fn().mockResolvedValueOnce({ data: { location } });
findConfirmButton().vm.$emit('click'); findConfirmButton().vm.$emit('click');
await flushPromises(); await flushPromises();
expect(UrlUtility.redirectTo).toHaveBeenCalledTimes(1); expect(UrlUtility.redirectTo).toHaveBeenCalledTimes(1);
expect(UrlUtility.redirectTo).toHaveBeenCalledWith(location); expect(UrlUtility.redirectTo).toHaveBeenCalledWith(location);
}); });
it('shows an error if it fails', async () => {
const errors = 'an error';
Api.confirmOrder = jest.fn().mockResolvedValueOnce({ data: { errors } });
findConfirmButton().vm.$emit('click');
await flushPromises();
it('shows an error', async () => { expect(flash.mock.calls[0][0]).toMatchObject({
const errors = 'an error'; message: GENERAL_ERROR_MESSAGE,
Api.confirmOrder = jest.fn().mockResolvedValueOnce({ data: { errors } }); captureError: true,
findConfirmButton().vm.$emit('click'); error: new Error(JSON.stringify(errors)),
await flushPromises();
expect(flash.mock.calls[0][0]).toMatchObject({
message: GENERAL_ERROR_MESSAGE,
captureError: true,
error: new Error(JSON.stringify(errors)),
});
}); });
}); });
}); });
...@@ -163,7 +162,7 @@ describe('Confirm Order', () => { ...@@ -163,7 +162,7 @@ describe('Confirm Order', () => {
}); });
}); });
describe('Inactive', () => { describe('when inactive', () => {
it('does not show buttons', () => { it('does not show buttons', () => {
mockApolloProvider = createMockApolloProvider(STEPS, 1); mockApolloProvider = createMockApolloProvider(STEPS, 1);
createComponent({ apolloProvider: mockApolloProvider }); createComponent({ apolloProvider: mockApolloProvider });
......
import { STEPS } from 'ee/subscriptions/constants'; import { STEPS } from 'ee/subscriptions/constants';
import {
CUSTOMER_TYPE,
NAMESPACE_TYPE,
PAYMENT_METHOD_TYPE,
PLAN_TYPE,
SUBSCRIPTION_TYPE,
} from 'ee/subscriptions/buy_addons_shared/constants';
export const accountId = '111111111111'; export const accountId = '111111111111';
export const subscriptionName = 'A-000000000';
export const mockCiMinutesPlans = [ export const mockCiMinutesPlans = [
{ {
...@@ -8,7 +16,7 @@ export const mockCiMinutesPlans = [ ...@@ -8,7 +16,7 @@ export const mockCiMinutesPlans = [
code: 'ci_minutes', code: 'ci_minutes',
pricePerYear: 10, pricePerYear: 10,
name: 'CI minutes pack', name: 'CI minutes pack',
__typename: 'Plan', __typename: PLAN_TYPE,
}, },
]; ];
...@@ -19,7 +27,7 @@ export const mockNamespaces = ` ...@@ -19,7 +27,7 @@ export const mockNamespaces = `
export const mockParsedNamespaces = JSON.parse(mockNamespaces).map((namespace) => ({ export const mockParsedNamespaces = JSON.parse(mockNamespaces).map((namespace) => ({
...namespace, ...namespace,
__typename: 'Namespace', __typename: NAMESPACE_TYPE,
})); }));
export const mockNewUser = 'false'; export const mockNewUser = 'false';
...@@ -35,18 +43,25 @@ export const stateData = { ...@@ -35,18 +43,25 @@ export const stateData = {
eligibleNamespaces: [], eligibleNamespaces: [],
subscription: { subscription: {
quantity: 1, quantity: 1,
__typename: 'Subscription', __typename: SUBSCRIPTION_TYPE,
},
activeSubscription: {
name: subscriptionName,
__typename: SUBSCRIPTION_TYPE,
}, },
redirectAfterSuccess: '/path/to/redirect/', redirectAfterSuccess: '/path/to/redirect/',
selectedNamespaceId: '30', selectedNamespaceId: '30',
selectedPlanId: null, selectedPlan: {
id: null,
isAddon: true,
},
paymentMethod: { paymentMethod: {
id: null, id: null,
creditCardExpirationMonth: null, creditCardExpirationMonth: null,
creditCardExpirationYear: null, creditCardExpirationYear: null,
creditCardType: null, creditCardType: null,
creditCardMaskNumber: null, creditCardMaskNumber: null,
__typename: 'PaymentMethod', __typename: PAYMENT_METHOD_TYPE,
}, },
customer: { customer: {
country: null, country: null,
...@@ -56,7 +71,7 @@ export const stateData = { ...@@ -56,7 +71,7 @@ export const stateData = {
state: null, state: null,
zipCode: null, zipCode: null,
company: null, company: null,
__typename: 'Customer', __typename: CUSTOMER_TYPE,
}, },
fullName: 'Full Name', fullName: 'Full Name',
isNewUser: false, isNewUser: false,
......
...@@ -134,11 +134,12 @@ RSpec.describe SubscriptionsHelper do ...@@ -134,11 +134,12 @@ RSpec.describe SubscriptionsHelper do
end end
describe '#buy_addon_data' do describe '#buy_addon_data' do
subject(:buy_addon_data) { helper.buy_addon_data(group, account_id, anchor, purchased_product) } subject(:buy_addon_data) { helper.buy_addon_data(group, account_id, active_subscription, anchor, purchased_product) }
let_it_be(:group) { create(:group, name: 'My Namespace') } let_it_be(:group) { create(:group, name: 'My Namespace') }
let_it_be(:user) { create(:user, name: 'First Last') } let_it_be(:user) { create(:user, name: 'First Last') }
let_it_be(:account_id) { '111111111111' } let_it_be(:account_id) { '111111111111' }
let_it_be(:active_subscription) { { name: 'S-000000000' } }
let(:anchor) { 'pipelines-quota-tab' } let(:anchor) { 'pipelines-quota-tab' }
let(:purchased_product) { 'CI Minutes' } let(:purchased_product) { 'CI Minutes' }
...@@ -150,6 +151,7 @@ RSpec.describe SubscriptionsHelper do ...@@ -150,6 +151,7 @@ RSpec.describe SubscriptionsHelper do
end end
it { is_expected.to include(namespace_id: group.id.to_s) } it { is_expected.to include(namespace_id: group.id.to_s) }
it { is_expected.to include(active_subscription: active_subscription) }
it { is_expected.to include(source: 'some_source') } it { is_expected.to include(source: 'some_source') }
it { is_expected.to include(group_data: %Q{[{"id":#{group.id},"account_id":"#{account_id}","name":"My Namespace","users":1,"guests":0}]}) } it { is_expected.to include(group_data: %Q{[{"id":#{group.id},"account_id":"#{account_id}","name":"My Namespace","users":1,"guests":0}]}) }
it { is_expected.to include(redirect_after_success: group_usage_quotas_path(group, anchor: anchor, purchased_product: purchased_product)) } it { is_expected.to include(redirect_after_success: group_usage_quotas_path(group, anchor: anchor, purchased_product: purchased_product)) }
......
...@@ -323,6 +323,7 @@ RSpec.describe Gitlab::SubscriptionPortal::Clients::Graphql do ...@@ -323,6 +323,7 @@ RSpec.describe Gitlab::SubscriptionPortal::Clients::Graphql do
namespaceEligibility(customerUid: $customerUid, namespaces: $namespaces, planId: $planId, eligibleForPurchase: $eligibleForPurchase) { namespaceEligibility(customerUid: $customerUid, namespaces: $namespaces, planId: $planId, eligibleForPurchase: $eligibleForPurchase) {
id id
accountId: zuoraAccountId accountId: zuoraAccountId
subscription { name }
} }
} }
GQL GQL
......
...@@ -67,13 +67,15 @@ RSpec.describe GitlabSubscriptions::FetchPurchaseEligibleNamespacesService do ...@@ -67,13 +67,15 @@ RSpec.describe GitlabSubscriptions::FetchPurchaseEligibleNamespacesService do
end end
context 'when all the namespaces are eligible' do context 'when all the namespaces are eligible' do
let(:subscription_name) { 'S-000000' }
before do before do
allow(Gitlab::SubscriptionPortal::Client) allow(Gitlab::SubscriptionPortal::Client)
.to receive(:filter_purchase_eligible_namespaces) .to receive(:filter_purchase_eligible_namespaces)
.with(user, [namespace_1, namespace_2], plan_id: 'test', any_self_service_plan: nil) .with(user, [namespace_1, namespace_2], plan_id: 'test', any_self_service_plan: nil)
.and_return(success: true, data: [ .and_return(success: true, data: [
{ 'id' => namespace_1.id, 'accountId' => nil }, { 'id' => namespace_1.id, 'accountId' => nil, 'subscription' => { 'name' => subscription_name } },
{ 'id' => namespace_2.id, 'accountId' => nil } { 'id' => namespace_2.id, 'accountId' => nil, 'subscription' => nil }
]) ])
end end
...@@ -83,8 +85,8 @@ RSpec.describe GitlabSubscriptions::FetchPurchaseEligibleNamespacesService do ...@@ -83,8 +85,8 @@ RSpec.describe GitlabSubscriptions::FetchPurchaseEligibleNamespacesService do
expect(result).to be_success expect(result).to be_success
expect(result.payload).to match_array [ expect(result.payload).to match_array [
namespace_result(namespace_1, nil), namespace_result(namespace_1, nil, { 'name' => subscription_name }),
namespace_result(namespace_2, nil) namespace_result(namespace_2, nil, nil)
] ]
end end
end end
...@@ -105,7 +107,7 @@ RSpec.describe GitlabSubscriptions::FetchPurchaseEligibleNamespacesService do ...@@ -105,7 +107,7 @@ RSpec.describe GitlabSubscriptions::FetchPurchaseEligibleNamespacesService do
expect(result).to be_success expect(result).to be_success
expect(result.payload).to match_array [ expect(result.payload).to match_array [
namespace_result(namespace_1, account_id) namespace_result(namespace_1, account_id, nil)
] ]
end end
end end
...@@ -124,7 +126,7 @@ RSpec.describe GitlabSubscriptions::FetchPurchaseEligibleNamespacesService do ...@@ -124,7 +126,7 @@ RSpec.describe GitlabSubscriptions::FetchPurchaseEligibleNamespacesService do
expect(result).to be_success expect(result).to be_success
expect(result.payload).to match_array [ expect(result.payload).to match_array [
namespace_result(namespace_1, nil) namespace_result(namespace_1, nil, nil)
] ]
end end
end end
...@@ -132,7 +134,7 @@ RSpec.describe GitlabSubscriptions::FetchPurchaseEligibleNamespacesService do ...@@ -132,7 +134,7 @@ RSpec.describe GitlabSubscriptions::FetchPurchaseEligibleNamespacesService do
private private
def namespace_result(namespace, account_id) def namespace_result(namespace, account_id, active_subscription)
{ namespace: namespace, account_id: account_id } { namespace: namespace, account_id: account_id, active_subscription: active_subscription }
end end
end end
...@@ -32,7 +32,7 @@ RSpec.describe Subscriptions::CreateService do ...@@ -32,7 +32,7 @@ RSpec.describe Subscriptions::CreateService do
let_it_be(:customer_email) { 'first.last@gitlab.com' } let_it_be(:customer_email) { 'first.last@gitlab.com' }
let_it_be(:client) { Gitlab::SubscriptionPortal::Client } let_it_be(:client) { Gitlab::SubscriptionPortal::Client }
let_it_be(:create_service_params) { Gitlab::Json.parse(fixture_file('create_service_params.json', dir: 'ee')).deep_symbolize_keys } let_it_be(:create_service_params) { Gitlab::Json.parse(fixture_file('create_service_params.json', dir: 'ee'))[0].deep_symbolize_keys }
describe '#execute' do describe '#execute' do
before do before do
...@@ -123,6 +123,27 @@ RSpec.describe Subscriptions::CreateService do ...@@ -123,6 +123,27 @@ RSpec.describe Subscriptions::CreateService do
execute execute
end end
context 'with add-on purchase' do
let_it_be(:subscription_params) do
{
is_addon: true,
active_subscription: 'A-000000',
plan_id: 'Add-on Plan ID',
payment_method_id: 'Payment method ID',
quantity: 111,
source: 'some_source'
}
end
it 'passes the correct parameters for creating a subscription' do
create_service_addon_params = Gitlab::Json.parse(fixture_file('create_service_params.json', dir: 'ee'))[1].deep_symbolize_keys
expect(client).to receive(:create_subscription).with(create_service_addon_params[:subscription], customer_email, 'token')
execute
end
end
it_behaves_like 'records an onboarding progress action', :subscription_created do it_behaves_like 'records an onboarding progress action', :subscription_created do
let(:namespace) { group } let(:namespace) { group }
end end
......
...@@ -21,8 +21,13 @@ RSpec.shared_examples_for 'subscription form data' do |js_selector| ...@@ -21,8 +21,13 @@ RSpec.shared_examples_for 'subscription form data' do |js_selector|
end end
RSpec.shared_examples_for 'buy minutes addon form data' do |js_selector| RSpec.shared_examples_for 'buy minutes addon form data' do |js_selector|
let_it_be(:group) { create(:group) }
let_it_be(:account_id) { '111111111111' }
let_it_be(:active_subscription) { { name: 'S-000000000' } }
before do before do
allow(view).to receive(:buy_addon_data).and_return( allow(view).to receive(:buy_addon_data).with(@group, @account_id, @active_subscription, 'pipelines-quota-tab', s_('Checkout|CI minutes')).and_return(
active_subscription: active_subscription,
group_data: '[{"id":"ci_minutes_plan_id","code":"ci_minutes","price_per_year":10.0}]', group_data: '[{"id":"ci_minutes_plan_id","code":"ci_minutes","price_per_year":10.0}]',
namespace_id: '1', namespace_id: '1',
plan_id: 'ci_minutes_plan_id', plan_id: 'ci_minutes_plan_id',
...@@ -33,6 +38,7 @@ RSpec.shared_examples_for 'buy minutes addon form data' do |js_selector| ...@@ -33,6 +38,7 @@ RSpec.shared_examples_for 'buy minutes addon form data' do |js_selector|
subject { render } subject { render }
it { is_expected.to have_selector("#{js_selector}[data-active-subscription-name='S-000000000']") }
it { is_expected.to have_selector("#{js_selector}[data-group-data='[{\"id\":\"ci_minutes_plan_id\",\"code\":\"ci_minutes\",\"price_per_year\":10.0}]']") } it { is_expected.to have_selector("#{js_selector}[data-group-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-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-namespace-id='1']") }
...@@ -41,8 +47,13 @@ RSpec.shared_examples_for 'buy minutes addon form data' do |js_selector| ...@@ -41,8 +47,13 @@ RSpec.shared_examples_for 'buy minutes addon form data' do |js_selector|
end end
RSpec.shared_examples_for 'buy storage addon form data' do |js_selector| RSpec.shared_examples_for 'buy storage addon form data' do |js_selector|
let_it_be(:group) { create(:group) }
let_it_be(:account_id) { '111111111111' }
let_it_be(:active_subscription) { { name: 'S-000000000' } }
before do before do
allow(view).to receive(:buy_addon_data).and_return( allow(view).to receive(:buy_addon_data).with(@group, @account_id, @active_subscription, 'storage-quota-tab', s_('Checkout|a storage subscription')).and_return(
active_subscription: active_subscription,
group_data: '[{"id":"storage_plan_id","code":"storage","price_per_year":10.0}]', group_data: '[{"id":"storage_plan_id","code":"storage","price_per_year":10.0}]',
namespace_id: '2', namespace_id: '2',
plan_id: 'storage_plan_id', plan_id: 'storage_plan_id',
...@@ -53,6 +64,7 @@ RSpec.shared_examples_for 'buy storage addon form data' do |js_selector| ...@@ -53,6 +64,7 @@ RSpec.shared_examples_for 'buy storage addon form data' do |js_selector|
subject { render } subject { render }
it { is_expected.to have_selector("#{js_selector}[data-active-subscription-name='S-000000000']") }
it { is_expected.to have_selector("#{js_selector}[data-group-data='[{\"id\":\"storage_plan_id\",\"code\":\"storage\",\"price_per_year\":10.0}]']") } it { is_expected.to have_selector("#{js_selector}[data-group-data='[{\"id\":\"storage_plan_id\",\"code\":\"storage\",\"price_per_year\":10.0}]']") }
it { is_expected.to have_selector("#{js_selector}[data-plan-id='storage_plan_id']") } it { is_expected.to have_selector("#{js_selector}[data-plan-id='storage_plan_id']") }
it { is_expected.to have_selector("#{js_selector}[data-namespace-id='2']") } it { is_expected.to have_selector("#{js_selector}[data-namespace-id='2']") }
......
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