Commit ca6cf402 authored by Oswaldo Ferreira's avatar Oswaldo Ferreira

Add initial Groups/Billing and Profile/Billing routing and template

parent c7837b50
...@@ -159,6 +159,10 @@ ...@@ -159,6 +159,10 @@
&.btn-remove { &.btn-remove {
@include btn-outline($white-light, $red-500, $red-500, $red-500, $white-light, $red-600, $red-600, $red-700); @include btn-outline($white-light, $red-500, $red-500, $red-500, $white-light, $red-600, $red-600, $red-700);
} }
&.btn-primary {
@include btn-outline($white-light, $blue-500, $blue-500, $blue-500, $white-light, $blue-600, $blue-600, $blue-700);
}
} }
&.btn-gray { &.btn-gray {
......
.billing-plan-header {
border-bottom: 0;
.billing-plan-logo svg {
height: 100px;
}
p {
margin: 0;
&:first-of-type {
margin-top: 16px;
}
&:last-of-type {
margin-bottom: 16px;
}
}
}
.billing-plans-alert {
margin: 0;
}
.billing-plans {
display: flex;
flex-direction: row;
flex-wrap: wrap;
justify-content: center;
margin-top: 8px;
.panel {
display: flex;
flex-direction: column;
margin: 8px;
width: 100%;
border: 0;
&:first-of-type {
margin-left: 0;
}
&:last-of-type {
margin-right: 0;
}
@media (min-width: $screen-sm-min) {
width: 290px;
}
.panel-heading,
.panel-body {
border-left: 1px solid $list-border;
border-right: 1px solid $list-border;
}
.panel-heading {
background-color: $blue-500;
color: $white-light;
border: 0;
border-radius: 4px 4px 0 0;
font-size: 20px;
text-align: center;
}
.panel-body {
flex-grow: 1;
border-radius: 0 0 4px 4px;
border-bottom: 1px solid $list-border;
padding: 0;
display: flex;
flex-direction: column;
.price-per-month {
display: flex;
flex-direction: row;
color: $blue-500;
padding: 16px;
padding-bottom: 0;
justify-content: center;
font-size: 50px;
font-weight: 700;
.billing-conditions {
list-style: none;
font-size: 20px;
font-weight: 600;
margin: auto 0;
line-height: 20px;
padding: 0;
}
}
.price-per-year {
color: $blue-500;
text-align: center;
font-size: 12px;
font-weight: 600;
padding-bottom: 16px;
height: 32px;
}
.feature-list {
display: flex;
flex-direction: column;
flex-grow: 1;
text-align: center;
margin: 0;
li {
background-color: $gray-light;
&:first-child {
border-top: 1px solid $list-border;
}
&:last-child {
border-bottom: 1px solid $list-border;
flex-grow: 1;
display: flex;
flex-direction: column;
justify-content: flex-end;
&:hover {
background-color: $gray-light;
}
}
}
}
.plan-action {
padding: 16px;
.btn {
width: 100%;
}
}
}
&.current {
.panel-heading {
background-color: $blue-600;
}
.panel-body {
border-color: $blue-600;
border-width: 2px;
.price-per-month,
.price-per-year {
color: $blue-600;
}
}
}
}
}
...@@ -104,6 +104,10 @@ class ApplicationController < ActionController::Base ...@@ -104,6 +104,10 @@ class ApplicationController < ActionController::Base
sessionless_sign_in(user) sessionless_sign_in(user)
end end
def verify_namespace_plan_check_enabled
render_404 unless current_application_settings.should_check_namespace_plan?
end
def log_exception(exception) def log_exception(exception)
Raven.capture_exception(exception) if sentry_enabled? Raven.capture_exception(exception) if sentry_enabled?
......
class Groups::BillingsController < Groups::ApplicationController
before_action :authorize_admin_group!
before_action :verify_namespace_plan_check_enabled
layout 'group_settings'
def index
@top_most_group = @group.root_ancestor if @group.has_parent?
current_plan = (@top_most_group || @group).actual_plan
@plans_data = FetchSubscriptionPlansService.new(plan: current_plan).execute
end
end
class Profiles::BillingsController < Profiles::ApplicationController
before_action :verify_namespace_plan_check_enabled
def index
@plans_data = FetchSubscriptionPlansService.new(plan: current_user.namespace.actual_plan).execute
end
end
module BillingPlansHelper
def subscription_plan_info(plans_data, current_plan_code)
plans_data.find { |plan| plan.code == current_plan_code }
end
def number_to_plan_currency(value)
number_to_currency(value, unit: '$', strip_insignificant_zeros: true, format: "%u%n")
end
def current_plan?(plan)
plan.purchase_link&.action == 'current_plan'
end
def has_plan_purchase_link?(plans_data)
plans_data.any? { |plan| plan.purchase_link&.href }
end
def plan_purchase_link(href, link_text)
if href
link_to link_text, href, class: 'btn btn-primary btn-inverted'
else
button_tag link_text, class: 'btn disabled'
end
end
end
class FetchSubscriptionPlansService
URL = 'https://customers.gitlab.com/gitlab_plans'.freeze
def initialize(plan:)
@plan = plan
end
def execute
cached { send_request }
end
private
def send_request
response = HTTParty.get(URL, query: { plan: @plan }, headers: { 'Accept' => 'application/json' })
JSON.parse(response.body).map { |plan| Hashie::Mash.new(plan) }
rescue => e
Rails.logger.info "Unable to connect to GitLab Customers App #{e}"
nil
end
def cached
if plans_data = cache.read(cache_key)
plans_data
else
cache.fetch(cache_key, force: true, expires_in: 1.day) { yield }
end
end
def cache
Rails.cache
end
def cache_key
"subscription-plans-#{@plan}"
end
end
...@@ -99,7 +99,7 @@ ...@@ -99,7 +99,7 @@
= f.check_box :user_default_external = f.check_box :user_default_external
Newly registered users will by default be external Newly registered users will by default be external
- if Gitlab.com? || Rails.env.development? - if current_application_settings.should_check_namespace_plan?
.form-group .form-group
= f.label :check_namespace_plan, 'Check feature availability on namespace plan', class: 'control-label col-sm-2' = f.label :check_namespace_plan, 'Check feature availability on namespace plan', class: 'control-label col-sm-2'
.col-sm-10 .col-sm-10
......
- page_title "Billing"
= render "groups/settings_head"
- if @top_most_group
- top_most_group_plan = subscription_plan_info(@plans_data, @top_most_group.actual_plan)
= render 'shared/billings/billing_plan_header', namespace: @group, plan: top_most_group_plan, parent_group: @top_most_group
- else
= render 'shared/billings/billing_plans', plans_data: @plans_data, namespace: @group
...@@ -20,6 +20,11 @@ ...@@ -20,6 +20,11 @@
= custom_icon('account') = custom_icon('account')
%span.nav-item-name %span.nav-item-name
Account Account
- if current_application_settings.should_check_namespace_plan?
= nav_link(controller: :billings) do
= link_to profile_billings_path, title: 'Billing' do
%span
Billing
- if current_application_settings.user_oauth_applications? - if current_application_settings.user_oauth_applications?
= nav_link(controller: 'oauth/applications') do = nav_link(controller: 'oauth/applications') do
= link_to applications_profile_path, title: 'Applications' do = link_to applications_profile_path, title: 'Applications' do
......
...@@ -12,6 +12,11 @@ ...@@ -12,6 +12,11 @@
= link_to profile_account_path, title: 'Account' do = link_to profile_account_path, title: 'Account' do
%span %span
Account Account
- if current_application_settings.should_check_namespace_plan?
= nav_link(controller: :billings) do
= link_to profile_billings_path, title: 'Billing' do
%span
Billing
- if current_application_settings.user_oauth_applications? - if current_application_settings.user_oauth_applications?
= nav_link(controller: 'oauth/applications') do = nav_link(controller: 'oauth/applications') do
= link_to applications_profile_path, title: 'Applications' do = link_to applications_profile_path, title: 'Applications' do
......
- page_title 'Billing'
= render 'profiles/head'
= render 'shared/billings/billing_plans', plans_data: @plans_data, namespace: current_user.namespace
.panel{ class: ('current' if current_plan?(plan)) }
.panel-heading
= plan.name
.panel-body
.price-per-month
.append-right-5
= number_to_plan_currency(plan.price_per_month)
%ul.billing-conditions
%li= s_("BillingPlans|per user")
%li= s_("BillingPlans|monthly")
.price-per-year
- if plan.price_per_year > 0
- price_per_year = number_to_plan_currency(plan.price_per_year)
= s_("BillingPlans|paid annually at %{price_per_year}") % { price_per_year: price_per_year }
%ul.feature-list.bordered-list
- plan.features.each do |feature|
%li
- if feature.highlight
%strong= feature.title
- else
= feature.title
%li
- if plan.about_page_href
= link_to s_("BillingPlans|See all %{plan_name} features") % { plan_name: plan.name }, plan.about_page_href
- purchase_link = plan.purchase_link
- if purchase_link
.plan-action
- href = purchase_link.href
- case purchase_link.action
- when 'downgrade'
= plan_purchase_link(href, s_("Billinglans|Downgrade"))
- when 'current_plan'
= plan_purchase_link(href, s_("BillingPlans|Current plan"))
- when 'upgrade'
= plan_purchase_link(href, s_("BillingPlans|Upgrade"))
- parent_group = local_assigns[:parent_group]
.billing-plan-header.content-block.center
.billing-plan-logo
- if Namespace::EE_PLANS.keys.include?(plan.code)
= render "shared/billings/plans/#{plan.code}.svg"
- elsif plan.free?
= render "shared/billings/plans/free.svg"
%h4
- plan_link = plan.about_page_href ? link_to(plan.name, plan.about_page_href) : plan.name
- if namespace == current_user.namespace
= s_("BillingPlans|You are currently on the %{plan_link} plan.").html_safe % { plan_link: plan_link }
- else
= s_("BillingPlans|%{group_name} is currently on the %{plan_link} plan.").html_safe % { group_name: namespace.full_name, plan_link: plan_link }
- if parent_group
%p= s_("BillingPlans|This group uses the plan associated with its parent group.")
- parent_billing_page_link = link_to parent_group.full_name, group_billings_path(parent_group)
%p= s_("BillingPlans|To manage the plan for this group, visit the billing section of %{parent_billing_page_link}.").html_safe % { parent_billing_page_link: parent_billing_page_link }
= link_to s_("BillingPlans|Manage plan"), group_billings_path(parent_group), class: 'btn btn-success'
- else
- faq_link = link_to s_("BillingPlans|frequently asked questions"), "https://about.gitlab.com/gitlab-com/#faq"
= s_("BillingPlans|Learn more about each plan by reading our %{faq_link}.").html_safe % { faq_link: faq_link }
- current_plan = subscription_plan_info(plans_data, namespace.actual_plan)
- if current_plan
= render 'shared/billings/billing_plan_header', namespace: namespace, plan: current_plan
- unless has_plan_purchase_link?(plans_data)
.billing-plans-alert.panel.panel-warning.prepend-top-10
.panel-heading
= s_("BillingPlans|Automatic downgrade and upgrade to some plans is currently not available.")
- customer_support_url = 'https://support.gitlab.com/hc/en-us/requests/new?ticket_form_id=334447';
- customer_support_link = link_to s_("BillingPlans|Customer Support"), customer_support_url
= s_("BillingPlans|Please contact %{customer_support_link} in that case.").html_safe % { customer_support_link: customer_support_link }
.billing-plans
- plans_data.each do |plan|
= render 'shared/billings/billing_plan', namespace: namespace, plan: plan
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 106 110" xmlns:xlink="http://www.w3.org/1999/xlink">
<defs>
<path id="a" d="M10,0 L14,0 L14,0 C19.5228475,-1.01453063e-15 24,4.4771525 24,10 L24,37 L24,37 C24,39.7614237 21.7614237,42 19,42 L5,42 L5,42 C2.23857625,42 1.2263553e-15,39.7614237 8.8817842e-16,37 L0,10 L0,10 C-6.76353751e-16,4.4771525 4.4771525,1.01453063e-15 10,0 Z"/>
<path id="b" d="M10,0 L14,0 L14,0 C19.5228475,-1.01453063e-15 24,4.4771525 24,10 L24,37 L24,37 C24,39.7614237 21.7614237,42 19,42 L5,42 L5,42 C2.23857625,42 1.2263553e-15,39.7614237 8.8817842e-16,37 L0,10 L0,10 C-6.76353751e-16,4.4771525 4.4771525,1.01453063e-15 10,0 Z"/>
<rect id="c" width="46" height="20" x="8" y="9" rx="10"/>
</defs>
<g fill="none" fill-rule="evenodd">
<rect width="106" height="106" x="1" y="4" fill="#F9F9F9" rx="12"/>
<rect width="106" height="106" fill="#FFFFFF" rx="10"/>
<path fill="#EEEEEE" fill-rule="nonzero" d="M10,4 C6.6862915,4 4,6.6862915 4,10 L4,96 C4,99.3137085 6.6862915,102 10,102 L96,102 C99.3137085,102 102,99.3137085 102,96 L102,10 C102,6.6862915 99.3137085,4 96,4 L10,4 Z M10,0 L96,0 C101.522847,-1.01453063e-15 106,4.4771525 106,10 L106,96 C106,101.522847 101.522847,106 96,106 L10,106 C4.4771525,106 6.76353751e-16,101.522847 0,96 L0,10 C-6.76353751e-16,4.4771525 4.4771525,1.01453063e-15 10,0 Z"/>
<g transform="translate(22 23)">
<path fill="#FFFFFF" stroke="#EEEEEE" stroke-width="4" d="M13,5 L49,5 L49,5 C54.5228475,5 59,9.4771525 59,15 L59,28 L59,28 C59,30.7614237 56.7614237,33 54,33 L8,33 L8,33 C5.23857625,33 3,30.7614237 3,28 L3,15 L3,15 C3,9.4771525 7.4771525,5 13,5 Z"/>
<g transform="translate(38)">
<path fill="#FFFFFF" stroke="#EFEDF8" stroke-width="4" d="M6,38 L18,38 L18,44 L18,44 C18,45.1045695 17.1045695,46 16,46 L8,46 L8,46 C6.8954305,46 6,45.1045695 6,44 L6,38 Z"/>
<use fill="#FFFFFF" xlink:href="#a"/>
<path stroke="#E1DBF1" stroke-width="4" d="M10,2 C5.581722,2 2,5.581722 2,10 L2,37 C2,38.6568542 3.34314575,40 5,40 L19,40 C20.6568542,40 22,38.6568542 22,37 L22,10 C22,5.581722 18.418278,2 14,2 L10,2 Z"/>
<g transform="translate(3 51)">
<rect width="10" height="3" x="4" fill="#FC6D26" rx="1.5"/>
<rect width="14" height="3" x="2" y="5" fill="#FDC4A8" rx="1.5"/>
<rect width="18" height="3" y="10" fill="#FEE1D3" rx="1.5"/>
<rect width="18" height="3" y="15" fill="#FEF0E8" rx="1.5"/>
</g>
</g>
<path fill="#FFFFFF" stroke="#EFEDF8" stroke-width="4" d="M6,38 L18,38 L18,44 L18,44 C18,45.1045695 17.1045695,46 16,46 L8,46 L8,46 C6.8954305,46 6,45.1045695 6,44 L6,38 Z"/>
<use fill="#FFFFFF" xlink:href="#b"/>
<path stroke="#E1DBF1" stroke-width="4" d="M10,2 C5.581722,2 2,5.581722 2,10 L2,37 C2,38.6568542 3.34314575,40 5,40 L19,40 C20.6568542,40 22,38.6568542 22,37 L22,10 C22,5.581722 18.418278,2 14,2 L10,2 Z"/>
<g transform="translate(3 51)">
<rect width="10" height="3" x="4" fill="#FC6D26" rx="1.5"/>
<rect width="14" height="3" x="2" y="5" fill="#FDC4A8" rx="1.5"/>
<rect width="18" height="3" y="10" fill="#FEE1D3" rx="1.5"/>
<rect width="18" height="3" y="15" fill="#FEF0E8" rx="1.5"/>
</g>
<g>
<use fill="#FFFFFF" xlink:href="#c"/>
<rect width="42" height="16" x="10" y="11" stroke="#6B4FBB" stroke-width="4" rx="8"/>
</g>
<circle cx="26" cy="19" r="2" fill="#6B4FBB"/>
<circle cx="36" cy="19" r="2" fill="#6B4FBB"/>
</g>
</g>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="106" height="110" viewBox="0 0 106 110" xmlns:xlink="http://www.w3.org/1999/xlink"><defs><circle id="a" cx="20" cy="40" r="5"/><circle id="b" cx="20" cy="20" r="20"/></defs><g fill="none" fill-rule="evenodd"><rect width="106" height="106" y="4" fill="#F9F9F9" rx="12"/><rect width="106" height="106" fill="#FFF" rx="10"/><path fill="#EEE" fill-rule="nonzero" d="M10 4a6 6 0 0 0-6 6v86a6 6 0 0 0 6 6h86a6 6 0 0 0 6-6V10a6 6 0 0 0-6-6H10zm0-4h86c5.523 0 10 4.477 10 10v86c0 5.523-4.477 10-10 10H10c-5.523 0-10-4.477-10-10V10C0 4.477 4.477 0 10 0z"/><path fill="#FDC4A8" d="M79.187 33.187l-.974 1.782a.3.3 0 0 1-.527 0l-.974-1.782-1.781-.974a.3.3 0 0 1 0-.526l1.781-.974.974-1.782a.3.3 0 0 1 .527 0l.974 1.782 1.781.974a.3.3 0 0 1 0 .526l-1.78.974z"/><path fill="#FEE1D3" d="M84.955 70.955l-1.328 2.428a.3.3 0 0 1-.526 0l-1.328-2.428-2.428-1.328a.3.3 0 0 1 0-.526l2.428-1.328 1.328-2.428a.3.3 0 0 1 .526 0l1.328 2.428 2.428 1.328a.3.3 0 0 1 0 .526l-2.428 1.328z"/><path fill="#FC6D26" d="M19.187 65.187l-.974 1.782a.3.3 0 0 1-.527 0l-.974-1.782-1.781-.974a.3.3 0 0 1 0-.526l1.781-.974.974-1.782a.3.3 0 0 1 .527 0l.974 1.782 1.781.974a.3.3 0 0 1 0 .526l-1.78.974z"/><ellipse cx="53" cy="90" fill="#F9F9F9" rx="22" ry="2"/><path fill="#6B4FBB" fill-rule="nonzero" d="M45.215 59.777c0 9.383 2.722 16.768 9.284 19.746 3.385 1.537 5.542 1.513 8.965.543.793-.224.868-.245 1.171-.321 2.864-.715 4.901-.054 8.558 3.637a1 1 0 1 0 1.42-1.407c-4.132-4.172-6.882-5.064-10.462-4.17-.333.083-.412.105-1.232.337-3.01.852-4.705.87-7.594-.44-5.647-2.564-8.11-9.245-8.11-17.925a1 1 0 1 0-2 0z"/><g transform="rotate(-5 227.182 -255.297)"><use fill="#FFF" xlink:href="#a"/><circle cx="20" cy="40" r="3" stroke="#C3B8E3" stroke-width="4"/><use fill="#FFF" xlink:href="#b"/><circle cx="20" cy="20" r="18" stroke="#C3B8E3" stroke-width="4"/></g><path fill="#6B4FBB" d="M44.535 40.745l-1.856.975a1 1 0 0 1-1.451-1.054l.354-2.066a1 1 0 0 0-.287-.885l-1.501-1.464a1 1 0 0 1 .554-1.705l2.074-.302a1 1 0 0 0 .753-.547l.928-1.88a1 1 0 0 1 1.794 0l.928 1.88a1 1 0 0 0 .753.547l2.074.302a1 1 0 0 1 .554 1.705l-1.5 1.464a1 1 0 0 0-.288.885l.354 2.066a1 1 0 0 1-1.451 1.054l-1.856-.975a1 1 0 0 0-.93 0z"/></g></svg>
\ No newline at end of file
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 106 110" xmlns:xlink="http://www.w3.org/1999/xlink"><defs><circle id="0" cx="20" cy="40" r="5"/><circle id="1" cx="20" cy="20" r="20"/></defs><g fill="none" fill-rule="evenodd"><rect width="106" height="106" y="4" fill="#f9f9f9" rx="12"/><rect width="106" height="106" fill="#fff" rx="10"/><path fill="#eee" fill-rule="nonzero" d="m10 4c-3.314 0-6 2.686-6 6v86c0 3.314 2.686 6 6 6h86c3.314 0 6-2.686 6-6v-86c0-3.314-2.686-6-6-6h-86m0-4h86c5.523 0 10 4.477 10 10v86c0 5.523-4.477 10-10 10h-86c-5.523 0-10-4.477-10-10v-86c0-5.523 4.477-10 10-10"/><path fill="#fdc4a8" d="m77.95 33.7l-1.948.571c-.159.047-.326-.045-.372-.204-.016-.055-.016-.114 0-.169l.571-1.948-.571-1.948c-.047-.159.045-.326.204-.372.055-.016.114-.016.169 0l1.948.571 1.948-.571c.159-.047.326.045.372.204.016.055.016.114 0 .169l-.571 1.948.571 1.948c.047.159-.045.326-.204.372-.055.016-.114.016-.169 0l-1.948-.571" transform="matrix(.70711-.70711.70711.70711.239 64.48)"/><path fill="#fee1d3" d="m83.36 71.61l-2.656.778c-.159.047-.326-.045-.372-.204-.016-.055-.016-.114 0-.169l.778-2.656-.778-2.656c-.047-.159.045-.326.204-.372.055-.016.114-.016.169 0l2.656.778 2.656-.778c.159-.047.326.045.372.204.016.055.016.114 0 .169l-.778 2.656.778 2.656c.047.159-.045.326-.204.372-.055.016-.114.016-.169 0l-2.656-.778" transform="matrix(.70711-.70711.70711.70711-24.631 79.26)"/><path fill="#fc6d26" d="m17.95 65.7l-1.948.571c-.159.047-.326-.045-.372-.204-.016-.055-.016-.114 0-.169l.571-1.948-.571-1.948c-.047-.159.045-.326.204-.372.055-.016.114-.016.169 0l1.948.571 1.948-.571c.159-.047.326.045.372.204.016.055.016.114 0 .169l-.571 1.948.571 1.948c.047.159-.045.326-.204.372-.055.016-.114.016-.169 0l-1.948-.571" transform="matrix(.70711-.70711.70711.70711-39.962 31.423)"/><ellipse cx="53" cy="90" fill="#f9f9f9" rx="22" ry="2"/><g transform="translate(23 15)"><path fill="#6b4fbb" fill-rule="nonzero" d="m22.21 44.777c0 9.383 2.722 16.767 9.284 19.746 3.385 1.537 5.542 1.513 8.966.543.792-.224.867-.245 1.171-.321 2.863-.715 4.901-.054 8.557 3.638.389.392 1.022.395 1.414.007.392-.389.395-1.022.007-1.414-4.133-4.172-6.882-5.064-10.463-4.17-.333.083-.412.105-1.232.337-3.01.853-4.705.871-7.594-.44-5.648-2.563-8.111-9.244-8.111-17.924 0-.552-.448-1-1-1-.552 0-1 .448-1 1"/><g transform="matrix(.99619-.08716.08716.99619.115 3.829)"><use fill="#fff" xlink:href="#0"/><circle cx="20" cy="40" r="3" stroke="#c3b8e3" stroke-width="4"/><g><use fill="#fff" xlink:href="#1"/><circle cx="20" cy="20" r="18" stroke="#c3b8e3" stroke-width="4"/></g></g></g></g></svg>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 106 110" xmlns:xlink="http://www.w3.org/1999/xlink"><defs><path id="1" d="m45 25.823c9.187 0 25-2.539 25-10.823 0-8.284-11.193-15-25-15-13.807 0-25 6.716-25 15 0 8.284 15.813 10.823 25 10.823" fill="#fff"/><ellipse id="0" cx="45" cy="28" rx="45" ry="17"/></defs><g fill="none" fill-rule="evenodd"><rect width="106" height="106" y="4" fill="#f9f9f9" rx="12"/><rect width="106" height="106" fill="#fff" rx="10"/><path fill="#eee" fill-rule="nonzero" d="m10 4c-3.314 0-6 2.686-6 6v86c0 3.314 2.686 6 6 6h86c3.314 0 6-2.686 6-6v-86c0-3.314-2.686-6-6-6h-86m0-4h86c5.523 0 10 4.477 10 10v86c0 5.523-4.477 10-10 10h-86c-5.523 0-10-4.477-10-10v-86c0-5.523 4.477-10 10-10"/><g transform="translate(8 11)"><g fill-rule="nonzero" transform="translate(16 47)"><path fill="#fef0e8" d="m4.455 1.761c4.504 2.62 13.518 4.239 23.545 4.239 9.962 0 18.929-1.599 23.459-4.19.959-.548 1.292-1.77.743-2.729-.548-.959-1.77-1.292-2.729-.743-3.802 2.174-12.146 3.662-21.474 3.662-9.383 0-17.767-1.506-21.533-3.697-.955-.555-2.179-.232-2.734.723-.555.955-.232 2.179.723 2.734"/><path fill="#fdc4a8" d="m2.888 11.835c4.995 2.569 14.991 4.165 26.11 4.165 11.04 0 20.978-1.574 26.01-4.111.986-.498 1.382-1.7.884-2.687-.498-.986-1.7-1.382-2.687-.884-4.352 2.197-13.734 3.682-24.2 3.682-10.54 0-19.974-1.506-24.282-3.722-.982-.505-2.188-.118-2.693.864-.505.982-.118 2.188.864 2.693"/><path fill="#fc6d26" d="m1.289 21.931c5.361 2.508 15.948 4.069 27.711 4.069 11.581 0 22.03-1.513 27.465-3.956 1.01-.453 1.457-1.637 1-2.644-.453-1.01-1.637-1.457-2.644-1-4.813 2.164-14.759 3.604-25.825 3.604-11.229 0-21.298-1.484-26.02-3.692-1-.468-2.191-.037-2.659.964-.468 1-.037 2.191.964 2.659"/><path fill="#fef0e8" d="m-.617 31.881c5.631 2.534 16.981 4.119 29.617 4.119 12.637 0 23.987-1.586 29.618-4.12 1.01-.453 1.456-1.637 1-2.645-.453-1.01-1.637-1.456-2.645-1-5 2.251-15.857 3.768-27.976 3.768-12.12 0-22.973-1.517-27.975-3.767-1.01-.453-2.191-.004-2.645 1-.453 1.01-.004 2.191 1 2.645"/></g><use fill="#fff" xlink:href="#0"/><ellipse cx="45" cy="28" stroke="#e1dbf1" stroke-width="4" rx="43" ry="15"/><use xlink:href="#1"/><path stroke="#c3b8e3" stroke-width="4" d="m45 23.823c12.11 0 23-3.567 23-8.823 0-6.908-10.154-13-23-13-12.846 0-23 6.092-23 13 0 5.256 10.894 8.823 23 8.823"/><path fill="#6b4fbb" d="m14.5 29c-1.381 0-2.5-1.119-2.5-2.5 0-1.381 1.119-2.5 2.5-2.5 1.381 0 2.5 1.119 2.5 2.5 0 1.381-1.119 2.5-2.5 2.5m11 5c-1.381 0-2.5-1.119-2.5-2.5 0-1.381 1.119-2.5 2.5-2.5 1.381 0 2.5 1.119 2.5 2.5 0 1.381-1.119 2.5-2.5 2.5m13 2c-1.381 0-2.5-1.119-2.5-2.5 0-1.381 1.119-2.5 2.5-2.5 1.381 0 2.5 1.119 2.5 2.5 0 1.381-1.119 2.5-2.5 2.5m13 0c-1.381 0-2.5-1.119-2.5-2.5 0-1.381 1.119-2.5 2.5-2.5 1.381 0 2.5 1.119 2.5 2.5 0 1.381-1.119 2.5-2.5 2.5m13-2c-1.381 0-2.5-1.119-2.5-2.5 0-1.381 1.119-2.5 2.5-2.5 1.381 0 2.5 1.119 2.5 2.5 0 1.381-1.119 2.5-2.5 2.5m11-5c-1.381 0-2.5-1.119-2.5-2.5 0-1.381 1.119-2.5 2.5-2.5 1.381 0 2.5 1.119 2.5 2.5 0 1.381-1.119 2.5-2.5 2.5"/></g></g></svg>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 106 110" xmlns:xlink="http://www.w3.org/1999/xlink"><g fill="none" fill-rule="evenodd"><rect width="106" height="106" y="4" fill="#f9f9f9" rx="12"/><rect width="106" height="106" fill="#fff" rx="10"/><path fill="#eee" fill-rule="nonzero" d="m10 4c-3.314 0-6 2.686-6 6v86c0 3.314 2.686 6 6 6h86c3.314 0 6-2.686 6-6v-86c0-3.314-2.686-6-6-6h-86m0-4h86c5.523 0 10 4.477 10 10v86c0 5.523-4.477 10-10 10h-86c-5.523 0-10-4.477-10-10v-86c0-5.523 4.477-10 10-10"/><path fill="#fdc4a8" d="m88.36 49.614l-2.656.778c-.159.047-.326-.045-.372-.204-.016-.055-.016-.114 0-.169l.778-2.656-.778-2.656c-.047-.159.045-.326.204-.372.055-.016.114-.016.169 0l2.656.778 2.656-.778c.159-.047.326.045.372.204.016.055.016.114 0 .169l-.778 2.656.778 2.656c.047.159-.045.326-.204.372-.055.016-.114.016-.169 0l-2.656-.778" transform="matrix(.70711-.70711.70711.70711-7.61 76.36)"/><g fill="#fc6d26"><path d="m19.95 39.7l-1.948.571c-.159.047-.326-.045-.372-.204-.016-.055-.016-.114 0-.169l.571-1.948-.571-1.948c-.047-.159.045-.326.204-.372.055-.016.114-.016.169 0l1.948.571 1.948-.571c.159-.047.326.045.372.204.016.055.016.114 0 .169l-.571 1.948.571 1.948c.047.159-.045.326-.204.372-.055.016-.114.016-.169 0l-1.948-.571" transform="matrix(.70711-.70711.70711.70711-20.991 25.22)"/><path d="m73.95 17.7l-1.948.571c-.159.047-.326-.045-.372-.204-.016-.055-.016-.114 0-.169l.571-1.948-.571-1.948c-.047-.159.045-.326.204-.372.055-.016.114-.016.169 0l1.948.571 1.948-.571c.159-.047.326.045.372.204.016.055.016.114 0 .169l-.571 1.948.571 1.948c.047.159-.045.326-.204.372-.055.016-.114.016-.169 0l-1.948-.571" transform="matrix(.70711-.70711.70711.70711 10.381 56.962)"/></g><path fill="#fef0e8" d="m27 83h10v17h-10z"/><path fill="#fee1d3" d="m27 83h5v17h-5z"/><path fill="#fef0e8" d="m48 83h10v17h-10z"/><path fill="#fee1d3" d="m48 83h5v17h-5z"/><path fill="#fef0e8" d="m69 83h10v17h-10z"/><path fill="#fee1d3" d="m69 83h5v17h-5z"/><path fill="#c3b8e3" fill-rule="nonzero" d="m29 80h6v-2h-6v2m-2-6h10c1.105 0 2 .895 2 2v6c0 1.105-.895 2-2 2h-10c-1.105 0-2-.895-2-2v-6c0-1.105.895-2 2-2m22 6h8v-2h-8v2m-2-6h12c1.105 0 2 .895 2 2v6c0 1.105-.895 2-2 2h-12c-1.105 0-2-.895-2-2v-6c0-1.105.895-2 2-2m24 6h6v-2h-6v2m-2-6h10c1.105 0 2 .895 2 2v6c0 1.105-.895 2-2 2h-10c-1.105 0-2-.895-2-2v-6c0-1.105.895-2 2-2"/><path fill="#fff" d="m42.802 43v36h-22.802c-1.657 0-3-1.343-3-3 0-.095.004-.19.013-.284.891-9.374 4.1-14.974 9.626-16.801 6.144-2.031 11.531-7.336 16.16-15.915"/><path fill="#e1dbf1" fill-rule="nonzero" d="m40.901 79h-20.901c-1.657 0-3-1.343-3-3 0-.095.004-.19.013-.284.891-9.374 4.1-14.974 9.626-16.801 4.421-1.462 8.451-4.619 12.09-9.471-.218 2.132-.375 4.351-.471 6.659-3.159 3.155-6.609 5.369-10.362 6.61-3.57 1.18-5.915 5.04-6.782 12.287h17.551c.098 1.313.21 2.647.336 4h1.901"/><rect width="6" height="3" x="28" y="69" fill="#efedf8" rx="1.5"/><rect width="6" height="3" x="72" y="69" fill="#c3b8e3" rx="1.5"/><g transform="matrix(-1 0 0 1 151.8 0)"><path fill="#fff" d="m88.8 43v36h-22.802c-1.657 0-3-1.343-3-3 0-.095.004-.19.013-.284.891-9.374 4.1-14.974 9.626-16.801 6.144-2.031 11.531-7.336 16.16-15.915"/><path fill="#e1dbf1" fill-rule="nonzero" d="m84.8 49.35c1.214-1.63 2.385-3.449 3.512-5.458l.488.123v6.628c-1.277 1.82-2.61 3.457-4 4.908v-6.201m-10.906 13.367c-3.57 1.18-5.915 5.04-6.782 12.287h17.689v-19.453c-3.306 3.452-6.936 5.854-10.906 7.166m14.906-19.713v36h-22.802c-1.657 0-3-1.343-3-3 0-.095.004-.19.013-.284.891-9.374 4.1-14.974 9.626-16.801 6.144-2.031 11.531-7.336 16.16-15.915"/></g><rect width="6" height="3" x="72" y="69" fill="#efedf8" rx="1.5"/><path fill="#fff" d="m40 41c1.927-9.373 5.828-17.497 11.703-24.371.718-.84 1.98-.938 2.82-.221.061.052.119.108.173.167 5.46 5.938 9.228 14.08 11.304 24.425 2.266 11.292 2.6 23.958 1 38h-28c-1.414-15.208-1.081-27.875 1-38" id="0"/><use xlink:href="#0"/><path fill="#e1dbf1" fill-rule="nonzero" d="m63.39 75c1.119-12.232.677-23.302-1.312-33.21-1.736-8.65-4.688-15.545-8.817-20.738-4.608 5.952-7.719 12.855-9.343 20.756-1.828 8.893-2.251 19.966-1.243 33.19h20.715m-23.39-34c1.927-9.373 5.828-17.497 11.703-24.371.718-.84 1.98-.938 2.82-.221.061.052.119.108.173.167 5.46 5.938 9.228 14.08 11.304 24.425 2.266 11.292 2.6 23.958 1 38h-28c-1.414-15.208-1.081-27.875 1-38"/><path fill="#c3b8e3" d="m51.5 64h3c.828 0 1.5.672 1.5 1.5 0 .828-.672 1.5-1.5 1.5h-3c-.828 0-1.5-.672-1.5-1.5 0-.828.672-1.5 1.5-1.5m0 5h3c.828 0 1.5.672 1.5 1.5 0 .828-.672 1.5-1.5 1.5h-3c-.828 0-1.5-.672-1.5-1.5 0-.828.672-1.5 1.5-1.5"/><path fill="#6b4fbb" fill-rule="nonzero" d="m58.987 37.466c-1.01-2.384-3.353-3.966-5.987-3.966-2.666 0-5.03 1.62-6.02 4.047-.313.767.056 1.643.823 1.955.767.313 1.643-.056 1.955-.823.533-1.307 1.807-2.18 3.243-2.18 1.419 0 2.681.852 3.225 2.136.323.763 1.204 1.119 1.966.796.763-.323 1.119-1.204.796-1.966"/></g></svg>
---
title: Add initial Groups/Billing and Profile/Billing routing and template
merge_request:
author:
...@@ -60,6 +60,7 @@ scope(path: 'groups/*group_id', ...@@ -60,6 +60,7 @@ scope(path: 'groups/*group_id',
end end
resources :variables, only: [:index, :show, :update, :create, :destroy] resources :variables, only: [:index, :show, :update, :create, :destroy]
resources :billings, only: [:index]
end end
end end
......
...@@ -55,6 +55,7 @@ resource :profile, only: [:show, :update] do ...@@ -55,6 +55,7 @@ resource :profile, only: [:show, :update] do
## EE-specific ## EE-specific
resources :pipeline_quota, only: [:index] resources :pipeline_quota, only: [:index]
resources :billings, only: [:index]
## EE-specific ## EE-specific
end end
end end
...@@ -46,7 +46,7 @@ module EE ...@@ -46,7 +46,7 @@ module EE
end end
def should_check_namespace_plan? def should_check_namespace_plan?
check_namespace_plan? && (::Gitlab.com? || Rails.env.development?) check_namespace_plan? && ::Gitlab.dev_env_or_com?
end end
private private
......
...@@ -6,6 +6,8 @@ module EE ...@@ -6,6 +6,8 @@ module EE
module Namespace module Namespace
extend ActiveSupport::Concern extend ActiveSupport::Concern
FREE_PLAN = 'free'.freeze
BRONZE_PLAN = 'bronze'.freeze BRONZE_PLAN = 'bronze'.freeze
SILVER_PLAN = 'silver'.freeze SILVER_PLAN = 'silver'.freeze
GOLD_PLAN = 'gold'.freeze GOLD_PLAN = 'gold'.freeze
...@@ -29,6 +31,10 @@ module EE ...@@ -29,6 +31,10 @@ module EE
validates :plan, inclusion: { in: EE_PLANS.keys }, allow_blank: true validates :plan, inclusion: { in: EE_PLANS.keys }, allow_blank: true
end end
def root_ancestor
ancestors.reorder(nil).find_by(parent_id: nil)
end
def move_dir def move_dir
raise NotImplementedError unless defined?(super) raise NotImplementedError unless defined?(super)
...@@ -68,6 +74,12 @@ module EE ...@@ -68,6 +74,12 @@ module EE
@features_available_in_plan[feature] @features_available_in_plan[feature]
end end
# The main difference between the "plan" column and this method is that "plan"
# returns nil / "" when it has no plan. Having no plan means it's a "free" plan.
def actual_plan
plan.presence || FREE_PLAN
end
def actual_shared_runners_minutes_limit def actual_shared_runners_minutes_limit
shared_runners_minutes_limit || shared_runners_minutes_limit ||
current_application_settings.shared_runners_minutes current_application_settings.shared_runners_minutes
......
...@@ -21,3 +21,9 @@ ...@@ -21,3 +21,9 @@
= link_to group_pipeline_quota_path(@group), title: 'Pipelines quota' do = link_to group_pipeline_quota_path(@group), title: 'Pipelines quota' do
%span %span
Pipelines quota Pipelines quota
- if current_application_settings.should_check_namespace_plan?
= nav_link(path: 'billings#index') do
= link_to group_billings_path(@group), title: 'Billing' do
%span
Billing
...@@ -12,4 +12,8 @@ module Gitlab ...@@ -12,4 +12,8 @@ module Gitlab
def self.gl_subdomain? def self.gl_subdomain?
SUBDOMAIN_REGEX === Gitlab.config.gitlab.url SUBDOMAIN_REGEX === Gitlab.config.gitlab.url
end end
def self.dev_env_or_com?
Rails.env.development? || com?
end
end end
require 'spec_helper'
describe Groups::BillingsController do
let(:user) { create(:user) }
let(:group) { create(:group, :private) }
describe 'GET index' do
before do
stub_application_setting(check_namespace_plan: true)
allow(Gitlab).to receive(:com?) { true }
end
context 'authorized' do
before do
group.add_owner(user)
sign_in(user)
end
it 'renders index with 200 status code' do
allow_any_instance_of(FetchSubscriptionPlansService).to receive(:execute)
get :index, group_id: group
expect(response).to have_http_status(200)
expect(response).to render_template(:index)
end
it 'fetches subscription plans data from customers.gitlab.com' do
data = double
expect_any_instance_of(FetchSubscriptionPlansService).to receive(:execute).and_return(data)
get :index, group_id: group
expect(assigns(:plans_data)).to eq(data)
end
end
context 'unauthorized' do
it 'renders 404 when user is not an owner' do
group.add_developer(user)
sign_in(user)
get :index, group_id: group.id
expect(response).to have_http_status(404)
end
it 'renders 404 when it is not gitlab.com' do
allow(Gitlab).to receive(:com?) { false }
group.add_owner(user)
sign_in(user)
get :index, group_id: group
expect(response).to have_http_status(404)
end
end
end
end
require 'spec_helper'
describe Profiles::BillingsController do
let(:user) { create(:user) }
describe 'GET #index' do
before do
stub_application_setting(check_namespace_plan: true)
allow(Gitlab).to receive(:com?) { true }
end
it 'renders index with 200 status code' do
allow_any_instance_of(FetchSubscriptionPlansService).to receive(:execute)
sign_in(user)
get :index
expect(response).to have_http_status(200)
expect(response).to render_template(:index)
end
it 'fetch subscription plans data from customers.gitlab.com' do
data = double
expect_any_instance_of(FetchSubscriptionPlansService).to receive(:execute).and_return(data)
sign_in(user)
get :index
expect(assigns(:plans_data)).to eq(data)
end
end
end
...@@ -258,4 +258,17 @@ describe Namespace do ...@@ -258,4 +258,17 @@ describe Namespace do
it { is_expected.to be_falsey } it { is_expected.to be_falsey }
end end
end end
describe '#root_ancestor' do
it 'returns the top most ancestor', :nested_groups do
root_group = create(:group)
nested_group = create(:group, parent: root_group)
deep_nested_group = create(:group, parent: nested_group)
very_deep_nested_group = create(:group, parent: deep_nested_group)
expect(nested_group.root_ancestor).to eq(root_group)
expect(deep_nested_group.root_ancestor).to eq(root_group)
expect(very_deep_nested_group.root_ancestor).to eq(root_group)
end
end
end end
require 'spec_helper'
describe 'Billing plan pages', :feature do
let(:user) { create(:user) }
let(:plans_data) do
[
{
name: "Free",
price_per_month: 0,
free: true,
code: "free",
price_per_year: 0,
purchase_link: {
action: "downgrade",
href: nil
},
features: []
},
{
name: "Bronze",
price_per_month: 4,
free: false,
code: "bronze",
price_per_year: 48,
purchase_link: {
action: "current_plan",
href: nil
},
features: []
},
{
name: "Silver",
price_per_month: 19,
free: false,
code: "silver",
price_per_year: 228,
purchase_link: {
action: "upgrade",
href: nil
},
features: []
},
{
name: "Gold",
price_per_month: 99,
free: false,
code: "gold",
price_per_year: 1188,
purchase_link: {
action: "upgrade",
href: nil
},
features: []
}
]
end
shared_examples 'displays all plans and correct actions' do
it 'displays all plans' do
page.within('.billing-plans') do
panels = page.all('.panel')
expect(panels.length).to eq(plans_data.length)
plans_data.each.with_index do |data, index|
expect(panels[index].find('.panel-heading')).to have_content(data[:name])
end
end
end
it 'displays correct plan actions' do
expected_actions = plans_data.map { |data| data.fetch(:purchase_link).fetch(:action) }
plan_actions = page.all('.billing-plans .panel .plan-action')
expect(plan_actions.length).to eq(expected_actions.length)
expected_actions.each_with_index do |expected_action, index|
action = plan_actions[index]
case expected_action
when 'downgrade'
expect(action).to have_content('Downgrade')
expect(action).to have_css('.disabled')
when 'current_plan'
expect(action).to have_content('Current plan')
expect(action).to have_css('.disabled')
when 'upgrade'
expect(action).to have_content('Upgrade')
expect(action).to have_css('.disabled')
end
end
end
end
before do
expect(HTTParty).to receive(:get).and_return(double(body: plans_data.to_json))
stub_application_setting(check_namespace_plan: true)
allow(Gitlab).to receive(:com?) { true }
gitlab_sign_in(user)
end
context 'users profile billing page' do
before do
allow_any_instance_of(EE::Namespace).to receive(:plan).and_return('bronze')
visit profile_billings_path
end
include_examples 'displays all plans and correct actions'
it 'displays plan header' do
page.within('.billing-plan-header') do
expect(page).to have_content("You are currently on the Bronze")
expect(page).to have_css('.billing-plan-logo svg')
end
end
end
context 'group billing page' do
let(:group) { create(:group) }
let!(:group_member) { create(:group_member, :owner, group: group, user: user) }
context 'top-most group' do
before do
expect_any_instance_of(EE::Group).to receive(:plan).at_least(:once).and_return('bronze')
visit group_billings_path(group)
end
include_examples 'displays all plans and correct actions'
it 'displays plan header' do
page.within('.billing-plan-header') do
expect(page).to have_content("#{group.name} is currently on the Bronze plan")
expect(page).to have_css('.billing-plan-logo svg')
end
end
end
end
context 'on sub-group', :nested_groups do
let(:group) { create(:group, plan: Namespace::BRONZE_PLAN) }
let!(:group_member) { create(:group_member, :owner, group: group, user: user) }
let(:subgroup1) { create(:group, parent: group, plan: Namespace::SILVER_PLAN) }
let!(:subgroup1_member) { create(:group_member, :owner, group: subgroup1, user: user) }
let(:subgroup2) { create(:group, parent: subgroup1) }
let!(:subgroup2_member) { create(:group_member, :owner, group: subgroup2, user: user) }
before do
visit group_billings_path(subgroup2)
end
it 'displays plan header only' do
page.within('.billing-plan-header') do
expect(page).to have_content("#{subgroup2.full_name} is currently on the Bronze plan")
expect(page).to have_css('.billing-plan-logo svg')
expect(page.find('.btn-success')).to have_content('Manage plan')
end
expect(page).not_to have_css('.billing-plans')
end
end
context 'with unexpected JSON' do
let(:plans_data) do
[
{
name: "Superhero",
price_per_month: 999.0,
free: true,
code: "not-found",
price_per_year: 111.0,
purchase_link: {
action: "upgrade",
href: "http://customers.test.host/subscriptions/new?plan_id=super_hero_id"
},
features: []
}
]
end
before do
expect_any_instance_of(EE::Namespace).to receive(:plan).at_least(:once).and_return(nil)
visit profile_billings_path
end
it 'renders no header for missing plan' do
expect(page).not_to have_css('.billing-plan-header')
end
it 'displays all plans' do
page.within('.billing-plans') do
panels = page.all('.panel')
expect(panels.length).to eq(plans_data.length)
plans_data.each_with_index do |data, index|
expect(panels[index].find('.panel-heading')).to have_content(data[:name])
end
end
end
end
end
require 'spec_helper'
describe BillingPlansHelper do
describe '#current_plan?' do
it 'returns true when current_plan' do
plan = Hashie::Mash.new(purchase_link: { action: 'current_plan' })
expect(helper.current_plan?(plan)).to be_truthy
end
it 'return false when not current_plan' do
plan = Hashie::Mash.new(purchase_link: { action: 'upgrade' })
expect(helper.current_plan?(plan)).to be_falsy
end
end
end
require 'spec_helper'
describe FetchSubscriptionPlansService do
describe '#execute' do
let(:endpoint_url) { 'https://customers.gitlab.com/gitlab_plans' }
subject { described_class.new(plan: 'bronze').execute }
context 'when successully fetching plans data' do
it 'returns parsed JSON' do
json_mock = double(body: [{ 'foo' => 'bar' }].to_json)
expect(HTTParty).to receive(:get)
.with(endpoint_url, query: { plan: 'bronze' }, headers: { 'Accept' => 'application/json' })
.and_return(json_mock)
is_expected.to eq([Hashie::Mash.new('foo' => 'bar')])
end
end
context 'when failing to fetch plans data' do
before do
expect(HTTParty).to receive(:get).and_raise(HTTParty::Error.new('Error message'))
end
it 'logs failure' do
expect(Rails).to receive_message_chain(:logger, :info).with('Unable to connect to GitLab Customers App Error message')
subject
end
it 'returns nil' do
is_expected.to be_nil
end
end
end
end
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