Skip to content
Snippets Groups Projects
Unverified Commit b7842592 authored by George Koltsov's avatar George Koltsov Committed by GitLab
Browse files

Merge branch '463741-add-new-internal-subscriptions-path' into 'master'

Create new gitlab_subscriptions internal API route

See merge request https://gitlab.com/gitlab-org/gitlab/-/merge_requests/169430



Merged-by: default avatarGeorge Koltsov <gkoltsov@gitlab.com>
Approved-by: default avatarJosianne Hyson <jhyson@gitlab.com>
Approved-by: default avatarGeorge Koltsov <gkoltsov@gitlab.com>
Reviewed-by: default avatarJosianne Hyson <jhyson@gitlab.com>
Co-authored-by: default avatarBishwa Hang Rai <bhrai@gitlab.com>
parents ef2f7f04 7412ec02
No related branches found
No related tags found
No related merge requests found
Loading
Loading
@@ -217,6 +217,108 @@ Example response:
}
```
 
#### Create a subscription
Use a POST command to create a subscription.
```plaintext
POST /internal/gitlab_subscriptions/namespaces/:id/gitlab_subscription
```
| Attribute | Type | Required | Description |
|:------------|:--------|:---------|:------------|
| `start_date` | date | yes | Start date of subscription |
| `end_date` | date | no | End date of subscription |
| `plan_code` | string | no | Subscription tier code |
| `seats` | integer | no | Number of seats in subscription |
| `max_seats_used` | integer | no | Highest number of billable users in the current subscription term |
| `auto_renew` | boolean | no | Whether subscription auto-renews on end date |
| `trial` | boolean | no | Whether subscription is a trial |
| `trial_starts_on` | date | no | Start date of trial |
| `trial_ends_on` | date | no | End date of trial |
Example request:
```shell
curl --request POST --header "X-CUSTOMERS-DOT-INTERNAL-TOKEN: <json-web-token>" "https://gitlab.com/api/v4/internal/gitlab_subscriptions/namespaces/1234/gitlab_subscription?start_date="2020-07-15"&plan="premium"&seats=10"
```
Example response:
```json
{
"plan": {
"code":"premium",
"name":"premium",
"trial":false,
"auto_renew":null,
"upgradable":false
},
"usage": {
"seats_in_subscription":10,
"seats_in_use":1,
"max_seats_used":0,
"seats_owed":0
},
"billing": {
"subscription_start_date":"2020-07-15",
"subscription_end_date":null,
"trial_ends_on":null
}
}
```
#### Update a subscription
Use a PUT command to update an existing subscription.
```plaintext
PUT /internal/gitlab_subscriptions/namespaces/:id/gitlab_subscription
```
| Attribute | Type | Required | Description |
|:------------|:--------|:---------|:------------|
| `start_date` | date | no | Start date of subscription |
| `end_date` | date | no | End date of subscription |
| `plan_code` | string | no | Subscription tier code |
| `seats` | integer | no | Number of seats in subscription |
| `max_seats_used` | integer | no | Highest number of billable users in the current subscription term |
| `auto_renew` | boolean | no | Whether subscription auto-renews on end date |
| `trial` | boolean | no | Whether subscription is a trial |
| `trial_starts_on` | date | no | Start date of trial. Required if trial is true. |
| `trial_ends_on` | date | no | End date of trial |
Example request:
```shell
curl --request PUT --header "X-CUSTOMERS-DOT-INTERNAL-TOKEN: <json-web-token>" "https://gitlab.com/api/v4/internal/gitlab_subscriptions/namespaces/1234/gitlab_subscription?max_seats_used=0"
```
Example response:
```json
{
"plan": {
"code":"premium",
"name":"premium",
"trial":false,
"auto_renew":null,
"upgradable":false
},
"usage": {
"seats_in_subscription":80,
"seats_in_use":82,
"max_seats_used":0,
"seats_owed":2
},
"billing": {
"subscription_start_date":"2020-07-15",
"subscription_end_date":"2021-07-15",
"trial_ends_on":null
}
}
```
### Upcoming Reconciliations
 
The `upcoming_reconciliations` endpoint is used by [CustomersDot](https://gitlab.com/gitlab-org/customers-gitlab-com) (`customers.gitlab.com`)
Loading
Loading
# frozen_string_literal: true
module GitlabSubscriptions
module API
module Entities
module Internal
class GitlabSubscription < Grape::Entity
expose :plan do
expose :plan_name, as: :code
expose :plan_title, as: :name
expose :trial
expose :auto_renew
expose :upgradable?, as: :upgradable
expose :exclude_guests?, as: :exclude_guests
end
expose :usage do
expose :seats, as: :seats_in_subscription
expose :seats_in_use
expose :max_seats_used
expose :seats_owed
end
expose :billing do
expose :start_date, as: :subscription_start_date
expose :end_date, as: :subscription_end_date
expose :trial_ends_on
end
end
end
end
end
end
Loading
Loading
@@ -17,11 +17,83 @@ class Subscriptions < ::API::Base
end
 
desc 'Returns the subscription for the namespace' do
success ::API::Entities::GitlabSubscription
success Entities::Internal::GitlabSubscription
end
get ":id/gitlab_subscription" do
present @namespace.gitlab_subscription || {}, with: ::API::Entities::GitlabSubscription
end
desc 'Create a subscription for the namespace' do
success Entities::Internal::GitlabSubscription
end
params do
requires :start_date, type: Date, desc: 'The date when subscription was started'
optional :end_date, type: Date, desc: 'End date of subscription'
optional :plan_code, type: String, desc: 'Subscription tier code'
optional :seats, type: Integer, desc: 'Number of seats in subscription'
optional :max_seats_used, type: Integer, desc: 'Highest number of active users in the last month'
optional :auto_renew, type: Grape::API::Boolean,
desc: 'Whether subscription will auto renew on end date'
optional :trial, type: Grape::API::Boolean, desc: 'Whether the subscription is a trial'
optional :trial_ends_on, type: Date, desc: 'End date of trial'
optional :trial_starts_on, type: Date, desc: 'Start date of trial'
optional :trial_extension_type, type: Integer, desc: 'Whether the trial was extended or reactivated'
end
post ":id/gitlab_subscription", urgency: :low, feature_category: :plan_provisioning do
subscription_params = declared_params(include_missing: false)
if subscription_params[:trial]
subscription_params[:trial_starts_on] ||= subscription_params[:start_date]
end
subscription = @namespace.create_gitlab_subscription(subscription_params)
if subscription.persisted?
present subscription, with: ::API::Entities::GitlabSubscription
else
render_validation_error!(subscription)
end
end
desc 'Update the subscription for the namespace' do
success Entities::Internal::GitlabSubscription
end
params do
optional :start_date, type: Date, desc: 'Start date of subscription'
optional :end_date, type: Date, desc: 'End date of subscription'
optional :plan_code, type: String, desc: 'Subscription tier code'
optional :seats, type: Integer, desc: 'Number of seats in subscription'
optional :max_seats_used, type: Integer, desc: 'Highest number of active users in the last month'
optional :auto_renew, type: Grape::API::Boolean,
desc: 'Whether subscription will auto renew on end date'
optional :trial, type: Grape::API::Boolean, desc: 'Whether the subscription is a trial'
optional :trial_ends_on, type: Date, desc: 'End date of trial'
optional :trial_starts_on, type: Date, desc: 'Start date of trial'
optional :trial_extension_type, type: Integer, desc: 'Whether the trial was extended or reactivated'
end
put ":id/gitlab_subscription" do
subscription = @namespace.gitlab_subscription
not_found!('GitlabSubscription') unless subscription
subscription_params = declared_params(include_missing: false)
if subscription_params[:trial]
subscription_params[:trial_starts_on] ||= subscription_params[:start_date]
end
subscription_params[:updated_at] = Time.current
if subscription.update(subscription_params)
present subscription, with: ::API::Entities::GitlabSubscription
else
render_validation_error!(subscription)
end
end
end
end
end
Loading
Loading
Loading
Loading
@@ -3,14 +3,14 @@
require 'spec_helper'
 
RSpec.describe GitlabSubscriptions::API::Internal::Subscriptions, :aggregate_failures, :api, feature_category: :plan_provisioning do
describe 'GET /internal/gitlab_subscriptions/namespaces/:id/gitlab_subscription', :saas do
include GitlabSubscriptions::InternalApiHelpers
include GitlabSubscriptions::InternalApiHelpers
 
let_it_be(:namespace) { create(:group) }
def subscription_path(namespace_id)
internal_api("namespaces/#{namespace_id}/gitlab_subscription")
end
 
def subscription_path(namespace_id)
internal_api("namespaces/#{namespace_id}/gitlab_subscription")
end
describe 'GET /internal/gitlab_subscriptions/namespaces/:id/gitlab_subscription', :saas do
let_it_be(:namespace) { create(:group) }
 
context 'when unauthenticated' do
it 'returns an error response' do
Loading
Loading
@@ -210,4 +210,437 @@ def subscription_path(namespace_id)
end
end
end
describe 'POST /internal/gitlab_subscriptions/namespaces/:id/gitlab_subscription', :saas do
let_it_be_with_reload(:namespace) { create(:namespace) }
let(:params) { { start_date: '2018-01-01', end_date: '2019-01-01', seats: 10, plan_code: 'ultimate' } }
context 'when unauthenticated' do
it 'returns authentication error' do
post subscription_path(namespace.id), params: params
expect(response).to have_gitlab_http_status(:unauthorized)
end
end
context 'when authenticated as the subscription portal' do
before do
stub_internal_api_authentication
end
context 'when the namespace does not exist' do
it 'returns a 404' do
post subscription_path(non_existing_record_id), headers: internal_api_headers, params: params
expect(response).to have_gitlab_http_status(:not_found)
expect(json_response).to eq('message' => '404 Namespace Not Found')
end
end
context 'when creating subscription for project namespace' do
it 'returns a 404' do
project_namespace = create(:project).project_namespace
post subscription_path(project_namespace.id), headers: internal_api_headers, params: params
expect(response).to have_gitlab_http_status(:not_found)
expect(json_response).to eq('message' => '404 Namespace Not Found')
end
end
context 'when the params are invalid' do
it 'responds with an error' do
post subscription_path(namespace.id), headers: internal_api_headers, params: params.merge(start_date: nil)
expect(response).to have_gitlab_http_status(:bad_request)
end
end
context 'when the params are valid' do
it 'creates a subscription for the namespace' do
post subscription_path(namespace.id), headers: internal_api_headers, params: params
expect(response).to have_gitlab_http_status(:created)
expect(namespace.gitlab_subscription).to be_present
end
end
context 'when creating a trial' do
it 'sets the trial_starts_on to the start_date' do
post subscription_path(namespace.id), headers: internal_api_headers, params: params.merge(trial: true)
expect(response).to have_gitlab_http_status(:created)
expect(namespace.reload.gitlab_subscription.trial_starts_on).to be_present
expect(namespace.gitlab_subscription.trial_starts_on.iso8601).to eq '2018-01-01'
end
end
end
# this method of authentication is deprecated and will be removed in
# https://gitlab.com/gitlab-org/gitlab/-/issues/473625
context 'when authenticating with a personal access token' do
let_it_be(:admin) { create(:admin) }
let(:params) { { start_date: '2018-01-01', end_date: '2019-01-01', seats: 10, plan_code: 'ultimate' } }
def subscription_path(namespace_id)
"/internal/gitlab_subscriptions/namespaces/#{namespace_id}/gitlab_subscription"
end
it_behaves_like 'POST request permissions for admin mode' do
let(:current_user) { admin }
let(:path) { "/namespaces/#{namespace.id}/gitlab_subscription" }
end
context 'when authenticated as a regular user' do
it 'returns an unauthorized error' do
user = create(:user)
post api(subscription_path(namespace.id), user), params: params
expect(response).to have_gitlab_http_status(:forbidden)
end
end
context 'when authenticated as an admin' do
it 'fails when the start_date is missing' do
post api(subscription_path(namespace.id), admin, admin_mode: true),
params: params.except(:start_date)
expect(response).to have_gitlab_http_status(:bad_request)
end
it 'fails when the record is invalid' do
post api(subscription_path(namespace.id), admin, admin_mode: true), params: params.merge(start_date: nil)
expect(response).to have_gitlab_http_status(:bad_request)
end
it 'creates a subscription for the namespace' do
post api(subscription_path(namespace.id), admin, admin_mode: true), params: params
expect(response).to have_gitlab_http_status(:created)
expect(namespace.gitlab_subscription).to be_present
end
it 'sets the trial_starts_on to the start_date' do
post api(subscription_path(namespace.id), admin, admin_mode: true), params: params.merge(trial: true)
expect(response).to have_gitlab_http_status(:created)
expect(namespace.reload.gitlab_subscription.trial_starts_on).to be_present
expect(namespace.gitlab_subscription.trial_starts_on.iso8601).to eq params[:start_date]
end
it 'can create a subscription using full_path' do
post api(subscription_path(namespace.full_path), admin, admin_mode: true), params: params
expect(response).to have_gitlab_http_status(:created)
expect(namespace.gitlab_subscription).to be_present
end
context 'when the namespace does not exist' do
it 'returns a 404' do
post api(subscription_path(non_existing_record_id), admin, admin_mode: true), params: params
expect(response).to have_gitlab_http_status(:not_found)
expect(json_response).to eq('message' => '404 Namespace Not Found')
end
end
context 'when creating subscription for project namespace' do
it 'returns a 404' do
project_namespace = create(:project).project_namespace
post api(subscription_path(project_namespace.id), admin, admin_mode: true), params: params
expect(response).to have_gitlab_http_status(:not_found)
expect(json_response).to eq('message' => '404 Namespace Not Found')
end
end
end
end
end
describe 'PUT /internal/gitlab_subscriptions/namespaces/:id/gitlab_subscription', :saas do
let_it_be_with_reload(:namespace) { create(:namespace) }
context 'when unauthenticated' do
it 'returns authentication error' do
put subscription_path(namespace.id)
expect(response).to have_gitlab_http_status(:unauthorized)
end
end
context 'when authenticated as the subscription portal' do
before do
stub_internal_api_authentication
end
context 'when namespace is not found' do
it 'returns a 404 error' do
put subscription_path(non_existing_record_id), headers: internal_api_headers, params: { seats: 150 }
expect(response).to have_gitlab_http_status(:not_found)
end
end
context 'when namespace does not have a subscription' do
it 'returns a 404 error' do
put subscription_path(namespace.id), headers: internal_api_headers, params: { seats: 150 }
expect(response).to have_gitlab_http_status(:not_found)
end
end
context 'when namespace is a project namespace' do
it 'returns a 404 error' do
project_namespace = create(:project).project_namespace
put subscription_path(project_namespace.id), headers: internal_api_headers, params: { seats: 150 }
expect(response).to have_gitlab_http_status(:not_found)
expect(json_response).to eq('message' => '404 Namespace Not Found')
end
end
context 'when the subscription exists' do
let_it_be(:premium_plan) { create(:premium_plan) }
let_it_be(:gitlab_subscription) do
create(:gitlab_subscription, namespace: namespace, start_date: '2018-01-01', end_date: '2019-01-01')
end
context 'when params are invalid' do
it 'returns a 400 error' do
put subscription_path(namespace.id), headers: internal_api_headers, params: { seats: nil }
expect(response).to have_gitlab_http_status(:bad_request)
end
end
context 'when the params are valid' do
it 'updates the subscription for the group' do
params = { seats: 150, plan_code: 'premium', start_date: '2018-01-01', end_date: '2019-01-01' }
put subscription_path(namespace.id), headers: internal_api_headers, params: params
expect(response).to have_gitlab_http_status(:ok)
expect(gitlab_subscription.reload.seats).to eq(150)
expect(gitlab_subscription.max_seats_used).to eq(0)
expect(gitlab_subscription.plan_name).to eq('premium')
expect(gitlab_subscription.plan_title).to eq('Premium')
end
it 'does not clear out existing data because of defaults' do
gitlab_subscription.update!(seats: 20, max_seats_used: 42)
params = { plan_code: 'premium', start_date: '2018-01-01', end_date: '2019-01-01' }
put subscription_path(namespace.id), headers: internal_api_headers, params: params
expect(response).to have_gitlab_http_status(:ok)
expect(gitlab_subscription.reload).to have_attributes(seats: 20, max_seats_used: 42)
end
it 'updates the timestamp when the attributes are the same' do
expect do
put subscription_path(namespace.id),
headers: internal_api_headers,
params: namespace.gitlab_subscription.attributes
end.to change { gitlab_subscription.reload.updated_at }
end
context 'when starting a new term' do
it 'resets the seat attributes for the subscription' do
gitlab_subscription.update!(seats: 20, max_seats_used: 42, seats_owed: 22)
expect(gitlab_subscription.seats_in_use).to eq 0
new_start = gitlab_subscription.end_date + 1.year
new_end = new_start + 1.year
params = { seats: 150, plan_code: 'premium', start_date: new_start, end_date: new_end }
put subscription_path(namespace.id), headers: internal_api_headers, params: params
expect(response).to have_gitlab_http_status(:ok)
expect(gitlab_subscription.reload).to have_attributes(max_seats_used: 1, seats_owed: 0)
end
end
context 'when updating as a trial' do
it 'sets the trial_starts_on to the start_date' do
params = {
start_date: '2018-01-01', trial: true
}
put subscription_path(namespace.id), headers: internal_api_headers, params: params
expect(response).to have_gitlab_http_status(:ok)
expect(namespace.reload.gitlab_subscription.trial_starts_on).to be_present
expect(namespace.gitlab_subscription.trial_starts_on.iso8601).to eq '2018-01-01'
end
end
context 'when updating the trial expiration date' do
it 'updates the trial expiration date' do
date = 30.days.from_now.to_date
params = { seats: 150, plan_code: 'ultimate', trial_ends_on: date.iso8601 }
put subscription_path(namespace.id), headers: internal_api_headers, params: params
expect(response).to have_gitlab_http_status(:ok)
expect(gitlab_subscription.reload.trial_ends_on).to eq(date)
end
end
end
end
end
# this method of authentication is deprecated and will be removed in
# https://gitlab.com/gitlab-org/gitlab/-/issues/473625
context 'when authenticating with a personal access token' do
def subscription_path(namespace_id)
"/internal/gitlab_subscriptions/namespaces/#{namespace_id}/gitlab_subscription"
end
let_it_be(:admin) { create(:admin) }
let_it_be(:premium_plan) { create(:premium_plan) }
let_it_be(:gitlab_subscription) do
create(:gitlab_subscription, namespace: namespace, start_date: '2018-01-01', end_date: '2019-01-01')
end
it_behaves_like 'PUT request permissions for admin mode' do
let(:path) { "/namespaces/#{namespace.id}/gitlab_subscription" }
let(:current_user) { admin }
let(:params) { { start_date: '2018-01-01', end_date: '2019-01-01', seats: 10, plan_code: 'ultimate' } }
end
context 'when authenticated as a regular user' do
it 'returns an unauthorized error' do
user = create(:user)
put api(subscription_path(namespace.id), user, admin_mode: false), params: { seats: 150 }
expect(response).to have_gitlab_http_status(:forbidden)
end
end
context 'when authenticated as an admin' do
context 'when namespace is not found' do
it 'returns a 404 error' do
put api(subscription_path(non_existing_record_id), admin, admin_mode: true), params: { seats: 150 }
expect(response).to have_gitlab_http_status(:not_found)
end
end
context 'when namespace does not have a subscription' do
let_it_be(:namespace_2) { create(:group) }
it 'returns a 404 error' do
put api(subscription_path(namespace_2.id), admin, admin_mode: true), params: { seats: 150 }
expect(response).to have_gitlab_http_status(:not_found)
end
end
context 'when namespace is a project namespace' do
it 'returns a 404 error' do
project_namespace = create(:project).project_namespace
put api(subscription_path(project_namespace.id), admin, admin_mode: true), params: { seats: 150 }
expect(response).to have_gitlab_http_status(:not_found)
expect(json_response).to eq('message' => '404 Namespace Not Found')
end
end
context 'when params are invalid' do
it 'returns a 400 error' do
put api(subscription_path(namespace.id), admin, admin_mode: true), params: { seats: nil }
expect(response).to have_gitlab_http_status(:bad_request)
end
end
context 'when params are valid' do
it 'updates the subscription for the group' do
params = { seats: 150, plan_code: 'premium', start_date: '2018-01-01', end_date: '2019-01-01' }
put api(subscription_path(namespace.id), admin, admin_mode: true), params: params
expect(response).to have_gitlab_http_status(:ok)
expect(gitlab_subscription.reload.seats).to eq(150)
expect(gitlab_subscription.max_seats_used).to eq(0)
expect(gitlab_subscription.plan_name).to eq('premium')
expect(gitlab_subscription.plan_title).to eq('Premium')
end
it 'is successful when using full_path routing' do
params = { seats: 150, plan_code: 'premium', start_date: '2018-01-01', end_date: '2019-01-01' }
put api(subscription_path(namespace.full_path), admin, admin_mode: true), params: params
expect(response).to have_gitlab_http_status(:ok)
end
it 'does not clear out existing data because of defaults' do
gitlab_subscription.update!(seats: 20, max_seats_used: 42)
params = { plan_code: 'premium', start_date: '2018-01-01', end_date: '2019-01-01' }
put api(subscription_path(namespace.id), admin, admin_mode: true), params: params
expect(response).to have_gitlab_http_status(:ok)
expect(gitlab_subscription.reload).to have_attributes(
seats: 20,
max_seats_used: 42
)
end
it 'updates the timestamp when the attributes are the same' do
expect do
put api(subscription_path(namespace.id), admin, admin_mode: true),
params: namespace.gitlab_subscription.attributes
end.to change { gitlab_subscription.reload.updated_at }
end
context 'when starting a new term' do
it 'resets the seat attributes for the subscription' do
gitlab_subscription.update!(seats: 20, max_seats_used: 42, seats_owed: 22)
new_start = gitlab_subscription.end_date + 1.year
new_end = new_start + 1.year
expect(gitlab_subscription.seats_in_use).to eq 0
params = { seats: 150, plan_code: 'premium', start_date: new_start, end_date: new_end }
put api(subscription_path(namespace.id), admin, admin_mode: true), params: params
expect(response).to have_gitlab_http_status(:ok)
expect(gitlab_subscription.reload).to have_attributes(max_seats_used: 1, seats_owed: 0)
end
end
context 'when updating the trial expiration date' do
it 'updates the trial expiration date' do
date = 30.days.from_now.to_date
params = { seats: 150, plan_code: 'ultimate', trial_ends_on: date.iso8601 }
put api(subscription_path(namespace.id), admin, admin_mode: true), params: params
expect(response).to have_gitlab_http_status(:ok)
expect(gitlab_subscription.reload.trial_ends_on).to eq(date)
end
end
end
end
end
end
end
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment