## Linear ticket - https://linear.app/chatwoot/issue/CW-7253/billing-brl-pix-new-users ## Description New accounts that sign up in Brazilian Portuguese are now billed in BRL instead of USD. Their Stripe customer is created with a Brazil address and Portuguese locale (so the Stripe portal offers Real prices and PIX), and the AI credit top-up flow shows packages priced in the account's billing currency. Currency support is config-driven, so adding another currency later is a configuration change rather than a code change. ## Type of change - [ ] New feature (non-breaking change which adds functionality) ## How Has This Been Tested? - https://www.loom.com/share/c8d3d08c1b844ed6b820438d4209491a ## Screenshot <img width="904" height="440" alt="image" src="https://github.com/user-attachments/assets/6f19fad8-e6af-46ea-b99f-b0265bb9eeec" /> ## Checklist: - [ ] My code follows the style guidelines of this project - [ ] I have performed a self-review of my code - [ ] I have commented on my code, particularly in hard-to-understand areas - [ ] I have made corresponding changes to the documentation - [ ] My changes generate no new warnings - [ ] I have added tests that prove my fix is effective or that my feature works - [ ] New and existing unit tests pass locally with my changes - [ ] Any dependent changes have been merged and published in downstream modules
204 lines
6.4 KiB
JavaScript
204 lines
6.4 KiB
JavaScript
import * as MutationHelpers from 'shared/helpers/vuex/mutationHelpers';
|
|
import * as types from '../mutation-types';
|
|
import AccountAPI from '../../api/account';
|
|
import OnboardingAPI from '../../api/onboarding';
|
|
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,
|
|
isFetchingLimits: false,
|
|
},
|
|
};
|
|
|
|
export const getters = {
|
|
getAccount: $state => id => {
|
|
return findRecordById($state, id);
|
|
},
|
|
getUIFlags($state) {
|
|
return $state.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 }, { silent } = {}) => {
|
|
if (!silent) {
|
|
commit(types.default.SET_ACCOUNT_UI_FLAG, { isFetchingItem: true });
|
|
}
|
|
try {
|
|
const response = await AccountAPI.get();
|
|
commit(types.default.ADD_ACCOUNT, response.data);
|
|
} catch {
|
|
// silent failure
|
|
} finally {
|
|
if (!silent) {
|
|
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);
|
|
}
|
|
},
|
|
finishOnboarding: async ({ commit }, payload) => {
|
|
commit(types.default.SET_ACCOUNT_UI_FLAG, { isUpdating: true });
|
|
try {
|
|
const response = await OnboardingAPI.update(payload);
|
|
commit(types.default.EDIT_ACCOUNT, response.data);
|
|
} finally {
|
|
commit(types.default.SET_ACCOUNT_UI_FLAG, { isUpdating: false });
|
|
}
|
|
},
|
|
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 {
|
|
const response = await EnterpriseAccountAPI.subscription();
|
|
return response.data;
|
|
} catch (error) {
|
|
throwErrorMessage(error);
|
|
return null;
|
|
} finally {
|
|
commit(types.default.SET_ACCOUNT_UI_FLAG, { isCheckoutInProcess: false });
|
|
}
|
|
},
|
|
|
|
selectBillingCurrency: async ({ commit }, currency) => {
|
|
commit(types.default.SET_ACCOUNT_UI_FLAG, { isCheckoutInProcess: true });
|
|
try {
|
|
await EnterpriseAccountAPI.selectBillingCurrency(currency);
|
|
} catch (error) {
|
|
throwErrorMessage(error);
|
|
} finally {
|
|
commit(types.default.SET_ACCOUNT_UI_FLAG, { isCheckoutInProcess: false });
|
|
}
|
|
},
|
|
|
|
limits: async ({ commit }) => {
|
|
commit(types.default.SET_ACCOUNT_UI_FLAG, { isFetchingLimits: true });
|
|
try {
|
|
const response = await EnterpriseAccountAPI.getLimits();
|
|
commit(types.default.SET_ACCOUNT_LIMITS, response.data);
|
|
} catch (error) {
|
|
// silent error
|
|
} finally {
|
|
commit(types.default.SET_ACCOUNT_UI_FLAG, { isFetchingLimits: false });
|
|
}
|
|
},
|
|
|
|
getCacheKeys: async () => {
|
|
return AccountAPI.getCacheKeys();
|
|
},
|
|
};
|
|
|
|
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,
|
|
};
|
|
|
|
export default {
|
|
namespaced: true,
|
|
state,
|
|
getters,
|
|
actions,
|
|
mutations,
|
|
};
|