mirror of
https://github.com/lingble/chatwoot.git
synced 2025-11-02 20:18:08 +00:00
feat: Allow SaaS users to manage subscription within the dashboard (#5059)
This commit is contained in:
@@ -72,6 +72,6 @@ class Api::V1::Accounts::AgentsController < Api::V1::Accounts::BaseController
|
|||||||
end
|
end
|
||||||
|
|
||||||
def validate_limit
|
def validate_limit
|
||||||
render_payment_required('Account limit exceeded. Upgrade to a higher plan') if agents.count >= Current.account.usage_limits[:agents]
|
render_payment_required('Account limit exceeded. Please purchase more licenses') if agents.count >= Current.account.usage_limits[:agents]
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -17,10 +17,6 @@ class ApplicationController < ActionController::Base
|
|||||||
Current.user = @user
|
Current.user = @user
|
||||||
end
|
end
|
||||||
|
|
||||||
def current_subscription
|
|
||||||
@subscription ||= Current.account.subscription
|
|
||||||
end
|
|
||||||
|
|
||||||
def pundit_user
|
def pundit_user
|
||||||
{
|
{
|
||||||
user: Current.user,
|
user: Current.user,
|
||||||
|
|||||||
@@ -13,8 +13,7 @@ class DashboardController < ActionController::Base
|
|||||||
|
|
||||||
def set_global_config
|
def set_global_config
|
||||||
@global_config = GlobalConfig.get(
|
@global_config = GlobalConfig.get(
|
||||||
'LOGO',
|
'LOGO', 'LOGO_THUMBNAIL',
|
||||||
'LOGO_THUMBNAIL',
|
|
||||||
'INSTALLATION_NAME',
|
'INSTALLATION_NAME',
|
||||||
'WIDGET_BRAND_URL',
|
'WIDGET_BRAND_URL',
|
||||||
'TERMS_URL',
|
'TERMS_URL',
|
||||||
@@ -29,7 +28,8 @@ class DashboardController < ActionController::Base
|
|||||||
'DIRECT_UPLOADS_ENABLED',
|
'DIRECT_UPLOADS_ENABLED',
|
||||||
'HCAPTCHA_SITE_KEY',
|
'HCAPTCHA_SITE_KEY',
|
||||||
'LOGOUT_REDIRECT_LINK',
|
'LOGOUT_REDIRECT_LINK',
|
||||||
'DISABLE_USER_PROFILE_UPDATE'
|
'DISABLE_USER_PROFILE_UPDATE',
|
||||||
|
'DEPLOYMENT_ENV'
|
||||||
).merge(app_config)
|
).merge(app_config)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
18
app/javascript/dashboard/api/enterprise/account.js
Normal file
18
app/javascript/dashboard/api/enterprise/account.js
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
/* global axios */
|
||||||
|
import ApiClient from '../ApiClient';
|
||||||
|
|
||||||
|
class EnterpriseAccountAPI extends ApiClient {
|
||||||
|
constructor() {
|
||||||
|
super('', { accountScoped: true, enterprise: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
checkout() {
|
||||||
|
return axios.post(`${this.url}checkout`);
|
||||||
|
}
|
||||||
|
|
||||||
|
subscription() {
|
||||||
|
return axios.post(`${this.url}subscription`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default new EnterpriseAccountAPI();
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
import accountAPI from '../account';
|
||||||
|
import ApiClient from '../../ApiClient';
|
||||||
|
import describeWithAPIMock from '../../specs/apiSpecHelper';
|
||||||
|
|
||||||
|
describe('#enterpriseAccountAPI', () => {
|
||||||
|
it('creates correct instance', () => {
|
||||||
|
expect(accountAPI).toBeInstanceOf(ApiClient);
|
||||||
|
expect(accountAPI).toHaveProperty('get');
|
||||||
|
expect(accountAPI).toHaveProperty('show');
|
||||||
|
expect(accountAPI).toHaveProperty('create');
|
||||||
|
expect(accountAPI).toHaveProperty('update');
|
||||||
|
expect(accountAPI).toHaveProperty('delete');
|
||||||
|
expect(accountAPI).toHaveProperty('checkout');
|
||||||
|
});
|
||||||
|
|
||||||
|
describeWithAPIMock('API calls', context => {
|
||||||
|
it('#checkout', () => {
|
||||||
|
accountAPI.checkout();
|
||||||
|
expect(context.axiosMock.post).toHaveBeenCalledWith(
|
||||||
|
'/enterprise/api/v1/checkout'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('#subscription', () => {
|
||||||
|
accountAPI.subscription();
|
||||||
|
expect(context.axiosMock.post).toHaveBeenCalledWith(
|
||||||
|
'/enterprise/api/v1/subscription'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -19,6 +19,7 @@
|
|||||||
:custom-views="customViews"
|
:custom-views="customViews"
|
||||||
:menu-config="activeSecondaryMenu"
|
:menu-config="activeSecondaryMenu"
|
||||||
:current-role="currentRole"
|
:current-role="currentRole"
|
||||||
|
:is-on-chatwoot-cloud="isOnChatwootCloud"
|
||||||
@add-label="showAddLabelPopup"
|
@add-label="showAddLabelPopup"
|
||||||
@toggle-accounts="toggleAccountModal"
|
@toggle-accounts="toggleAccountModal"
|
||||||
/>
|
/>
|
||||||
@@ -67,6 +68,7 @@ export default {
|
|||||||
...mapGetters({
|
...mapGetters({
|
||||||
currentUser: 'getCurrentUser',
|
currentUser: 'getCurrentUser',
|
||||||
globalConfig: 'globalConfig/get',
|
globalConfig: 'globalConfig/get',
|
||||||
|
isOnChatwootCloud: 'globalConfig/isOnChatwootCloud',
|
||||||
inboxes: 'inboxes/getInboxes',
|
inboxes: 'inboxes/getInboxes',
|
||||||
accountId: 'getCurrentAccountId',
|
accountId: 'getCurrentAccountId',
|
||||||
currentRole: 'getCurrentRole',
|
currentRole: 'getCurrentRole',
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ const settings = accountId => ({
|
|||||||
'settings_teams_edit',
|
'settings_teams_edit',
|
||||||
'settings_teams_edit_members',
|
'settings_teams_edit_members',
|
||||||
'settings_teams_edit_finish',
|
'settings_teams_edit_finish',
|
||||||
|
'billing_settings_index',
|
||||||
'automation_list',
|
'automation_list',
|
||||||
],
|
],
|
||||||
menuItems: [
|
menuItems: [
|
||||||
@@ -100,6 +101,14 @@ const settings = accountId => ({
|
|||||||
toState: frontendURL(`accounts/${accountId}/settings/applications`),
|
toState: frontendURL(`accounts/${accountId}/settings/applications`),
|
||||||
toStateName: 'settings_applications',
|
toStateName: 'settings_applications',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
icon: 'credit-card-person',
|
||||||
|
label: 'BILLING',
|
||||||
|
hasSubMenu: false,
|
||||||
|
toState: frontendURL(`accounts/${accountId}/settings/billing`),
|
||||||
|
toStateName: 'billing_settings_index',
|
||||||
|
showOnlyOnCloud: true,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
icon: 'settings',
|
icon: 'settings',
|
||||||
label: 'ACCOUNT_SETTINGS',
|
label: 'ACCOUNT_SETTINGS',
|
||||||
|
|||||||
@@ -55,6 +55,10 @@ export default {
|
|||||||
type: String,
|
type: String,
|
||||||
default: '',
|
default: '',
|
||||||
},
|
},
|
||||||
|
isOnChatwootCloud: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
hasSecondaryMenu() {
|
hasSecondaryMenu() {
|
||||||
@@ -67,12 +71,18 @@ export default {
|
|||||||
if (!this.currentRole) {
|
if (!this.currentRole) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
return this.menuConfig.menuItems.filter(
|
const menuItemsFilteredByRole = this.menuConfig.menuItems.filter(
|
||||||
menuItem =>
|
menuItem =>
|
||||||
window.roleWiseRoutes[this.currentRole].indexOf(
|
window.roleWiseRoutes[this.currentRole].indexOf(
|
||||||
menuItem.toStateName
|
menuItem.toStateName
|
||||||
) > -1
|
) > -1
|
||||||
);
|
);
|
||||||
|
return menuItemsFilteredByRole.filter(item => {
|
||||||
|
if (item.showOnlyOnCloud) {
|
||||||
|
return this.isOnChatwootCloud;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
});
|
||||||
},
|
},
|
||||||
inboxSection() {
|
inboxSection() {
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -167,6 +167,7 @@
|
|||||||
"CUSTOM_ATTRIBUTES": "Custom Attributes",
|
"CUSTOM_ATTRIBUTES": "Custom Attributes",
|
||||||
"AUTOMATION": "Automation",
|
"AUTOMATION": "Automation",
|
||||||
"TEAMS": "Teams",
|
"TEAMS": "Teams",
|
||||||
|
"BILLING": "Billing",
|
||||||
"CUSTOM_VIEWS_FOLDER": "Folders",
|
"CUSTOM_VIEWS_FOLDER": "Folders",
|
||||||
"CUSTOM_VIEWS_SEGMENTS": "Segments",
|
"CUSTOM_VIEWS_SEGMENTS": "Segments",
|
||||||
"ALL_CONTACTS": "All Contacts",
|
"ALL_CONTACTS": "All Contacts",
|
||||||
@@ -195,6 +196,25 @@
|
|||||||
"CATEGORY": "Category"
|
"CATEGORY": "Category"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"BILLING_SETTINGS": {
|
||||||
|
"TITLE": "Billing",
|
||||||
|
"CURRENT_PLAN" : {
|
||||||
|
"TITLE": "Current Plan",
|
||||||
|
"PLAN_NOTE": "You are currently subscribed to the **%{plan}** plan with **%{quantity}** licenses"
|
||||||
|
},
|
||||||
|
"MANAGE_SUBSCRIPTION": {
|
||||||
|
"TITLE": "Manage your subscription",
|
||||||
|
"DESCRIPTION": "View your previous invoices, edit your billing details, or cancel your subscription.",
|
||||||
|
"BUTTON_TXT": "Go to the billing portal"
|
||||||
|
},
|
||||||
|
|
||||||
|
"CHAT_WITH_US": {
|
||||||
|
"TITLE": "Need help?",
|
||||||
|
"DESCRIPTION": "Do you face any issues in billing? We are here to help.",
|
||||||
|
"BUTTON_TXT": "Chat with us"
|
||||||
|
},
|
||||||
|
"NO_BILLING_USER": "Your billing account is being configured. Please refresh the page and try again."
|
||||||
|
},
|
||||||
"CREATE_ACCOUNT": {
|
"CREATE_ACCOUNT": {
|
||||||
"NO_ACCOUNT_WARNING": "Uh oh! We could not find any Chatwoot accounts. Please create a new account to continue.",
|
"NO_ACCOUNT_WARNING": "Uh oh! We could not find any Chatwoot accounts. Please create a new account to continue.",
|
||||||
"NEW_ACCOUNT": "New Account",
|
"NEW_ACCOUNT": "New Account",
|
||||||
|
|||||||
@@ -0,0 +1,116 @@
|
|||||||
|
<template>
|
||||||
|
<div class="columns profile--settings">
|
||||||
|
<woot-loading-state v-if="uiFlags.isFetchingItem" />
|
||||||
|
<div v-else-if="!hasABillingPlan">
|
||||||
|
<p>{{ $t('BILLING_SETTINGS.NO_BILLING_USER') }}</p>
|
||||||
|
</div>
|
||||||
|
<div v-else class="small-12 columns ">
|
||||||
|
<div class="current-plan--details">
|
||||||
|
<h6>{{ $t('BILLING_SETTINGS.CURRENT_PLAN.TITLE') }}</h6>
|
||||||
|
<div
|
||||||
|
v-dompurify-html="
|
||||||
|
formatMessage(
|
||||||
|
$t('BILLING_SETTINGS.CURRENT_PLAN.PLAN_NOTE', {
|
||||||
|
plan: planName,
|
||||||
|
quantity: subscribedQuantity,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<billing-item
|
||||||
|
:title="$t('BILLING_SETTINGS.MANAGE_SUBSCRIPTION.TITLE')"
|
||||||
|
:description="$t('BILLING_SETTINGS.MANAGE_SUBSCRIPTION.DESCRIPTION')"
|
||||||
|
:button-label="$t('BILLING_SETTINGS.MANAGE_SUBSCRIPTION.BUTTON_TXT')"
|
||||||
|
@click="onClickBillingPortal"
|
||||||
|
/>
|
||||||
|
<billing-item
|
||||||
|
:title="$t('BILLING_SETTINGS.CHAT_WITH_US.TITLE')"
|
||||||
|
:description="$t('BILLING_SETTINGS.CHAT_WITH_US.DESCRIPTION')"
|
||||||
|
:button-label="$t('BILLING_SETTINGS.CHAT_WITH_US.BUTTON_TXT')"
|
||||||
|
button-icon="chat-multiple"
|
||||||
|
@click="onToggleChatWindow"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import messageFormatterMixin from 'shared/mixins/messageFormatterMixin';
|
||||||
|
|
||||||
|
import { mapGetters } from 'vuex';
|
||||||
|
import alertMixin from 'shared/mixins/alertMixin';
|
||||||
|
import accountMixin from '../../../../mixins/account';
|
||||||
|
import BillingItem from './components/BillingItem.vue';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: { BillingItem },
|
||||||
|
mixins: [accountMixin, alertMixin, messageFormatterMixin],
|
||||||
|
computed: {
|
||||||
|
...mapGetters({
|
||||||
|
getAccount: 'accounts/getAccount',
|
||||||
|
uiFlags: 'accounts/getUIFlags',
|
||||||
|
}),
|
||||||
|
currentAccount() {
|
||||||
|
return this.getAccount(this.accountId) || {};
|
||||||
|
},
|
||||||
|
customAttributes() {
|
||||||
|
return this.currentAccount.custom_attributes || {};
|
||||||
|
},
|
||||||
|
hasABillingPlan() {
|
||||||
|
return !!this.planName;
|
||||||
|
},
|
||||||
|
planName() {
|
||||||
|
return this.customAttributes.plan_name || '';
|
||||||
|
},
|
||||||
|
subscribedQuantity() {
|
||||||
|
return this.customAttributes.subscribed_quantity || 0;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.fetchAccountDetails();
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
async fetchAccountDetails() {
|
||||||
|
await this.$store.dispatch('accounts/get');
|
||||||
|
|
||||||
|
if (!this.hasABillingPlan) {
|
||||||
|
this.$store.dispatch('accounts/subscription');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onClickBillingPortal() {
|
||||||
|
this.$store.dispatch('accounts/checkout');
|
||||||
|
},
|
||||||
|
onToggleChatWindow() {
|
||||||
|
if (window.$chatwoot) {
|
||||||
|
window.$chatwoot.toggle();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.manage-subscription {
|
||||||
|
align-items: center;
|
||||||
|
background: var(--white);
|
||||||
|
border-radius: var(--border-radius-normal);
|
||||||
|
border: 1px solid var(--color-border);
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: var(--space-small);
|
||||||
|
padding: var(--space-medium) var(--space-normal);
|
||||||
|
}
|
||||||
|
|
||||||
|
.current-plan--details {
|
||||||
|
border-bottom: 1px solid var(--color-border);
|
||||||
|
margin-bottom: var(--space-normal);
|
||||||
|
padding-bottom: var(--space-normal);
|
||||||
|
}
|
||||||
|
|
||||||
|
.manage-subscription {
|
||||||
|
.manage-subscription--description {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
import SettingsContent from '../Wrapper';
|
||||||
|
import Index from './Index.vue';
|
||||||
|
import { frontendURL } from '../../../../helper/URLHelper';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
routes: [
|
||||||
|
{
|
||||||
|
path: frontendURL('accounts/:accountId/settings/billing'),
|
||||||
|
roles: ['administrator'],
|
||||||
|
component: SettingsContent,
|
||||||
|
props: {
|
||||||
|
headerTitle: 'BILLING_SETTINGS.TITLE',
|
||||||
|
icon: 'credit-card-person',
|
||||||
|
showNewButton: false,
|
||||||
|
},
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: '',
|
||||||
|
name: 'billing_settings_index',
|
||||||
|
component: Index,
|
||||||
|
roles: ['administrator'],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
<template>
|
||||||
|
<div class="manage-subscription">
|
||||||
|
<div>
|
||||||
|
<h6>{{ title }}</h6>
|
||||||
|
<p class="manage-subscription--description">
|
||||||
|
{{ description }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<woot-button variant="smooth" :icon="buttonIcon" @click="$emit('click')">
|
||||||
|
{{ buttonLabel }}
|
||||||
|
</woot-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
title: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
description: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
buttonLabel: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
|
||||||
|
buttonIcon: {
|
||||||
|
type: String,
|
||||||
|
default: 'edit',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
@@ -13,6 +13,7 @@ import teams from './teams/teams.routes';
|
|||||||
import attributes from './attributes/attributes.routes';
|
import attributes from './attributes/attributes.routes';
|
||||||
import automation from './automation/automation.routes';
|
import automation from './automation/automation.routes';
|
||||||
import store from '../../../store';
|
import store from '../../../store';
|
||||||
|
import billing from './billing/billing.routes';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
routes: [
|
routes: [
|
||||||
@@ -29,16 +30,17 @@ export default {
|
|||||||
},
|
},
|
||||||
...account.routes,
|
...account.routes,
|
||||||
...agent.routes,
|
...agent.routes,
|
||||||
|
...attributes.routes,
|
||||||
|
...automation.routes,
|
||||||
|
...billing.routes,
|
||||||
|
...campaigns.routes,
|
||||||
...canned.routes,
|
...canned.routes,
|
||||||
...inbox.routes,
|
...inbox.routes,
|
||||||
|
...integrationapps.routes,
|
||||||
...integrations.routes,
|
...integrations.routes,
|
||||||
...labels.routes,
|
...labels.routes,
|
||||||
...profile.routes,
|
...profile.routes,
|
||||||
...reports.routes,
|
...reports.routes,
|
||||||
...teams.routes,
|
...teams.routes,
|
||||||
...campaigns.routes,
|
|
||||||
...integrationapps.routes,
|
|
||||||
...attributes.routes,
|
|
||||||
...automation.routes,
|
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
/* eslint no-console: 0 */
|
|
||||||
/* eslint no-param-reassign: 0 */
|
|
||||||
/* eslint no-shadow: 0 */
|
|
||||||
import * as MutationHelpers from 'shared/helpers/vuex/mutationHelpers';
|
import * as MutationHelpers from 'shared/helpers/vuex/mutationHelpers';
|
||||||
import * as types from '../mutation-types';
|
import * as types from '../mutation-types';
|
||||||
import AccountAPI from '../../api/account';
|
import AccountAPI from '../../api/account';
|
||||||
|
import EnterpriseAccountAPI from '../../api/enterprise/account';
|
||||||
|
import { throwErrorMessage } from '../utils/api';
|
||||||
|
|
||||||
const state = {
|
const state = {
|
||||||
records: [],
|
records: [],
|
||||||
@@ -11,6 +10,7 @@ const state = {
|
|||||||
isFetching: false,
|
isFetching: false,
|
||||||
isFetchingItem: false,
|
isFetchingItem: false,
|
||||||
isUpdating: false,
|
isUpdating: false,
|
||||||
|
isCheckoutInProcess: false,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -60,6 +60,29 @@ export const actions = {
|
|||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
checkout: async ({ commit }) => {
|
||||||
|
commit(types.default.SET_ACCOUNT_UI_FLAG, { isCheckoutInProcess: true });
|
||||||
|
try {
|
||||||
|
const response = await EnterpriseAccountAPI.checkout();
|
||||||
|
window.location = response.data.redirect_url;
|
||||||
|
} catch (error) {
|
||||||
|
throwErrorMessage(error);
|
||||||
|
} finally {
|
||||||
|
commit(types.default.SET_ACCOUNT_UI_FLAG, { isCheckoutInProcess: false });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
subscription: async ({ commit }) => {
|
||||||
|
commit(types.default.SET_ACCOUNT_UI_FLAG, { isCheckoutInProcess: true });
|
||||||
|
try {
|
||||||
|
await EnterpriseAccountAPI.subscription();
|
||||||
|
} catch (error) {
|
||||||
|
throwErrorMessage(error);
|
||||||
|
} finally {
|
||||||
|
commit(types.default.SET_ACCOUNT_UI_FLAG, { isCheckoutInProcess: false });
|
||||||
|
}
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const mutations = {
|
export const mutations = {
|
||||||
|
|||||||
@@ -63,6 +63,7 @@
|
|||||||
"M10 13.5a.5.5 0 0 1 .5-.5h3a.5.5 0 0 1 0 1h-3a.5.5 0 0 1-.5-.5z",
|
"M10 13.5a.5.5 0 0 1 .5-.5h3a.5.5 0 0 1 0 1h-3a.5.5 0 0 1-.5-.5z",
|
||||||
"M4 6h1v6.5A2.5 2.5 0 0 0 7.5 15H14v1a2 2 0 0 1-2 2H5.5A3.5 3.5 0 0 1 2 14.5V8a2 2 0 0 1 2-2z"
|
"M4 6h1v6.5A2.5 2.5 0 0 0 7.5 15H14v1a2 2 0 0 1-2 2H5.5A3.5 3.5 0 0 1 2 14.5V8a2 2 0 0 1 2-2z"
|
||||||
],
|
],
|
||||||
|
"credit-card-person-outline": "M2 7.25A3.25 3.25 0 0 1 5.25 4h13.5A3.25 3.25 0 0 1 22 7.25V10h-.258A3.74 3.74 0 0 0 20.5 7.455V7.25a1.75 1.75 0 0 0-1.75-1.75H5.25A1.75 1.75 0 0 0 3.5 7.25v.25h11.95c-.44.409-.782.922-.987 1.5H3.5v5.75c0 .966.784 1.75 1.75 1.75h6.78c.06.522.217 1.028.458 1.5H5.25A3.25 3.25 0 0 1 2 14.75v-7.5Zm21 8.25a1.5 1.5 0 0 0-1.5-1.5h-7a1.5 1.5 0 0 0-1.5 1.5v.5c0 1.971 1.86 4 5 4 3.14 0 5-2.029 5-4v-.5Zm-2.25-5.25a2.75 2.75 0 1 0-5.5 0 2.75 2.75 0 0 0 5.5 0Z",
|
||||||
"delete-outline": "M12 1.75a3.25 3.25 0 0 1 3.245 3.066L15.25 5h5.25a.75.75 0 0 1 .102 1.493L20.5 6.5h-.796l-1.28 13.02a2.75 2.75 0 0 1-2.561 2.474l-.176.006H8.313a2.75 2.75 0 0 1-2.714-2.307l-.023-.174L4.295 6.5H3.5a.75.75 0 0 1-.743-.648L2.75 5.75a.75.75 0 0 1 .648-.743L3.5 5h5.25A3.25 3.25 0 0 1 12 1.75Zm6.197 4.75H5.802l1.267 12.872a1.25 1.25 0 0 0 1.117 1.122l.127.006h7.374c.6 0 1.109-.425 1.225-1.002l.02-.126L18.196 6.5ZM13.75 9.25a.75.75 0 0 1 .743.648L14.5 10v7a.75.75 0 0 1-1.493.102L13 17v-7a.75.75 0 0 1 .75-.75Zm-3.5 0a.75.75 0 0 1 .743.648L11 10v7a.75.75 0 0 1-1.493.102L9.5 17v-7a.75.75 0 0 1 .75-.75Zm1.75-6a1.75 1.75 0 0 0-1.744 1.606L10.25 5h3.5A1.75 1.75 0 0 0 12 3.25Z",
|
"delete-outline": "M12 1.75a3.25 3.25 0 0 1 3.245 3.066L15.25 5h5.25a.75.75 0 0 1 .102 1.493L20.5 6.5h-.796l-1.28 13.02a2.75 2.75 0 0 1-2.561 2.474l-.176.006H8.313a2.75 2.75 0 0 1-2.714-2.307l-.023-.174L4.295 6.5H3.5a.75.75 0 0 1-.743-.648L2.75 5.75a.75.75 0 0 1 .648-.743L3.5 5h5.25A3.25 3.25 0 0 1 12 1.75Zm6.197 4.75H5.802l1.267 12.872a1.25 1.25 0 0 0 1.117 1.122l.127.006h7.374c.6 0 1.109-.425 1.225-1.002l.02-.126L18.196 6.5ZM13.75 9.25a.75.75 0 0 1 .743.648L14.5 10v7a.75.75 0 0 1-1.493.102L13 17v-7a.75.75 0 0 1 .75-.75Zm-3.5 0a.75.75 0 0 1 .743.648L11 10v7a.75.75 0 0 1-1.493.102L9.5 17v-7a.75.75 0 0 1 .75-.75Zm1.75-6a1.75 1.75 0 0 0-1.744 1.606L10.25 5h3.5A1.75 1.75 0 0 0 12 3.25Z",
|
||||||
"dismiss-circle-outline": "M12 2c5.523 0 10 4.477 10 10s-4.477 10-10 10S2 17.523 2 12 6.477 2 12 2Zm0 1.5a8.5 8.5 0 1 0 0 17 8.5 8.5 0 0 0 0-17Zm3.446 4.897.084.073a.75.75 0 0 1 .073.976l-.073.084L13.061 12l2.47 2.47a.75.75 0 0 1 .072.976l-.073.084a.75.75 0 0 1-.976.073l-.084-.073L12 13.061l-2.47 2.47a.75.75 0 0 1-.976.072l-.084-.073a.75.75 0 0 1-.073-.976l.073-.084L10.939 12l-2.47-2.47a.75.75 0 0 1-.072-.976l.073-.084a.75.75 0 0 1 .976-.073l.084.073L12 10.939l2.47-2.47a.75.75 0 0 1 .976-.072Z",
|
"dismiss-circle-outline": "M12 2c5.523 0 10 4.477 10 10s-4.477 10-10 10S2 17.523 2 12 6.477 2 12 2Zm0 1.5a8.5 8.5 0 1 0 0 17 8.5 8.5 0 0 0 0-17Zm3.446 4.897.084.073a.75.75 0 0 1 .073.976l-.073.084L13.061 12l2.47 2.47a.75.75 0 0 1 .072.976l-.073.084a.75.75 0 0 1-.976.073l-.084-.073L12 13.061l-2.47 2.47a.75.75 0 0 1-.976.072l-.084-.073a.75.75 0 0 1-.073-.976l.073-.084L10.939 12l-2.47-2.47a.75.75 0 0 1-.072-.976l.073-.084a.75.75 0 0 1 .976-.073l.084.073L12 10.939l2.47-2.47a.75.75 0 0 1 .976-.072Z",
|
||||||
"dismiss-outline": "m4.397 4.554.073-.084a.75.75 0 0 1 .976-.073l.084.073L12 10.939l6.47-6.47a.75.75 0 1 1 1.06 1.061L13.061 12l6.47 6.47a.75.75 0 0 1 .072.976l-.073.084a.75.75 0 0 1-.976.073l-.084-.073L12 13.061l-6.47 6.47a.75.75 0 0 1-1.06-1.061L10.939 12l-6.47-6.47a.75.75 0 0 1-.072-.976l.073-.084-.073.084Z",
|
"dismiss-outline": "m4.397 4.554.073-.084a.75.75 0 0 1 .976-.073l.084.073L12 10.939l6.47-6.47a.75.75 0 1 1 1.06 1.061L13.061 12l6.47 6.47a.75.75 0 0 1 .072.976l-.073.084a.75.75 0 0 1-.976.073l-.084-.073L12 13.061l-6.47 6.47a.75.75 0 0 1-1.06-1.061L10.939 12l-6.47-6.47a.75.75 0 0 1-.072-.976l.073-.084-.073.084Z",
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ const {
|
|||||||
TERMS_URL: termsURL,
|
TERMS_URL: termsURL,
|
||||||
WIDGET_BRAND_URL: widgetBrandURL,
|
WIDGET_BRAND_URL: widgetBrandURL,
|
||||||
DISABLE_USER_PROFILE_UPDATE: disableUserProfileUpdate,
|
DISABLE_USER_PROFILE_UPDATE: disableUserProfileUpdate,
|
||||||
|
DEPLOYMENT_ENV: deploymentEnv,
|
||||||
} = window.globalConfig || {};
|
} = window.globalConfig || {};
|
||||||
|
|
||||||
const state = {
|
const state = {
|
||||||
@@ -23,6 +24,7 @@ const state = {
|
|||||||
appVersion,
|
appVersion,
|
||||||
brandName,
|
brandName,
|
||||||
chatwootInboxToken,
|
chatwootInboxToken,
|
||||||
|
deploymentEnv,
|
||||||
createNewAccountFromDashboard,
|
createNewAccountFromDashboard,
|
||||||
directUploadsEnabled: directUploadsEnabled === 'true',
|
directUploadsEnabled: directUploadsEnabled === 'true',
|
||||||
disableUserProfileUpdate: disableUserProfileUpdate === 'true',
|
disableUserProfileUpdate: disableUserProfileUpdate === 'true',
|
||||||
@@ -38,6 +40,7 @@ const state = {
|
|||||||
|
|
||||||
export const getters = {
|
export const getters = {
|
||||||
get: $state => $state,
|
get: $state => $state,
|
||||||
|
isOnChatwootCloud: $state => $state.deploymentEnv === 'cloud',
|
||||||
};
|
};
|
||||||
|
|
||||||
export const actions = {};
|
export const actions = {};
|
||||||
|
|||||||
@@ -10,4 +10,12 @@ class AccountPolicy < ApplicationPolicy
|
|||||||
def update_active_at?
|
def update_active_at?
|
||||||
true
|
true
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def subscription?
|
||||||
|
@account_user.administrator?
|
||||||
|
end
|
||||||
|
|
||||||
|
def checkout?
|
||||||
|
@account_user.administrator?
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,6 +1,11 @@
|
|||||||
json.auto_resolve_duration resource.auto_resolve_duration
|
json.auto_resolve_duration resource.auto_resolve_duration
|
||||||
json.created_at resource.created_at
|
json.created_at resource.created_at
|
||||||
json.custom_attributes resource.custom_attributes
|
if resource.custom_attributes.present?
|
||||||
|
json.custom_attributes do
|
||||||
|
json.plan_name resource.custom_attributes['plan_name']
|
||||||
|
json.subscribed_quantity resource.custom_attributes['subscribed_quantity']
|
||||||
|
end
|
||||||
|
end
|
||||||
json.custom_email_domain_enabled @account.custom_email_domain_enabled
|
json.custom_email_domain_enabled @account.custom_email_domain_enabled
|
||||||
json.domain @account.domain
|
json.domain @account.domain
|
||||||
json.features @account.enabled_features
|
json.features @account.enabled_features
|
||||||
|
|||||||
@@ -68,3 +68,7 @@
|
|||||||
- name: CSML_BOT_API_KEY
|
- name: CSML_BOT_API_KEY
|
||||||
value:
|
value:
|
||||||
locked: false
|
locked: false
|
||||||
|
- name: CHATWOOT_CLOUD_PLANS
|
||||||
|
value:
|
||||||
|
- name: DEPLOYMENT_ENV
|
||||||
|
value: self-hosted
|
||||||
|
|||||||
@@ -224,6 +224,23 @@ Rails.application.routes.draw do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
if ChatwootApp.enterprise?
|
||||||
|
namespace :enterprise, defaults: { format: 'json' } do
|
||||||
|
namespace :api do
|
||||||
|
namespace :v1 do
|
||||||
|
resources :accounts do
|
||||||
|
member do
|
||||||
|
post :checkout
|
||||||
|
post :subscription
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
post 'webhooks/stripe', to: 'webhooks/stripe#process_payload'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
# ----------------------------------------------------------------------
|
# ----------------------------------------------------------------------
|
||||||
# Routes for platform APIs
|
# Routes for platform APIs
|
||||||
namespace :platform, defaults: { format: 'json' } do
|
namespace :platform, defaults: { format: 'json' } do
|
||||||
|
|||||||
@@ -0,0 +1,47 @@
|
|||||||
|
class Enterprise::Api::V1::AccountsController < Api::BaseController
|
||||||
|
before_action :fetch_account
|
||||||
|
before_action :check_authorization
|
||||||
|
|
||||||
|
def subscription
|
||||||
|
Enterprise::CreateStripeCustomerJob.perform_later(@account) if stripe_customer_id.blank?
|
||||||
|
head :no_content
|
||||||
|
end
|
||||||
|
|
||||||
|
def checkout
|
||||||
|
return create_stripe_billing_session(stripe_customer_id) if stripe_customer_id.present?
|
||||||
|
|
||||||
|
render_invalid_billing_details
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def fetch_account
|
||||||
|
@account = current_user.accounts.find(params[:id])
|
||||||
|
@current_account_user = @account.account_users.find_by(user_id: current_user.id)
|
||||||
|
end
|
||||||
|
|
||||||
|
def stripe_customer_id
|
||||||
|
@account.custom_attributes['stripe_customer_id']
|
||||||
|
end
|
||||||
|
|
||||||
|
def render_invalid_billing_details
|
||||||
|
render_could_not_create_error('Please subscribe to a plan before viewing the billing details')
|
||||||
|
end
|
||||||
|
|
||||||
|
def create_stripe_billing_session(customer_id)
|
||||||
|
session = Enterprise::Billing::CreateSessionService.new.create_session(customer_id)
|
||||||
|
render_redirect_url(session.url)
|
||||||
|
end
|
||||||
|
|
||||||
|
def render_redirect_url(redirect_url)
|
||||||
|
render json: { redirect_url: redirect_url }
|
||||||
|
end
|
||||||
|
|
||||||
|
def pundit_user
|
||||||
|
{
|
||||||
|
user: current_user,
|
||||||
|
account: @account,
|
||||||
|
account_user: @current_account_user
|
||||||
|
}
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
class Enterprise::Api::V1::Webhooks::StripeController < ActionController::API
|
||||||
|
def process_payload
|
||||||
|
# Get the event payload and signature
|
||||||
|
payload = request.body.read
|
||||||
|
sig_header = request.headers['Stripe-Signature']
|
||||||
|
|
||||||
|
# Attempt to verify the signature. If successful, we'll handle the event
|
||||||
|
begin
|
||||||
|
event = Stripe::Webhook.construct_event(payload, sig_header, ENV.fetch('STRIPE_WEBHOOK_SECRET', nil))
|
||||||
|
::Enterprise::Billing::HandleStripeEventService.new.perform(event: event)
|
||||||
|
# If we fail to verify the signature, then something was wrong with the request
|
||||||
|
rescue JSON::ParserError, Stripe::SignatureVerificationError
|
||||||
|
# Invalid payload
|
||||||
|
head :bad_request
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
# We've successfully processed the event without blowing up
|
||||||
|
head :ok
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
class Enterprise::CreateStripeCustomerJob < ApplicationJob
|
||||||
|
queue_as :default
|
||||||
|
|
||||||
|
def perform(account)
|
||||||
|
Enterprise::Billing::CreateStripeCustomerService.new(account: account).perform
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -1,13 +1,18 @@
|
|||||||
module Enterprise::Account
|
module Enterprise::Account
|
||||||
def usage_limits
|
def usage_limits
|
||||||
{
|
{
|
||||||
agents: get_limits(:agents).to_i,
|
agents: agent_limits,
|
||||||
inboxes: get_limits(:inboxes).to_i
|
inboxes: get_limits(:inboxes).to_i
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
|
def agent_limits
|
||||||
|
subscribed_quantity = custom_attributes['subscribed_quantity']
|
||||||
|
subscribed_quantity || get_limits(:agents)
|
||||||
|
end
|
||||||
|
|
||||||
def get_limits(limit_name)
|
def get_limits(limit_name)
|
||||||
config_name = "ACCOUNT_#{limit_name.to_s.upcase}_LIMIT"
|
config_name = "ACCOUNT_#{limit_name.to_s.upcase}_LIMIT"
|
||||||
self[:limits][limit_name.to_s] || GlobalConfig.get(config_name)[config_name] || ChatwootApp.max_limit
|
self[:limits][limit_name.to_s] || GlobalConfig.get(config_name)[config_name] || ChatwootApp.max_limit
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
class Enterprise::Billing::CreateSessionService
|
||||||
|
def create_session(customer_id, return_url = ENV.fetch('FRONTEND_URL'))
|
||||||
|
Stripe::BillingPortal::Session.create(
|
||||||
|
{
|
||||||
|
customer: customer_id,
|
||||||
|
return_url: return_url
|
||||||
|
}
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
class Enterprise::Billing::CreateStripeCustomerService
|
||||||
|
pattr_initialize [:account!]
|
||||||
|
|
||||||
|
DEFAULT_QUANTITY = 2
|
||||||
|
|
||||||
|
def perform
|
||||||
|
customer_id = prepare_customer_id
|
||||||
|
subscription = Stripe::Subscription.create(
|
||||||
|
{
|
||||||
|
customer: customer_id,
|
||||||
|
items: [{ price: price_id, quantity: default_quantity }]
|
||||||
|
}
|
||||||
|
)
|
||||||
|
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
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def prepare_customer_id
|
||||||
|
customer_id = account.custom_attributes['stripe_customer_id']
|
||||||
|
if customer_id.blank?
|
||||||
|
customer = Stripe::Customer.create({ name: account.name, email: billing_email })
|
||||||
|
customer_id = customer.id
|
||||||
|
end
|
||||||
|
customer_id
|
||||||
|
end
|
||||||
|
|
||||||
|
def default_quantity
|
||||||
|
default_plan['default_quantity'] || DEFAULT_QUANTITY
|
||||||
|
end
|
||||||
|
|
||||||
|
def billing_email
|
||||||
|
account.administrators.first.email
|
||||||
|
end
|
||||||
|
|
||||||
|
def default_plan
|
||||||
|
installation_config = InstallationConfig.find_by(name: 'CHATWOOT_CLOUD_PLANS')
|
||||||
|
@default_plan ||= installation_config.value.first
|
||||||
|
end
|
||||||
|
|
||||||
|
def price_id
|
||||||
|
price_ids = default_plan['price_ids']
|
||||||
|
price_ids.first
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
class Enterprise::Billing::HandleStripeEventService
|
||||||
|
def perform(event:)
|
||||||
|
ensure_event_context(event)
|
||||||
|
case @event.type
|
||||||
|
when 'customer.subscription.updated'
|
||||||
|
plan = find_plan(subscription['plan']['product'])
|
||||||
|
account.update(
|
||||||
|
custom_attributes: {
|
||||||
|
stripe_customer_id: subscription.customer,
|
||||||
|
stripe_price_id: subscription['plan']['id'],
|
||||||
|
stripe_product_id: subscription['plan']['product'],
|
||||||
|
plan_name: plan['name'],
|
||||||
|
subscribed_quantity: subscription['quantity']
|
||||||
|
}
|
||||||
|
)
|
||||||
|
when 'customer.subscription.deleted'
|
||||||
|
Enterprise::Billing::CreateStripeCustomerService.new(account: account).perform
|
||||||
|
else
|
||||||
|
Rails.logger.debug { "Unhandled event type: #{event.type}" }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def ensure_event_context(event)
|
||||||
|
@event = event
|
||||||
|
end
|
||||||
|
|
||||||
|
def subscription
|
||||||
|
@subscription ||= @event.data.object
|
||||||
|
end
|
||||||
|
|
||||||
|
def account
|
||||||
|
@account ||= Account.where("custom_attributes->>'stripe_customer_id' = ?", subscription.customer).first
|
||||||
|
end
|
||||||
|
|
||||||
|
def find_plan(plan_id)
|
||||||
|
installation_config = InstallationConfig.find_by(name: 'CHATWOOT_CLOUD_PLANS')
|
||||||
|
installation_config.value.find { |config| config['product_id'].include?(plan_id) }
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,102 @@
|
|||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
RSpec.describe 'Enterprise Billing APIs', type: :request do
|
||||||
|
let(:account) { create(:account) }
|
||||||
|
let(:admin) { create(:user, account: account, role: :administrator) }
|
||||||
|
let(:agent) { create(:user, account: account, role: :agent) }
|
||||||
|
|
||||||
|
describe 'POST /enterprise/api/v1/accounts/{account.id}/subscription' do
|
||||||
|
context 'when it is an unauthenticated user' do
|
||||||
|
it 'returns unauthorized' do
|
||||||
|
post "/enterprise/api/v1/accounts/#{account.id}/subscription", 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
|
||||||
|
post "/enterprise/api/v1/accounts/#{account.id}/subscription",
|
||||||
|
headers: agent.create_new_auth_token,
|
||||||
|
as: :json
|
||||||
|
|
||||||
|
expect(response).to have_http_status(:unauthorized)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when it is an admin' do
|
||||||
|
it 'enqueues a job' do
|
||||||
|
expect do
|
||||||
|
post "/enterprise/api/v1/accounts/#{account.id}/subscription",
|
||||||
|
headers: admin.create_new_auth_token,
|
||||||
|
as: :json
|
||||||
|
end.to have_enqueued_job(Enterprise::CreateStripeCustomerJob).with(account)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'does not enqueues a job if customer id is present' do
|
||||||
|
account.update!(custom_attributes: { 'stripe_customer_id': 'cus_random_string' })
|
||||||
|
|
||||||
|
expect do
|
||||||
|
post "/enterprise/api/v1/accounts/#{account.id}/subscription",
|
||||||
|
headers: admin.create_new_auth_token,
|
||||||
|
as: :json
|
||||||
|
end.not_to have_enqueued_job(Enterprise::CreateStripeCustomerJob).with(account)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'POST /enterprise/api/v1/accounts/{account.id}/checkout' do
|
||||||
|
context 'when it is an unauthenticated user' do
|
||||||
|
it 'returns unauthorized' do
|
||||||
|
post "/enterprise/api/v1/accounts/#{account.id}/checkout", 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
|
||||||
|
post "/enterprise/api/v1/accounts/#{account.id}/checkout",
|
||||||
|
headers: agent.create_new_auth_token,
|
||||||
|
as: :json
|
||||||
|
|
||||||
|
expect(response).to have_http_status(:unauthorized)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when it is an admin and the stripe customer id is not present' do
|
||||||
|
it 'returns error' do
|
||||||
|
post "/enterprise/api/v1/accounts/#{account.id}/checkout",
|
||||||
|
headers: admin.create_new_auth_token,
|
||||||
|
as: :json
|
||||||
|
|
||||||
|
expect(response).to have_http_status(:unprocessable_entity)
|
||||||
|
json_response = JSON.parse(response.body)
|
||||||
|
expect(json_response['error']).to eq('Please subscribe to a plan before viewing the billing details')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when it is an admin and the stripe customer is present' do
|
||||||
|
it 'calls create session' do
|
||||||
|
account.update!(custom_attributes: { 'stripe_customer_id': 'cus_random_string' })
|
||||||
|
|
||||||
|
create_session_service = double
|
||||||
|
allow(Enterprise::Billing::CreateSessionService).to receive(:new).and_return(create_session_service)
|
||||||
|
allow(create_session_service).to receive(:create_session).and_return(create_session_service)
|
||||||
|
allow(create_session_service).to receive(:url).and_return('https://billing.stripe.com/random_string')
|
||||||
|
|
||||||
|
post "/enterprise/api/v1/accounts/#{account.id}/checkout",
|
||||||
|
headers: admin.create_new_auth_token,
|
||||||
|
as: :json
|
||||||
|
|
||||||
|
expect(response).to have_http_status(:success)
|
||||||
|
json_response = JSON.parse(response.body)
|
||||||
|
expect(json_response['redirect_url']).to eq('https://billing.stripe.com/random_string')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
RSpec.describe Enterprise::CreateStripeCustomerJob, type: :job do
|
||||||
|
include ActiveJob::TestHelper
|
||||||
|
subject(:job) { described_class.perform_later(account) }
|
||||||
|
|
||||||
|
let(:account) { create(:account) }
|
||||||
|
|
||||||
|
it 'queues the job' do
|
||||||
|
expect { job }.to have_enqueued_job(described_class)
|
||||||
|
.with(account)
|
||||||
|
.on_queue('default')
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'executes perform' do
|
||||||
|
create_stripe_customer_service = double
|
||||||
|
allow(Enterprise::Billing::CreateStripeCustomerService)
|
||||||
|
.to receive(:new)
|
||||||
|
.with(account: account)
|
||||||
|
.and_return(create_stripe_customer_service)
|
||||||
|
allow(create_stripe_customer_service).to receive(:perform)
|
||||||
|
|
||||||
|
perform_enqueued_jobs { job }
|
||||||
|
|
||||||
|
expect(Enterprise::Billing::CreateStripeCustomerService).to have_received(:new).with(account: account)
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -28,5 +28,15 @@ RSpec.describe Account do
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
it 'returns limits based on subscription' do
|
||||||
|
account.update(limits: { agents: 10 }, custom_attributes: { subscribed_quantity: 5 })
|
||||||
|
expect(account.usage_limits).to eq(
|
||||||
|
{
|
||||||
|
agents: 5,
|
||||||
|
inboxes: ChatwootApp.max_limit
|
||||||
|
}
|
||||||
|
)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
describe Enterprise::Billing::CreateSessionService do
|
||||||
|
subject(:create_session_service) { described_class }
|
||||||
|
|
||||||
|
describe '#perform' do
|
||||||
|
it 'calls stripe billing portal session' do
|
||||||
|
customer_id = 'cus_random_number'
|
||||||
|
return_url = 'https://www.chatwoot.com'
|
||||||
|
allow(Stripe::BillingPortal::Session).to receive(:create).with({ customer: customer_id, return_url: return_url })
|
||||||
|
|
||||||
|
create_session_service.new.create_session(customer_id, return_url)
|
||||||
|
|
||||||
|
expect(Stripe::BillingPortal::Session).to have_received(:create).with(
|
||||||
|
{
|
||||||
|
customer: customer_id,
|
||||||
|
return_url: return_url
|
||||||
|
}
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
describe Enterprise::Billing::CreateStripeCustomerService do
|
||||||
|
subject(:create_stripe_customer_service) { described_class }
|
||||||
|
|
||||||
|
let(:account) { create(:account) }
|
||||||
|
let!(:admin1) { create(:user, account: account, role: :administrator) }
|
||||||
|
let(:admin2) { create(:user, account: account, role: :administrator) }
|
||||||
|
|
||||||
|
describe '#perform' do
|
||||||
|
before do
|
||||||
|
create(
|
||||||
|
:installation_config,
|
||||||
|
{ name: 'CHATWOOT_CLOUD_PLANS', value: [
|
||||||
|
{ 'name' => 'A Plan Name', 'product_id' => ['prod_hacker_random'], 'price_ids' => ['price_hacker_random'] }
|
||||||
|
] }
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'does not call stripe methods if customer id is present' do
|
||||||
|
account.update!(custom_attributes: { stripe_customer_id: 'cus_random_number' })
|
||||||
|
|
||||||
|
allow(Stripe::Customer).to receive(:create)
|
||||||
|
allow(Stripe::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
|
||||||
|
|
||||||
|
expect(Stripe::Customer).not_to have_received(:create)
|
||||||
|
expect(Stripe::Subscription)
|
||||||
|
.to have_received(:create)
|
||||||
|
.with({ customer: 'cus_random_number', items: [{ price: 'price_hacker_random', quantity: 2 }] })
|
||||||
|
|
||||||
|
expect(account.reload.custom_attributes).to eq(
|
||||||
|
{
|
||||||
|
stripe_customer_id: 'cus_random_number',
|
||||||
|
stripe_price_id: 'price_random_number',
|
||||||
|
stripe_product_id: 'prod_random_number',
|
||||||
|
subscribed_quantity: 2,
|
||||||
|
plan_name: 'A Plan Name'
|
||||||
|
}.with_indifferent_access
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'calls stripe methods to create a customer and updates the account' do
|
||||||
|
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(
|
||||||
|
{
|
||||||
|
plan: { id: 'price_random_number', product: 'prod_random_number' },
|
||||||
|
quantity: 2
|
||||||
|
}.with_indifferent_access
|
||||||
|
)
|
||||||
|
|
||||||
|
create_stripe_customer_service.new(account: account).perform
|
||||||
|
|
||||||
|
expect(Stripe::Customer).to have_received(:create).with({ name: account.name, email: admin1.email })
|
||||||
|
expect(Stripe::Subscription)
|
||||||
|
.to have_received(:create)
|
||||||
|
.with({ customer: customer.id, items: [{ price: 'price_hacker_random', quantity: 2 }] })
|
||||||
|
|
||||||
|
expect(account.reload.custom_attributes).to eq(
|
||||||
|
{
|
||||||
|
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'
|
||||||
|
}.with_indifferent_access
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
Reference in New Issue
Block a user