feat: Add APIs for limit check in accounts (#7242)
This commit is contained in:
21
app/helpers/billing_helper.rb
Normal file
21
app/helpers/billing_helper.rb
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
module BillingHelper
|
||||||
|
private
|
||||||
|
|
||||||
|
def default_plan?(account)
|
||||||
|
installation_config = InstallationConfig.find_by(name: 'CHATWOOT_CLOUD_PLANS')
|
||||||
|
default_plan = installation_config&.value&.first
|
||||||
|
|
||||||
|
# Return false if not plans are configured, so that no checks are enforced
|
||||||
|
return false if default_plan.blank?
|
||||||
|
|
||||||
|
account.custom_attributes['plan_name'].nil? || account.custom_attributes['plan_name'] == default_plan['name']
|
||||||
|
end
|
||||||
|
|
||||||
|
def conversations_this_month(account)
|
||||||
|
account.conversations.where('created_at > ?', 30.days.ago).count
|
||||||
|
end
|
||||||
|
|
||||||
|
def non_web_inboxes(account)
|
||||||
|
account.inboxes.where.not(channel_type: Channel::WebWidget.to_s).count
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -7,6 +7,10 @@ class AccountPolicy < ApplicationPolicy
|
|||||||
@account_user.administrator? || @account_user.agent?
|
@account_user.administrator? || @account_user.agent?
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def limits?
|
||||||
|
@account_user.administrator?
|
||||||
|
end
|
||||||
|
|
||||||
def update?
|
def update?
|
||||||
@account_user.administrator?
|
@account_user.administrator?
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ if resource.custom_attributes.present?
|
|||||||
json.custom_attributes do
|
json.custom_attributes do
|
||||||
json.plan_name resource.custom_attributes['plan_name']
|
json.plan_name resource.custom_attributes['plan_name']
|
||||||
json.subscribed_quantity resource.custom_attributes['subscribed_quantity']
|
json.subscribed_quantity resource.custom_attributes['subscribed_quantity']
|
||||||
|
json.subscription_status resource.custom_attributes['subscription_status']
|
||||||
|
json.subscription_ends_on resource.custom_attributes['subscription_ends_on']
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
json.domain @account.domain
|
json.domain @account.domain
|
||||||
|
|||||||
@@ -286,6 +286,7 @@ Rails.application.routes.draw do
|
|||||||
member do
|
member do
|
||||||
post :checkout
|
post :checkout
|
||||||
post :subscription
|
post :subscription
|
||||||
|
get :limits
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
class Enterprise::Api::V1::AccountsController < Api::BaseController
|
class Enterprise::Api::V1::AccountsController < Api::BaseController
|
||||||
|
include BillingHelper
|
||||||
before_action :fetch_account
|
before_action :fetch_account
|
||||||
before_action :check_authorization
|
before_action :check_authorization
|
||||||
|
before_action :check_cloud_env, only: [:limits]
|
||||||
|
|
||||||
def subscription
|
def subscription
|
||||||
if stripe_customer_id.blank? && @account.custom_attributes['is_creating_customer'].blank?
|
if stripe_customer_id.blank? && @account.custom_attributes['is_creating_customer'].blank?
|
||||||
@@ -10,12 +12,40 @@ class Enterprise::Api::V1::AccountsController < Api::BaseController
|
|||||||
head :no_content
|
head :no_content
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def limits
|
||||||
|
limits = {
|
||||||
|
'conversation' => {},
|
||||||
|
'non_web_inboxes' => {}
|
||||||
|
}
|
||||||
|
|
||||||
|
if default_plan?(@account)
|
||||||
|
limits = {
|
||||||
|
'conversation' => {
|
||||||
|
'allowed' => 500,
|
||||||
|
'consumed' => conversations_this_month(@account)
|
||||||
|
},
|
||||||
|
'non_web_inboxes' => {
|
||||||
|
'allowed' => 0,
|
||||||
|
'consumed' => non_web_inboxes(@account)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
# include id in response to ensure that the store can be updated on the frontend
|
||||||
|
render json: { id: @account.id, limits: limits }, status: :ok
|
||||||
|
end
|
||||||
|
|
||||||
def checkout
|
def checkout
|
||||||
return create_stripe_billing_session(stripe_customer_id) if stripe_customer_id.present?
|
return create_stripe_billing_session(stripe_customer_id) if stripe_customer_id.present?
|
||||||
|
|
||||||
render_invalid_billing_details
|
render_invalid_billing_details
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def check_cloud_env
|
||||||
|
installation_config = InstallationConfig.find_by(name: 'DEPLOYMENT_ENV')
|
||||||
|
render json: { error: 'Not found' }, status: :not_found unless installation_config&.value == 'cloud'
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def fetch_account
|
def fetch_account
|
||||||
|
|||||||
@@ -18,16 +18,25 @@ class Enterprise::Billing::HandleStripeEventService
|
|||||||
# skipping self hosted plan events
|
# skipping self hosted plan events
|
||||||
return if plan.blank? || account.blank?
|
return if plan.blank? || account.blank?
|
||||||
|
|
||||||
|
update_account_attributes(subscription, plan)
|
||||||
|
|
||||||
|
change_plan_features
|
||||||
|
end
|
||||||
|
|
||||||
|
def update_account_attributes(subscription, plan)
|
||||||
|
# https://stripe.com/docs/api/subscriptions/object
|
||||||
|
|
||||||
account.update(
|
account.update(
|
||||||
custom_attributes: {
|
custom_attributes: {
|
||||||
stripe_customer_id: subscription.customer,
|
stripe_customer_id: subscription.customer,
|
||||||
stripe_price_id: subscription['plan']['id'],
|
stripe_price_id: subscription['plan']['id'],
|
||||||
stripe_product_id: subscription['plan']['product'],
|
stripe_product_id: subscription['plan']['product'],
|
||||||
plan_name: plan['name'],
|
plan_name: plan['name'],
|
||||||
subscribed_quantity: subscription['quantity']
|
subscribed_quantity: subscription['quantity'],
|
||||||
|
subscription_status: subscription['status'],
|
||||||
|
subscription_ends_on: Time.zone.at(subscription['current_period_end'])
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
change_plan_features
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def process_subscription_deleted
|
def process_subscription_deleted
|
||||||
|
|||||||
@@ -110,4 +110,99 @@ RSpec.describe 'Enterprise Billing APIs', type: :request do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe 'GET /enterprise/api/v1/accounts/{account.id}/limits' do
|
||||||
|
context 'when it is an unauthenticated user' do
|
||||||
|
it 'returns unauthorized' do
|
||||||
|
get "/enterprise/api/v1/accounts/#{account.id}/limits", as: :json
|
||||||
|
|
||||||
|
expect(response).to have_http_status(:unauthorized)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when it is an authenticated user' do
|
||||||
|
context 'when it is an agent' do
|
||||||
|
it 'returns unauthorized' do
|
||||||
|
get "/enterprise/api/v1/accounts/#{account.id}/limits",
|
||||||
|
headers: agent.create_new_auth_token,
|
||||||
|
as: :json
|
||||||
|
|
||||||
|
expect(response).to have_http_status(:unauthorized)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when it is an admin' do
|
||||||
|
before do
|
||||||
|
create(:conversation, account: account)
|
||||||
|
create(:channel_api, account: account)
|
||||||
|
InstallationConfig.where(name: 'DEPLOYMENT_ENV').first_or_create(value: 'cloud')
|
||||||
|
InstallationConfig.where(name: 'CHATWOOT_CLOUD_PLANS').first_or_create(value: [{ 'name': 'Hacker' }])
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns the limits if the plan is default' do
|
||||||
|
account.update!(custom_attributes: { plan_name: 'Hacker' })
|
||||||
|
get "/enterprise/api/v1/accounts/#{account.id}/limits",
|
||||||
|
headers: admin.create_new_auth_token,
|
||||||
|
as: :json
|
||||||
|
|
||||||
|
expected_response = {
|
||||||
|
'id' => account.id,
|
||||||
|
'limits' => {
|
||||||
|
'conversation' => {
|
||||||
|
'allowed' => 500,
|
||||||
|
'consumed' => 1
|
||||||
|
},
|
||||||
|
'non_web_inboxes' => {
|
||||||
|
'allowed' => 0,
|
||||||
|
'consumed' => 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(response).to have_http_status(:ok)
|
||||||
|
expect(JSON.parse(response.body)).to eq(expected_response)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns nil if the plan is not default' do
|
||||||
|
account.update!(custom_attributes: { plan_name: 'Startups' })
|
||||||
|
get "/enterprise/api/v1/accounts/#{account.id}/limits",
|
||||||
|
headers: admin.create_new_auth_token,
|
||||||
|
as: :json
|
||||||
|
|
||||||
|
expected_response = {
|
||||||
|
'id' => account.id,
|
||||||
|
'limits' => {
|
||||||
|
'conversation' => {},
|
||||||
|
'non_web_inboxes' => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(response).to have_http_status(:ok)
|
||||||
|
expect(JSON.parse(response.body)).to eq(expected_response)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns limits if a plan is not configured' do
|
||||||
|
get "/enterprise/api/v1/accounts/#{account.id}/limits",
|
||||||
|
headers: admin.create_new_auth_token,
|
||||||
|
as: :json
|
||||||
|
|
||||||
|
expected_response = {
|
||||||
|
'id' => account.id,
|
||||||
|
'limits' => {
|
||||||
|
'conversation' => {
|
||||||
|
'allowed' => 500,
|
||||||
|
'consumed' => 1
|
||||||
|
},
|
||||||
|
'non_web_inboxes' => {
|
||||||
|
'allowed' => 0,
|
||||||
|
'consumed' => 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
expect(response).to have_http_status(:ok)
|
||||||
|
expect(JSON.parse(response.body)).to eq(expected_response)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -16,6 +16,8 @@ describe Enterprise::Billing::HandleStripeEventService do
|
|||||||
'id' => 'test', 'product' => 'plan_id', 'name' => 'plan_name'
|
'id' => 'test', 'product' => 'plan_id', 'name' => 'plan_name'
|
||||||
})
|
})
|
||||||
allow(subscription).to receive(:[]).with('quantity').and_return('10')
|
allow(subscription).to receive(:[]).with('quantity').and_return('10')
|
||||||
|
allow(subscription).to receive(:[]).with('status').and_return('active')
|
||||||
|
allow(subscription).to receive(:[]).with('current_period_end').and_return(1_686_567_520)
|
||||||
allow(subscription).to receive(:customer).and_return('cus_123')
|
allow(subscription).to receive(:customer).and_return('cus_123')
|
||||||
create(:installation_config, {
|
create(:installation_config, {
|
||||||
name: 'CHATWOOT_CLOUD_PLANS',
|
name: 'CHATWOOT_CLOUD_PLANS',
|
||||||
@@ -44,7 +46,9 @@ describe Enterprise::Billing::HandleStripeEventService do
|
|||||||
'stripe_price_id' => 'test',
|
'stripe_price_id' => 'test',
|
||||||
'stripe_product_id' => 'plan_id',
|
'stripe_product_id' => 'plan_id',
|
||||||
'plan_name' => 'Hacker',
|
'plan_name' => 'Hacker',
|
||||||
'subscribed_quantity' => '10'
|
'subscribed_quantity' => '10',
|
||||||
|
'subscription_ends_on' => '2023-06-12T10:58:40.000Z',
|
||||||
|
'subscription_status' => 'active'
|
||||||
})
|
})
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -57,7 +61,9 @@ describe Enterprise::Billing::HandleStripeEventService do
|
|||||||
'stripe_price_id' => 'test',
|
'stripe_price_id' => 'test',
|
||||||
'stripe_product_id' => 'plan_id',
|
'stripe_product_id' => 'plan_id',
|
||||||
'plan_name' => 'Hacker',
|
'plan_name' => 'Hacker',
|
||||||
'subscribed_quantity' => '10'
|
'subscribed_quantity' => '10',
|
||||||
|
'subscription_ends_on' => '2023-06-12T10:58:40.000Z',
|
||||||
|
'subscription_status' => 'active'
|
||||||
})
|
})
|
||||||
expect(account).not_to be_feature_enabled('channel_email')
|
expect(account).not_to be_feature_enabled('channel_email')
|
||||||
expect(account).not_to be_feature_enabled('help_center')
|
expect(account).not_to be_feature_enabled('help_center')
|
||||||
@@ -94,7 +100,9 @@ describe Enterprise::Billing::HandleStripeEventService do
|
|||||||
'stripe_price_id' => 'test',
|
'stripe_price_id' => 'test',
|
||||||
'stripe_product_id' => 'plan_id_2',
|
'stripe_product_id' => 'plan_id_2',
|
||||||
'plan_name' => 'Startups',
|
'plan_name' => 'Startups',
|
||||||
'subscribed_quantity' => '10'
|
'subscribed_quantity' => '10',
|
||||||
|
'subscription_ends_on' => '2023-06-12T10:58:40.000Z',
|
||||||
|
'subscription_status' => 'active'
|
||||||
})
|
})
|
||||||
expect(account).to be_feature_enabled('channel_email')
|
expect(account).to be_feature_enabled('channel_email')
|
||||||
expect(account).to be_feature_enabled('help_center')
|
expect(account).to be_feature_enabled('help_center')
|
||||||
|
|||||||
48
spec/helpers/billing_helper_spec.rb
Normal file
48
spec/helpers/billing_helper_spec.rb
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
RSpec.describe BillingHelper do
|
||||||
|
describe '#conversations_this_month' do
|
||||||
|
let(:user) { create(:user) }
|
||||||
|
let(:account) { create(:account, custom_attributes: { 'plan_name' => 'Hacker' }) }
|
||||||
|
|
||||||
|
before do
|
||||||
|
create(:installation_config, {
|
||||||
|
name: 'CHATWOOT_CLOUD_PLANS',
|
||||||
|
value: [
|
||||||
|
{
|
||||||
|
'name' => 'Hacker',
|
||||||
|
'product_id' => ['plan_id'],
|
||||||
|
'price_ids' => ['price_1']
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name' => 'Startups',
|
||||||
|
'product_id' => ['plan_id_2'],
|
||||||
|
'price_ids' => ['price_2']
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'counts only the conversations created this month' do
|
||||||
|
create_list(:conversation, 5, account: account, created_at: Time.zone.today - 1.day)
|
||||||
|
create_list(:conversation, 3, account: account, created_at: 2.months.ago)
|
||||||
|
expect(helper.send(:conversations_this_month, account)).to eq(5)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'counts only non web widget channels' do
|
||||||
|
create(:inbox, account: account, channel_type: Channel::WebWidget)
|
||||||
|
expect(account.inboxes.count).to eq(1)
|
||||||
|
expect(helper.send(:non_web_inboxes, account)).to eq(0)
|
||||||
|
|
||||||
|
create(:inbox, account: account, channel_type: Channel::Api)
|
||||||
|
expect(account.inboxes.count).to eq(2)
|
||||||
|
expect(helper.send(:non_web_inboxes, account)).to eq(1)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns true for the default plan name' do
|
||||||
|
expect(helper.send(:default_plan?, account)).to be(true)
|
||||||
|
account.custom_attributes['plan_name'] = 'Startups'
|
||||||
|
expect(helper.send(:default_plan?, account)).to be(false)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
Reference in New Issue
Block a user