Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore: Introduce enterprise edition, license #3209

Merged
merged 12 commits into from
Dec 9, 2021
5 changes: 5 additions & 0 deletions app/controllers/api/v1/accounts/agents_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ class Api::V1::Accounts::AgentsController < Api::V1::Accounts::BaseController
before_action :fetch_agent, except: [:create, :index]
before_action :check_authorization
before_action :find_user, only: [:create]
before_action :validate_limit, only: [:create]
before_action :create_user, only: [:create]
before_action :save_account_user, only: [:create]

Expand Down Expand Up @@ -69,4 +70,8 @@ def new_agent_params
def agents
@agents ||= Current.account.users.order_by_full_name.includes({ avatar_attachment: [:blob] })
end

def validate_limit
render_payment_required('Account limit exceeded. Upgrade to a higher plan') if agents.count >= Current.account.usage_limits[:agents]
end
end
4 changes: 4 additions & 0 deletions app/controllers/concerns/request_exception_handler.rb
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,10 @@ def render_could_not_create_error(message)
render json: { error: message }, status: :unprocessable_entity
end

def render_payment_required(message)
render json: { error: message }, status: :payment_required
end

def render_internal_server_error(message)
render json: { error: message }, status: :internal_server_error
end
Expand Down
9 changes: 9 additions & 0 deletions app/models/account.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
# auto_resolve_duration :integer
# domain :string(100)
# feature_flags :integer default(0), not null
# limits :jsonb
# locale :integer default("en")
# name :string not null
# settings_flags :integer default(0), not null
Expand All @@ -19,6 +20,7 @@ class Account < ApplicationRecord
include FlagShihTzu
include Reportable
include Featurable
prepend_mod_with('Account')

DEFAULT_QUERY_SETTING = {
flag_query_mode: :bit_operator,
Expand Down Expand Up @@ -106,6 +108,13 @@ def support_email
super || GlobalConfig.get('MAILER_SUPPORT_EMAIL')['MAILER_SUPPORT_EMAIL'] || ENV.fetch('MAILER_SENDER_EMAIL', 'Chatwoot <accounts@chatwoot.com>')
end

def usage_limits
{
agents: ChatwootApp.max_limit,
inboxes: ChatwootApp.max_limit
}
end

private

def notify_creation
Expand Down
5 changes: 4 additions & 1 deletion config/application.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,11 @@ class Application < Rails::Application
# Initialize configuration defaults for originally generated Rails version.
config.load_defaults 6.0

config.autoload_paths << Rails.root.join('lib')
config.eager_load_paths << Rails.root.join('lib')
config.eager_load_paths << Rails.root.join('enterprise/lib')
# rubocop:disable Rails/FilePath
config.eager_load_paths += Dir["#{Rails.root}/enterprise/app/**"]
# rubocop:enable Rails/FilePath

# Settings in config/environments/* take precedence over those specified here.
# Application configuration can go into files in config/initializers
Expand Down
87 changes: 87 additions & 0 deletions config/initializers/01_inject_enterprise_edition_module.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
# frozen_string_literal: true

# original Authors: Gitlab
# https://gitlab.com/gitlab-org/gitlab/-/blob/master/config/initializers/0_inject_enterprise_edition_module.rb
#

### Ref: https://medium.com/@leo_hetsch/ruby-modules-include-vs-prepend-vs-extend-f09837a5b073
# Ancestors chain : it holds a list of constant names which are its ancestors
# example, by calling ancestors on the String class,
# String.ancestors => [String, Comparable, Object, PP::ObjectMixin, Kernel, BasicObject]
#
# Include: Ruby will insert the module into the ancestors chain of the class, just after its superclass
# ancestor chain : [OriginalClass, IncludedModule, ...]
#
# Extend: class will actually import the module methods as class methods
#
# Prepend: Ruby will look into the module methods before looking into the class.
# ancestor chain : [PrependedModule, OriginalClass, ...]
########

require 'active_support/inflector'

module InjectEnterpriseEditionModule
def prepend_mod_with(constant_name, namespace: Object, with_descendants: false)
each_extension_for(constant_name, namespace) do |constant|
prepend_module(constant, with_descendants)
end
end

def extend_mod_with(constant_name, namespace: Object)
# rubocop:disable Performance/MethodObjectAsBlock
each_extension_for(
constant_name,
namespace,
&method(:extend)
)
# rubocop:enable Performance/MethodObjectAsBlock
end

def include_mod_with(constant_name, namespace: Object)
# rubocop:disable Performance/MethodObjectAsBlock
each_extension_for(
constant_name,
namespace,
&method(:include)
)
# rubocop:enable Performance/MethodObjectAsBlock
end

def prepend_mod(with_descendants: false)
prepend_mod_with(name, with_descendants: with_descendants)
end

def extend_mod
extend_mod_with(name)
end

def include_mod
include_mod_with(name)
end

private

def prepend_module(mod, with_descendants)
prepend(mod)

descendants.each { |descendant| descendant.prepend(mod) } if with_descendants
end

def each_extension_for(constant_name, namespace)
ChatwootApp.extensions.each do |extension_name|
extension_namespace =
const_get_maybe_false(namespace, extension_name.camelize)

extension_module =
const_get_maybe_false(extension_namespace, constant_name)

yield(extension_module) if extension_module
end
end

def const_get_maybe_false(mod, name)
mod&.const_defined?(name, false) && mod&.const_get(name, false)
end
end

Module.prepend(InjectEnterpriseEditionModule)
5 changes: 5 additions & 0 deletions db/migrate/20211012135050_add_limits_to_accounts.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
class AddLimitsToAccounts < ActiveRecord::Migration[6.1]
def change
add_column :accounts, :limits, :jsonb, default: {}
end
end
1 change: 1 addition & 0 deletions db/schema.rb
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@
t.integer "settings_flags", default: 0, null: false
t.integer "feature_flags", default: 0, null: false
t.integer "auto_resolve_duration"
t.jsonb "limits", default: {}
end

create_table "action_mailbox_inbound_emails", force: :cascade do |t|
Expand Down
15 changes: 15 additions & 0 deletions enterprise/app/models/enterprise/account.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
module Enterprise::Account
def usage_limits
{
agents: get_limits(:agents),
inboxes: get_limits(:inboxes)
}
end

private

def get_limits(limit_name)
config_name = "ACCOUNT_#{limit_name.to_s.upcase}_LIMIT"
self[:limits][limit_name.to_s] || GlobalConfig.get(config_name)[config_name] || ChatwootApp.max_limit
end
end
2 changes: 2 additions & 0 deletions enterprise/lib/enterprise.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
module Enterprise
end
33 changes: 33 additions & 0 deletions lib/chatwoot_app.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# frozen_string_literal: true

require 'pathname'

module ChatwootApp
def self.root
Pathname.new(File.expand_path('..', __dir__))
end

def self.max_limit
100_000
end

def self.enterprise?
return if ENV.fetch('DISABLE_ENTERPRISE', false)

@enterprise ||= root.join('enterprise').exist?
end

def self.custom?
@custom ||= root.join('custom').exist?
end

def self.extensions
if custom?
%w[enterprise custom]
elsif enterprise?
%w[enterprise]
else
%w[]
end
end
end
6 changes: 6 additions & 0 deletions lib/custom_exceptions/account.rb
Original file line number Diff line number Diff line change
Expand Up @@ -28,4 +28,10 @@ def message
I18n.t 'errors.signup.failed'
end
end

class PlanUpgradeRequired < CustomExceptions::Base
def message
I18n.t 'errors.plan_upgrade_required.failed'
end
end
end
32 changes: 32 additions & 0 deletions spec/enterprise/models/account_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# frozen_string_literal: true

require 'rails_helper'

RSpec.describe Account do
describe 'usage_limits' do
before do
create(:installation_config, name: 'ACCOUNT_AGENTS_LIMIT', value: 20)
end

let!(:account) { create(:account) }

it 'returns max limits from global config when enterprise version' do
expect(account.usage_limits).to eq(
{
agents: 20,
inboxes: ChatwootApp.max_limit
}
)
end

it 'returns max limits from account when enterprise version' do
account.update(limits: { agents: 10 })
expect(account.usage_limits).to eq(
{
agents: 10,
inboxes: ChatwootApp.max_limit
}
)
end
end
end
8 changes: 8 additions & 0 deletions spec/models/account_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,12 @@
it { is_expected.to have_many(:kbase_portals).dependent(:destroy_async) }
it { is_expected.to have_many(:kbase_categories).dependent(:destroy_async) }
it { is_expected.to have_many(:teams).dependent(:destroy_async) }

describe 'usage_limits' do
let(:account) { create(:account) }

it 'returns ChatwootApp.max limits' do
expect(account.usage_limits).to eq({ agents: ChatwootApp.max_limit, inboxes: ChatwootApp.max_limit })
end
end
end