Files
chatwoot/app/javascript/dashboard/store/modules/accounts.js
T

411 lines
13 KiB
JavaScript

import * as MutationHelpers from 'shared/helpers/vuex/mutationHelpers';
import * as types from '../mutation-types';
import AccountAPI from '../../api/account';
import { differenceInDays } from 'date-fns';
import EnterpriseAccountAPI from '../../api/enterprise/account';
import { throwErrorMessage } from '../utils/api';
import { getLanguageDirection } from 'dashboard/components/widgets/conversation/advancedFilterItems/languages';
const findRecordById = ($state, id) =>
$state.records.find(record => record.id === Number(id)) || {};
const TRIAL_PERIOD_DAYS = 15;
const state = {
records: [],
uiFlags: {
isFetching: false,
isFetchingItem: false,
isUpdating: false,
isCheckoutInProcess: false,
},
v2Billing: {
creditsBalance: null,
creditGrants: [],
pricingPlans: [],
topupOptions: [],
uiFlags: {
isFetchingBalance: false,
isFetchingGrants: false,
isFetchingPlans: false,
isFetchingTopupOptions: false,
isTopupInProcess: false,
isSubscribeInProcess: false,
isCancelInProcess: false,
},
},
};
export const getters = {
getAccount: $state => id => {
return findRecordById($state, id);
},
getUIFlags($state) {
return $state.uiFlags;
},
getV2BillingData($state) {
return $state.v2Billing;
},
getV2BillingUIFlags($state) {
return $state.v2Billing.uiFlags;
},
isRTL: ($state, _getters, rootState, rootGetters) => {
const accountId = Number(rootState.route?.params?.accountId);
const userLocale = rootGetters?.getUISettings?.locale;
const accountLocale =
accountId && findRecordById($state, accountId)?.locale;
// Prefer user locale; fallback to account locale
const effectiveLocale = userLocale ?? accountLocale;
return effectiveLocale ? getLanguageDirection(effectiveLocale) : false;
},
isTrialAccount: $state => id => {
const account = findRecordById($state, id);
const createdAt = new Date(account.created_at);
const diffDays = differenceInDays(new Date(), createdAt);
return diffDays <= TRIAL_PERIOD_DAYS;
},
isFeatureEnabledonAccount: $state => (id, featureName) => {
const { features = {} } = findRecordById($state, id);
return features[featureName] || false;
},
};
export const actions = {
get: async ({ commit }) => {
commit(types.default.SET_ACCOUNT_UI_FLAG, { isFetchingItem: true });
try {
const response = await AccountAPI.get();
commit(types.default.ADD_ACCOUNT, response.data);
commit(types.default.SET_ACCOUNT_UI_FLAG, {
isFetchingItem: false,
});
} catch (error) {
commit(types.default.SET_ACCOUNT_UI_FLAG, {
isFetchingItem: false,
});
}
},
update: async ({ commit }, { options, ...updateObj }) => {
if (options?.silent !== true) {
commit(types.default.SET_ACCOUNT_UI_FLAG, { isUpdating: true });
}
try {
const response = await AccountAPI.update('', updateObj);
commit(types.default.EDIT_ACCOUNT, response.data);
commit(types.default.SET_ACCOUNT_UI_FLAG, { isUpdating: false });
} catch (error) {
commit(types.default.SET_ACCOUNT_UI_FLAG, { isUpdating: false });
throw new Error(error);
}
},
delete: async ({ commit }, { id }) => {
commit(types.default.SET_ACCOUNT_UI_FLAG, { isUpdating: true });
try {
await AccountAPI.delete(id);
commit(types.default.SET_ACCOUNT_UI_FLAG, { isUpdating: false });
} catch (error) {
commit(types.default.SET_ACCOUNT_UI_FLAG, { isUpdating: false });
throw new Error(error);
}
},
toggleDeletion: async (
{ commit },
{ action_type } = { action_type: 'delete' }
) => {
commit(types.default.SET_ACCOUNT_UI_FLAG, { isUpdating: true });
try {
await EnterpriseAccountAPI.toggleDeletion(action_type);
commit(types.default.SET_ACCOUNT_UI_FLAG, { isUpdating: false });
} catch (error) {
commit(types.default.SET_ACCOUNT_UI_FLAG, { isUpdating: false });
throw new Error(error);
}
},
create: async ({ commit }, accountInfo) => {
commit(types.default.SET_ACCOUNT_UI_FLAG, { isCreating: true });
try {
const response = await AccountAPI.createAccount(accountInfo);
const account_id = response.data.data.account_id;
commit(types.default.SET_ACCOUNT_UI_FLAG, { isCreating: false });
return account_id;
} catch (error) {
commit(types.default.SET_ACCOUNT_UI_FLAG, { isCreating: false });
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 });
}
},
limits: async ({ commit }) => {
try {
const response = await EnterpriseAccountAPI.getLimits();
commit(types.default.SET_ACCOUNT_LIMITS, response.data);
} catch (error) {
// silent error
}
},
getCacheKeys: async () => {
return AccountAPI.getCacheKeys();
},
// V2 Billing actions
fetchCreditsBalance: async ({ commit }) => {
commit(types.default.SET_V2_BILLING_UI_FLAG, { isFetchingBalance: true });
try {
const response = await EnterpriseAccountAPI.getLimits();
const responses = response.data?.limits?.captain?.responses || {};
// Transform captain.responses to the format expected by the UI
const creditsBalance = {
total_credits: responses.total_count || 0,
monthly_credits: responses.monthly || 0,
topup_credits: responses.topup || 0,
usage_this_month: responses.consumed || 0,
current_available: responses.current_available || 0,
};
commit(types.default.SET_CREDITS_BALANCE, creditsBalance);
} catch (error) {
throwErrorMessage(error);
} finally {
commit(types.default.SET_V2_BILLING_UI_FLAG, {
isFetchingBalance: false,
});
}
},
fetchCreditGrants: async ({ commit }) => {
commit(types.default.SET_V2_BILLING_UI_FLAG, { isFetchingGrants: true });
try {
const response = await EnterpriseAccountAPI.creditGrants();
commit(types.default.SET_CREDIT_GRANTS, response.data.credit_grants);
} catch (error) {
throwErrorMessage(error);
} finally {
commit(types.default.SET_V2_BILLING_UI_FLAG, {
isFetchingGrants: false,
});
}
},
fetchV2PricingPlans: async ({ commit }) => {
commit(types.default.SET_V2_BILLING_UI_FLAG, { isFetchingPlans: true });
try {
const response = await EnterpriseAccountAPI.v2PricingPlans();
commit(types.default.SET_V2_PRICING_PLANS, response.data.pricing_plans);
} catch (error) {
throwErrorMessage(error);
} finally {
commit(types.default.SET_V2_BILLING_UI_FLAG, { isFetchingPlans: false });
}
},
fetchV2TopupOptions: async ({ commit }) => {
commit(types.default.SET_V2_BILLING_UI_FLAG, {
isFetchingTopupOptions: true,
});
try {
const response = await EnterpriseAccountAPI.v2TopupOptions();
// Transform backend structure: rename 'amount' to 'price'
const transformedOptions = response.data.topup_options.map(option => ({
...option,
price: option.amount,
id: `topup-${option.credits}`,
}));
commit(types.default.SET_V2_TOPUP_OPTIONS, transformedOptions);
} catch (error) {
throwErrorMessage(error);
} finally {
commit(types.default.SET_V2_BILLING_UI_FLAG, {
isFetchingTopupOptions: false,
});
}
},
v2Topup: async ({ commit }, data) => {
commit(types.default.SET_V2_BILLING_UI_FLAG, { isTopupInProcess: true });
try {
await EnterpriseAccountAPI.v2Topup(data);
return { success: true };
} catch (error) {
const errorMessage = error.response?.data?.error || error.message;
return { success: false, error: errorMessage };
} finally {
commit(types.default.SET_V2_BILLING_UI_FLAG, { isTopupInProcess: false });
}
},
v2Subscribe: async ({ commit, dispatch }, data) => {
commit(types.default.SET_V2_BILLING_UI_FLAG, {
isSubscribeInProcess: true,
});
try {
const response = await EnterpriseAccountAPI.v2Subscribe(data);
// If backend returns redirect_url, redirect to Stripe checkout
if (response.data.redirect_url) {
window.location.href = response.data.redirect_url;
return { success: true, redirecting: true };
}
// Otherwise refresh data (for non-Stripe flows)
await Promise.all([
dispatch('fetchCreditsBalance'),
dispatch('fetchV2PricingPlans'),
dispatch('subscription'),
]);
return { success: true };
} catch (error) {
throwErrorMessage(error);
return { success: false };
} finally {
commit(types.default.SET_V2_BILLING_UI_FLAG, {
isSubscribeInProcess: false,
});
}
},
v2CancelSubscription: async ({ commit, dispatch }, data) => {
commit(types.default.SET_V2_BILLING_UI_FLAG, { isCancelInProcess: true });
try {
const response = await EnterpriseAccountAPI.cancelSubscription(data);
// Update account with new subscription status
if (response.data.id && response.data.custom_attributes) {
commit(types.default.SET_ACCOUNT_LIMITS, response.data);
}
// Refresh balance and plans after cancellation
await Promise.all([
dispatch('fetchCreditsBalance'),
dispatch('fetchV2PricingPlans'),
]);
return { success: true };
} catch (error) {
const errorMessage = error.response?.data?.error || error.message;
return { success: false, error: errorMessage };
} finally {
commit(types.default.SET_V2_BILLING_UI_FLAG, {
isCancelInProcess: false,
});
}
},
v2UpdateQuantity: async (
{ commit, dispatch },
{ pricingPlanId, quantity }
) => {
commit(types.default.SET_V2_BILLING_UI_FLAG, {
isSubscribeInProcess: true,
});
try {
const response = await EnterpriseAccountAPI.changePricingPlan({
pricing_plan_id: pricingPlanId,
quantity,
});
// Update account with new quantity
if (response.data.id && response.data.custom_attributes) {
commit(types.default.SET_ACCOUNT_LIMITS, response.data);
}
// Refresh balance after quantity update
await dispatch('fetchCreditsBalance');
return { success: true };
} catch (error) {
const errorMessage = error.response?.data?.error || error.message;
return { success: false, error: errorMessage };
} finally {
commit(types.default.SET_V2_BILLING_UI_FLAG, {
isSubscribeInProcess: false,
});
}
},
v2ChangePlan: async ({ commit, dispatch }, data) => {
commit(types.default.SET_V2_BILLING_UI_FLAG, {
isSubscribeInProcess: true,
});
try {
const response = await EnterpriseAccountAPI.changePricingPlan(data);
// Update account with new plan
if (response.data.id && response.data.custom_attributes) {
commit(types.default.SET_ACCOUNT_LIMITS, response.data);
}
// Refresh balance and plans after change
await Promise.all([
dispatch('fetchCreditsBalance'),
dispatch('fetchV2PricingPlans'),
]);
return { success: true };
} catch (error) {
const errorMessage = error.response?.data?.error || error.message;
return { success: false, error: errorMessage };
} finally {
commit(types.default.SET_V2_BILLING_UI_FLAG, {
isSubscribeInProcess: false,
});
}
},
};
export const mutations = {
[types.default.SET_ACCOUNT_UI_FLAG]($state, data) {
$state.uiFlags = {
...$state.uiFlags,
...data,
};
},
[types.default.ADD_ACCOUNT]: MutationHelpers.setSingleRecord,
[types.default.EDIT_ACCOUNT]: MutationHelpers.update,
[types.default.SET_ACCOUNT_LIMITS]: MutationHelpers.updateAttributes,
// V2 Billing mutations
[types.default.SET_V2_BILLING_UI_FLAG]($state, data) {
$state.v2Billing.uiFlags = {
...$state.v2Billing.uiFlags,
...data,
};
},
[types.default.SET_CREDITS_BALANCE]($state, data) {
$state.v2Billing.creditsBalance = data;
},
[types.default.SET_CREDIT_GRANTS]($state, data) {
$state.v2Billing.creditGrants = data;
},
[types.default.SET_V2_PRICING_PLANS]($state, data) {
$state.v2Billing.pricingPlans = data;
},
[types.default.SET_V2_TOPUP_OPTIONS]($state, data) {
$state.v2Billing.topupOptions = data;
},
};
export default {
namespaced: true,
state,
getters,
actions,
mutations,
};