feat: disable helpcenter on hacker plans (#12068)
This change blocks Help Center access for default/Hacker-plan accounts and closes the downgrade gap that could leave `help_center` enabled after a subscription falls back to the default cloud plan. Fixes: none Closes: none ## Why Default-plan accounts should not be able to access the Help Center, but the downgrade fallback path only reset the plan name and did not reconcile premium feature flags. That meant some accounts could keep `help_center` enabled even after landing back on the Hacker/default plan. ## What this change does - blocks Help Center portal and article access for default/Hacker-plan accounts - reconciles premium feature flags when a subscription falls back to the default cloud plan, so `help_center` is disabled immediately instead of waiting for a later webhook - preserves existing account `custom_attributes` during Stripe customer recreation instead of overwriting them - adds Enterprise coverage for the default-plan access checks on hosted and custom-domain Help Center routes - fixes the public access check to use the resolved portal object so blocked requests return the intended response instead of raising an error ## Validation 1. Create or use an account on the default/Hacker cloud plan with an active portal. 2. Visit the portal home page and a published article on both the Chatwoot-hosted URL and a configured custom domain. 3. Confirm the Help Center is blocked for that account. 4. Downgrade a paid account back to the default/Hacker plan through the Stripe webhook flow. 5. Confirm `help_center` is disabled right after the downgrade fallback is processed and the account can no longer access the Help Center. --------- Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com> Co-authored-by: Sojan Jose <sojan@pepalo.com>
This commit is contained in:
@@ -1,6 +1,7 @@
|
|||||||
class Public::Api::V1::Portals::ArticlesController < Public::Api::V1::Portals::BaseController
|
class Public::Api::V1::Portals::ArticlesController < Public::Api::V1::Portals::BaseController
|
||||||
before_action :ensure_custom_domain_request, only: [:show, :index]
|
before_action :ensure_custom_domain_request, only: [:show, :index]
|
||||||
before_action :portal
|
before_action :portal
|
||||||
|
before_action :ensure_portal_feature_enabled
|
||||||
before_action :set_category, except: [:index, :show, :tracking_pixel]
|
before_action :set_category, except: [:index, :show, :tracking_pixel]
|
||||||
before_action :set_article, only: [:show]
|
before_action :set_article, only: [:show]
|
||||||
layout 'portal'
|
layout 'portal'
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
class Public::Api::V1::Portals::CategoriesController < Public::Api::V1::Portals::BaseController
|
class Public::Api::V1::Portals::CategoriesController < Public::Api::V1::Portals::BaseController
|
||||||
before_action :ensure_custom_domain_request, only: [:show, :index]
|
before_action :ensure_custom_domain_request, only: [:show, :index]
|
||||||
before_action :portal
|
before_action :portal
|
||||||
|
before_action :ensure_portal_feature_enabled
|
||||||
before_action :set_category, only: [:show]
|
before_action :set_category, only: [:show]
|
||||||
layout 'portal'
|
layout 'portal'
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
class Public::Api::V1::PortalsController < Public::Api::V1::Portals::BaseController
|
class Public::Api::V1::PortalsController < Public::Api::V1::Portals::BaseController
|
||||||
before_action :ensure_custom_domain_request, only: [:show]
|
before_action :ensure_custom_domain_request, only: [:show]
|
||||||
before_action :portal
|
|
||||||
before_action :redirect_to_portal_with_locale, only: [:show]
|
before_action :redirect_to_portal_with_locale, only: [:show]
|
||||||
|
before_action :portal
|
||||||
|
before_action :ensure_portal_feature_enabled
|
||||||
layout 'portal'
|
layout 'portal'
|
||||||
|
|
||||||
def show
|
def show
|
||||||
@@ -24,6 +25,7 @@ class Public::Api::V1::PortalsController < Public::Api::V1::Portals::BaseControl
|
|||||||
def redirect_to_portal_with_locale
|
def redirect_to_portal_with_locale
|
||||||
return if params[:locale].present?
|
return if params[:locale].present?
|
||||||
|
|
||||||
|
portal
|
||||||
redirect_to "/hc/#{@portal.slug}/#{@portal.default_locale}"
|
redirect_to "/hc/#{@portal.slug}/#{@portal.default_locale}"
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -18,4 +18,11 @@ class PublicController < ActionController::Base
|
|||||||
Please send us an email at support@chatwoot.com with the custom domain name and account API key"
|
Please send us an email at support@chatwoot.com with the custom domain name and account API key"
|
||||||
}, status: :unauthorized and return
|
}, status: :unauthorized and return
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def ensure_portal_feature_enabled
|
||||||
|
return unless ChatwootApp.chatwoot_cloud?
|
||||||
|
return if @portal.account.feature_enabled?('help_center')
|
||||||
|
|
||||||
|
render 'public/api/v1/portals/not_active', status: :payment_required
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
12
app/views/public/api/v1/portals/not_active.html.erb
Normal file
12
app/views/public/api/v1/portals/not_active.html.erb
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
<div class="max-w-6xl h-full w-full flex-grow flex flex-col items-center justify-center mx-auto py-16 px-4 relative">
|
||||||
|
<div class="text-center mb-12">
|
||||||
|
<div class="mx-auto flex h-20 w-20 items-center justify-center rounded-full bg-slate-100 text-slate-500 dark:bg-slate-800 dark:text-slate-300">
|
||||||
|
<span class="text-5xl font-medium">i</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<h1 class="text-6xl text-center font-semibold text-slate-800 dark:text-slate-100 leading-relaxed"><%= I18n.t('public_portal.not_active.title') %></h1>
|
||||||
|
<p class="text-center text-slate-700 dark:text-slate-300 my-1"><%= I18n.t('public_portal.not_active.description') %></p>
|
||||||
|
<div class="text-center my-8">
|
||||||
|
<p class="text-slate-600 dark:text-slate-400 text-sm"><%= I18n.t('public_portal.not_active.action') %></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -423,6 +423,10 @@ en:
|
|||||||
title: Page not found
|
title: Page not found
|
||||||
description: We couldn't find the page you were looking for.
|
description: We couldn't find the page you were looking for.
|
||||||
back_to_home: Go to home page
|
back_to_home: Go to home page
|
||||||
|
not_active:
|
||||||
|
title: Help Center Unavailable
|
||||||
|
description: Please contact the site administrator for more information.
|
||||||
|
action: If you are the administrator, please upgrade your plan to restore access.
|
||||||
slack_unfurl:
|
slack_unfurl:
|
||||||
fields:
|
fields:
|
||||||
name: Name
|
name: Name
|
||||||
|
|||||||
@@ -7,21 +7,12 @@ class Enterprise::Billing::CreateStripeCustomerService
|
|||||||
return if existing_subscription?
|
return if existing_subscription?
|
||||||
|
|
||||||
customer_id = prepare_customer_id
|
customer_id = prepare_customer_id
|
||||||
subscription = Stripe::Subscription.create(
|
subscription = Stripe::Subscription.create(customer: customer_id, items: [{ price: price_id, quantity: default_quantity }])
|
||||||
{
|
custom_attributes = build_custom_attributes(customer_id, subscription)
|
||||||
customer: customer_id,
|
custom_attributes.except!('is_creating_customer')
|
||||||
items: [{ price: price_id, quantity: default_quantity }]
|
|
||||||
}
|
account.update!(custom_attributes: custom_attributes)
|
||||||
)
|
Enterprise::Billing::ReconcilePlanFeaturesService.new(account: account).perform
|
||||||
account.update!(
|
|
||||||
custom_attributes: {
|
|
||||||
stripe_customer_id: customer_id,
|
|
||||||
stripe_price_id: subscription['plan']['id'],
|
|
||||||
stripe_product_id: subscription['plan']['product'],
|
|
||||||
plan_name: default_plan['name'],
|
|
||||||
subscribed_quantity: subscription['quantity']
|
|
||||||
}
|
|
||||||
)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
@@ -66,4 +57,23 @@ class Enterprise::Billing::CreateStripeCustomerService
|
|||||||
)
|
)
|
||||||
subscriptions.data.present?
|
subscriptions.data.present?
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def build_custom_attributes(customer_id, subscription)
|
||||||
|
(account.custom_attributes || {}).merge(
|
||||||
|
'stripe_customer_id' => customer_id,
|
||||||
|
'stripe_price_id' => subscription['plan']['id'],
|
||||||
|
'stripe_product_id' => subscription['plan']['product'],
|
||||||
|
'plan_name' => default_plan['name'],
|
||||||
|
'subscribed_quantity' => subscription['quantity'],
|
||||||
|
'subscription_status' => subscription['status'],
|
||||||
|
'subscription_ends_on' => subscription_ends_on(subscription)
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
def subscription_ends_on(subscription)
|
||||||
|
period_end = subscription['current_period_end']
|
||||||
|
return if period_end.blank?
|
||||||
|
|
||||||
|
Time.zone.at(period_end)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -2,29 +2,9 @@ class Enterprise::Billing::HandleStripeEventService
|
|||||||
CLOUD_PLANS_CONFIG = 'CHATWOOT_CLOUD_PLANS'.freeze
|
CLOUD_PLANS_CONFIG = 'CHATWOOT_CLOUD_PLANS'.freeze
|
||||||
CAPTAIN_CLOUD_PLAN_LIMITS = 'CAPTAIN_CLOUD_PLAN_LIMITS'.freeze
|
CAPTAIN_CLOUD_PLAN_LIMITS = 'CAPTAIN_CLOUD_PLAN_LIMITS'.freeze
|
||||||
|
|
||||||
# Plan hierarchy: Hacker (default) -> Startups -> Business -> Enterprise
|
STARTUP_PLAN_FEATURES = Enterprise::Billing::ReconcilePlanFeaturesService::STARTUP_PLAN_FEATURES
|
||||||
# Each higher tier includes all features from the lower tiers
|
BUSINESS_PLAN_FEATURES = Enterprise::Billing::ReconcilePlanFeaturesService::BUSINESS_PLAN_FEATURES
|
||||||
|
ENTERPRISE_PLAN_FEATURES = Enterprise::Billing::ReconcilePlanFeaturesService::ENTERPRISE_PLAN_FEATURES
|
||||||
# Basic features available starting with the Startups plan
|
|
||||||
STARTUP_PLAN_FEATURES = %w[
|
|
||||||
inbound_emails
|
|
||||||
help_center
|
|
||||||
campaigns
|
|
||||||
team_management
|
|
||||||
channel_facebook
|
|
||||||
channel_email
|
|
||||||
channel_instagram
|
|
||||||
captain_integration
|
|
||||||
advanced_search_indexing
|
|
||||||
advanced_search
|
|
||||||
linear_integration
|
|
||||||
].freeze
|
|
||||||
|
|
||||||
# Additional features available starting with the Business plan
|
|
||||||
BUSINESS_PLAN_FEATURES = %w[sla custom_roles csat_review_notes conversation_required_attributes advanced_assignment].freeze
|
|
||||||
|
|
||||||
# Additional features available only in the Enterprise plan
|
|
||||||
ENTERPRISE_PLAN_FEATURES = %w[audit_logs disable_branding saml].freeze
|
|
||||||
|
|
||||||
def perform(event:)
|
def perform(event:)
|
||||||
@event = event
|
@event = event
|
||||||
@@ -49,7 +29,7 @@ class Enterprise::Billing::HandleStripeEventService
|
|||||||
|
|
||||||
previous_usage = capture_previous_usage
|
previous_usage = capture_previous_usage
|
||||||
update_account_attributes(subscription, plan)
|
update_account_attributes(subscription, plan)
|
||||||
update_plan_features
|
Enterprise::Billing::ReconcilePlanFeaturesService.new(account: account).perform
|
||||||
|
|
||||||
if billing_period_renewed?
|
if billing_period_renewed?
|
||||||
ActiveRecord::Base.transaction do
|
ActiveRecord::Base.transaction do
|
||||||
@@ -94,34 +74,6 @@ class Enterprise::Billing::HandleStripeEventService
|
|||||||
Enterprise::Billing::CreateStripeCustomerService.new(account: account).perform
|
Enterprise::Billing::CreateStripeCustomerService.new(account: account).perform
|
||||||
end
|
end
|
||||||
|
|
||||||
def update_plan_features
|
|
||||||
if default_plan?
|
|
||||||
disable_all_premium_features
|
|
||||||
else
|
|
||||||
enable_features_for_current_plan
|
|
||||||
end
|
|
||||||
|
|
||||||
# Enable any manually managed features configured in internal_attributes
|
|
||||||
enable_account_manually_managed_features
|
|
||||||
|
|
||||||
account.save!
|
|
||||||
end
|
|
||||||
|
|
||||||
def disable_all_premium_features
|
|
||||||
# Disable all features (for default Hacker plan)
|
|
||||||
account.disable_features(*STARTUP_PLAN_FEATURES)
|
|
||||||
account.disable_features(*BUSINESS_PLAN_FEATURES)
|
|
||||||
account.disable_features(*ENTERPRISE_PLAN_FEATURES)
|
|
||||||
end
|
|
||||||
|
|
||||||
def enable_features_for_current_plan
|
|
||||||
# First disable all premium features to handle downgrades
|
|
||||||
disable_all_premium_features
|
|
||||||
|
|
||||||
# Then enable features based on the current plan
|
|
||||||
enable_plan_specific_features
|
|
||||||
end
|
|
||||||
|
|
||||||
def handle_subscription_credits(plan, previous_usage)
|
def handle_subscription_credits(plan, previous_usage)
|
||||||
current_limits = account.limits || {}
|
current_limits = account.limits || {}
|
||||||
|
|
||||||
@@ -153,19 +105,6 @@ class Enterprise::Billing::HandleStripeEventService
|
|||||||
config[plan_name.downcase]&.symbolize_keys
|
config[plan_name.downcase]&.symbolize_keys
|
||||||
end
|
end
|
||||||
|
|
||||||
def enable_plan_specific_features
|
|
||||||
plan_name = account.custom_attributes['plan_name']
|
|
||||||
return if plan_name.blank?
|
|
||||||
|
|
||||||
case plan_name
|
|
||||||
when 'Startups' then account.enable_features(*STARTUP_PLAN_FEATURES)
|
|
||||||
when 'Business'
|
|
||||||
account.enable_features(*STARTUP_PLAN_FEATURES, *BUSINESS_PLAN_FEATURES)
|
|
||||||
when 'Enterprise'
|
|
||||||
account.enable_features(*STARTUP_PLAN_FEATURES, *BUSINESS_PLAN_FEATURES, *ENTERPRISE_PLAN_FEATURES)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def subscription
|
def subscription
|
||||||
@subscription ||= @event.data.object
|
@subscription ||= @event.data.object
|
||||||
end
|
end
|
||||||
@@ -197,19 +136,4 @@ class Enterprise::Billing::HandleStripeEventService
|
|||||||
cloud_plans = InstallationConfig.find_by(name: CLOUD_PLANS_CONFIG)&.value || []
|
cloud_plans = InstallationConfig.find_by(name: CLOUD_PLANS_CONFIG)&.value || []
|
||||||
cloud_plans.find { |config| config['product_id'].include?(plan_id) }
|
cloud_plans.find { |config| config['product_id'].include?(plan_id) }
|
||||||
end
|
end
|
||||||
|
|
||||||
def default_plan?
|
|
||||||
cloud_plans = InstallationConfig.find_by(name: CLOUD_PLANS_CONFIG)&.value || []
|
|
||||||
default_plan = cloud_plans.first || {}
|
|
||||||
account.custom_attributes['plan_name'] == default_plan['name']
|
|
||||||
end
|
|
||||||
|
|
||||||
def enable_account_manually_managed_features
|
|
||||||
# Get manually managed features from internal attributes using the service
|
|
||||||
service = Internal::Accounts::InternalAttributesService.new(account)
|
|
||||||
features = service.manually_managed_features
|
|
||||||
|
|
||||||
# Enable each feature
|
|
||||||
account.enable_features(*features) if features.present?
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -0,0 +1,61 @@
|
|||||||
|
class Enterprise::Billing::ReconcilePlanFeaturesService
|
||||||
|
CLOUD_PLANS_CONFIG = 'CHATWOOT_CLOUD_PLANS'.freeze
|
||||||
|
|
||||||
|
# Plan hierarchy: Hacker (default) -> Startups -> Business -> Enterprise
|
||||||
|
# Each higher tier includes all features from the lower tiers
|
||||||
|
STARTUP_PLAN_FEATURES = %w[
|
||||||
|
inbound_emails
|
||||||
|
help_center
|
||||||
|
campaigns
|
||||||
|
team_management
|
||||||
|
channel_facebook
|
||||||
|
channel_email
|
||||||
|
channel_instagram
|
||||||
|
captain_integration
|
||||||
|
advanced_search_indexing
|
||||||
|
advanced_search
|
||||||
|
linear_integration
|
||||||
|
].freeze
|
||||||
|
|
||||||
|
BUSINESS_PLAN_FEATURES = %w[sla custom_roles csat_review_notes conversation_required_attributes advanced_assignment].freeze
|
||||||
|
ENTERPRISE_PLAN_FEATURES = %w[audit_logs disable_branding saml].freeze
|
||||||
|
PREMIUM_PLAN_FEATURES = (STARTUP_PLAN_FEATURES + BUSINESS_PLAN_FEATURES + ENTERPRISE_PLAN_FEATURES).freeze
|
||||||
|
|
||||||
|
pattr_initialize [:account!]
|
||||||
|
|
||||||
|
def perform
|
||||||
|
account.disable_features(*PREMIUM_PLAN_FEATURES)
|
||||||
|
account.enable_features(*current_plan_features)
|
||||||
|
account.enable_features(*manually_managed_features)
|
||||||
|
account.save!
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def current_plan_features
|
||||||
|
return [] if default_plan?
|
||||||
|
|
||||||
|
case account.custom_attributes['plan_name']
|
||||||
|
when 'Startups' then STARTUP_PLAN_FEATURES
|
||||||
|
when 'Business' then STARTUP_PLAN_FEATURES + BUSINESS_PLAN_FEATURES
|
||||||
|
when 'Enterprise' then PREMIUM_PLAN_FEATURES
|
||||||
|
else []
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def default_plan?
|
||||||
|
default_plan_name = cloud_plans.first&.dig('name')
|
||||||
|
return false if default_plan_name.blank?
|
||||||
|
|
||||||
|
plan_name = account.custom_attributes['plan_name']
|
||||||
|
plan_name.blank? || plan_name == default_plan_name
|
||||||
|
end
|
||||||
|
|
||||||
|
def cloud_plans
|
||||||
|
@cloud_plans ||= InstallationConfig.find_by(name: CLOUD_PLANS_CONFIG)&.value || []
|
||||||
|
end
|
||||||
|
|
||||||
|
def manually_managed_features
|
||||||
|
@manually_managed_features ||= Internal::Accounts::InternalAttributesService.new(account).manually_managed_features
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -53,8 +53,8 @@ class Internal::Accounts::InternalAttributesService
|
|||||||
# Get list of valid features that can be manually managed
|
# Get list of valid features that can be manually managed
|
||||||
def valid_feature_list
|
def valid_feature_list
|
||||||
# Business and Enterprise plan features only
|
# Business and Enterprise plan features only
|
||||||
Enterprise::Billing::HandleStripeEventService::BUSINESS_PLAN_FEATURES +
|
Enterprise::Billing::ReconcilePlanFeaturesService::BUSINESS_PLAN_FEATURES +
|
||||||
Enterprise::Billing::HandleStripeEventService::ENTERPRISE_PLAN_FEATURES
|
Enterprise::Billing::ReconcilePlanFeaturesService::ENTERPRISE_PLAN_FEATURES
|
||||||
end
|
end
|
||||||
|
|
||||||
# Account notes functionality removed for now
|
# Account notes functionality removed for now
|
||||||
|
|||||||
@@ -13,6 +13,12 @@ RSpec.describe Public::Api::V1::PortalsController, type: :request do
|
|||||||
end
|
end
|
||||||
|
|
||||||
describe 'GET /public/api/v1/portals/{portal_slug}' do
|
describe 'GET /public/api/v1/portals/{portal_slug}' do
|
||||||
|
it 'redirects to the portal default locale when locale is not present' do
|
||||||
|
get "/hc/#{portal.slug}"
|
||||||
|
|
||||||
|
expect(response).to redirect_to("/hc/#{portal.slug}/#{portal.default_locale}")
|
||||||
|
end
|
||||||
|
|
||||||
it 'Show portal and categories belonging to the portal' do
|
it 'Show portal and categories belonging to the portal' do
|
||||||
get "/hc/#{portal.slug}/en"
|
get "/hc/#{portal.slug}/en"
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,46 @@
|
|||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
RSpec.describe 'Public Help Center Access', type: :request do
|
||||||
|
let(:plan_name) { 'Startups' }
|
||||||
|
let!(:account) { create(:account, custom_attributes: { 'plan_name' => plan_name }) }
|
||||||
|
let!(:agent) { create(:user, account: account, role: :agent) }
|
||||||
|
let!(:portal) { create(:portal, account: account, custom_domain: 'docs-helpcenter.example.com') }
|
||||||
|
let!(:category) { create(:category, portal: portal, account: account, locale: 'en', slug: 'category-slug') }
|
||||||
|
let!(:article) { create(:article, category: category, portal: portal, account: account, author: agent, status: :published) }
|
||||||
|
|
||||||
|
around do |example|
|
||||||
|
with_modified_env FRONTEND_URL: 'https://app.chatwoot.com', HELPCENTER_URL: 'https://help.chatwoot.com' do
|
||||||
|
previous_deployment_env = InstallationConfig.find_by(name: 'DEPLOYMENT_ENV')&.value
|
||||||
|
InstallationConfig.where(name: 'DEPLOYMENT_ENV').first_or_initialize.update!(value: 'cloud')
|
||||||
|
|
||||||
|
example.run
|
||||||
|
ensure
|
||||||
|
config = InstallationConfig.where(name: 'DEPLOYMENT_ENV').first_or_initialize
|
||||||
|
previous_deployment_env.present? ? config.update!(value: previous_deployment_env) : config.destroy!
|
||||||
|
host! 'www.example.com'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'blocks chatwoot-hosted portal pages when the help center feature is disabled' do
|
||||||
|
account.disable_features!(:help_center)
|
||||||
|
host! 'help.chatwoot.com'
|
||||||
|
|
||||||
|
get "/hc/#{portal.slug}/en"
|
||||||
|
|
||||||
|
expect(response).to have_http_status(:payment_required)
|
||||||
|
expect(response.body).to include('Help Center Unavailable')
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when the account is on the default plan' do
|
||||||
|
let(:plan_name) { 'Hacker' }
|
||||||
|
|
||||||
|
it 'still allows access if the feature flag is enabled' do
|
||||||
|
account.enable_features!(:help_center)
|
||||||
|
host! portal.custom_domain
|
||||||
|
|
||||||
|
get "/hc/#{portal.slug}/articles/#{article.slug}"
|
||||||
|
|
||||||
|
expect(response).to have_http_status(:ok)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -7,6 +7,16 @@ describe Enterprise::Billing::CreateStripeCustomerService do
|
|||||||
let!(:admin1) { create(:user, account: account, role: :administrator) }
|
let!(:admin1) { create(:user, account: account, role: :administrator) }
|
||||||
let(:admin2) { create(:user, account: account, role: :administrator) }
|
let(:admin2) { create(:user, account: account, role: :administrator) }
|
||||||
let(:subscriptions_list) { double }
|
let(:subscriptions_list) { double }
|
||||||
|
let(:current_period_end) { 1_686_567_520 }
|
||||||
|
let(:subscription_ends_on) { Time.zone.at(current_period_end).as_json }
|
||||||
|
let(:created_subscription) do
|
||||||
|
{
|
||||||
|
plan: { id: 'price_random_number', product: 'prod_random_number' },
|
||||||
|
quantity: 2,
|
||||||
|
status: 'active',
|
||||||
|
current_period_end: current_period_end
|
||||||
|
}.with_indifferent_access
|
||||||
|
end
|
||||||
|
|
||||||
describe '#perform' do
|
describe '#perform' do
|
||||||
before do
|
before do
|
||||||
@@ -18,18 +28,44 @@ describe Enterprise::Billing::CreateStripeCustomerService do
|
|||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
it 'preserves unrelated custom attributes, clears is_creating_customer, and reconciles default-plan features' do
|
||||||
|
account.update!(
|
||||||
|
custom_attributes: {
|
||||||
|
'is_creating_customer' => true,
|
||||||
|
'onboarding_source' => 'billing_page',
|
||||||
|
'subscription_status' => 'past_due',
|
||||||
|
'subscription_ends_on' => 1.day.ago
|
||||||
|
}
|
||||||
|
)
|
||||||
|
account.enable_features!(:help_center)
|
||||||
|
|
||||||
|
customer = double
|
||||||
|
allow(Stripe::Customer).to receive(:create).and_return(customer)
|
||||||
|
allow(customer).to receive(:id).and_return('cus_random_number')
|
||||||
|
allow(Stripe::Subscription).to receive(:create).and_return(created_subscription)
|
||||||
|
|
||||||
|
create_stripe_customer_service.new(account: account).perform
|
||||||
|
|
||||||
|
expect(account.reload.custom_attributes).to include(
|
||||||
|
'stripe_customer_id' => customer.id,
|
||||||
|
'stripe_price_id' => 'price_random_number',
|
||||||
|
'stripe_product_id' => 'prod_random_number',
|
||||||
|
'subscribed_quantity' => 2,
|
||||||
|
'plan_name' => 'A Plan Name',
|
||||||
|
'onboarding_source' => 'billing_page',
|
||||||
|
'subscription_status' => 'active',
|
||||||
|
'subscription_ends_on' => subscription_ends_on
|
||||||
|
)
|
||||||
|
expect(account.custom_attributes).not_to have_key('is_creating_customer')
|
||||||
|
expect(account).not_to be_feature_enabled('help_center')
|
||||||
|
end
|
||||||
|
|
||||||
it 'does not call stripe methods if customer id is present' do
|
it 'does not call stripe methods if customer id is present' do
|
||||||
account.update!(custom_attributes: { stripe_customer_id: 'cus_random_number' })
|
account.update!(custom_attributes: { stripe_customer_id: 'cus_random_number' })
|
||||||
allow(subscriptions_list).to receive(:data).and_return([])
|
allow(subscriptions_list).to receive(:data).and_return([])
|
||||||
allow(Stripe::Customer).to receive(:create)
|
allow(Stripe::Customer).to receive(:create)
|
||||||
allow(Stripe::Subscription).to receive(:list).and_return(subscriptions_list)
|
allow(Stripe::Subscription).to receive(:list).and_return(subscriptions_list)
|
||||||
allow(Stripe::Subscription).to receive(:create)
|
allow(Stripe::Subscription).to receive(:create).and_return(created_subscription)
|
||||||
.and_return(
|
|
||||||
{
|
|
||||||
plan: { id: 'price_random_number', product: 'prod_random_number' },
|
|
||||||
quantity: 2
|
|
||||||
}.with_indifferent_access
|
|
||||||
)
|
|
||||||
|
|
||||||
create_stripe_customer_service.new(account: account).perform
|
create_stripe_customer_service.new(account: account).perform
|
||||||
|
|
||||||
@@ -44,7 +80,9 @@ describe Enterprise::Billing::CreateStripeCustomerService do
|
|||||||
stripe_price_id: 'price_random_number',
|
stripe_price_id: 'price_random_number',
|
||||||
stripe_product_id: 'prod_random_number',
|
stripe_product_id: 'prod_random_number',
|
||||||
subscribed_quantity: 2,
|
subscribed_quantity: 2,
|
||||||
plan_name: 'A Plan Name'
|
plan_name: 'A Plan Name',
|
||||||
|
subscription_status: 'active',
|
||||||
|
subscription_ends_on: subscription_ends_on
|
||||||
}.with_indifferent_access
|
}.with_indifferent_access
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
@@ -53,14 +91,7 @@ describe Enterprise::Billing::CreateStripeCustomerService do
|
|||||||
customer = double
|
customer = double
|
||||||
allow(Stripe::Customer).to receive(:create).and_return(customer)
|
allow(Stripe::Customer).to receive(:create).and_return(customer)
|
||||||
allow(customer).to receive(:id).and_return('cus_random_number')
|
allow(customer).to receive(:id).and_return('cus_random_number')
|
||||||
allow(Stripe::Subscription)
|
allow(Stripe::Subscription).to receive(:create).and_return(created_subscription)
|
||||||
.to receive(:create)
|
|
||||||
.and_return(
|
|
||||||
{
|
|
||||||
plan: { id: 'price_random_number', product: 'prod_random_number' },
|
|
||||||
quantity: 2
|
|
||||||
}.with_indifferent_access
|
|
||||||
)
|
|
||||||
|
|
||||||
create_stripe_customer_service.new(account: account).perform
|
create_stripe_customer_service.new(account: account).perform
|
||||||
|
|
||||||
@@ -75,7 +106,9 @@ describe Enterprise::Billing::CreateStripeCustomerService do
|
|||||||
stripe_price_id: 'price_random_number',
|
stripe_price_id: 'price_random_number',
|
||||||
stripe_product_id: 'prod_random_number',
|
stripe_product_id: 'prod_random_number',
|
||||||
subscribed_quantity: 2,
|
subscribed_quantity: 2,
|
||||||
plan_name: 'A Plan Name'
|
plan_name: 'A Plan Name',
|
||||||
|
subscription_status: 'active',
|
||||||
|
subscription_ends_on: subscription_ends_on
|
||||||
}.with_indifferent_access
|
}.with_indifferent_access
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
@@ -96,12 +129,7 @@ describe Enterprise::Billing::CreateStripeCustomerService do
|
|||||||
customer = double
|
customer = double
|
||||||
allow(Stripe::Customer).to receive(:create).and_return(customer)
|
allow(Stripe::Customer).to receive(:create).and_return(customer)
|
||||||
allow(customer).to receive(:id).and_return('cus_random_number')
|
allow(customer).to receive(:id).and_return('cus_random_number')
|
||||||
allow(Stripe::Subscription).to receive(:create).and_return(
|
allow(Stripe::Subscription).to receive(:create).and_return(created_subscription)
|
||||||
{
|
|
||||||
plan: { id: 'price_random_number', product: 'prod_random_number' },
|
|
||||||
quantity: 2
|
|
||||||
}.with_indifferent_access
|
|
||||||
)
|
|
||||||
|
|
||||||
create_stripe_customer_service.new(account: account).perform
|
create_stripe_customer_service.new(account: account).perform
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user