From cb0b554e941b9a27138fd887ea8a153bcfb0ca5c Mon Sep 17 00:00:00 2001 From: Rogee Date: Sun, 23 Aug 2026 23:19:51 +0800 Subject: [PATCH] HH-555: remove advanced assignment commercial gate (#138) * fix(HH-555): remove advanced assignment paywall * fix(HH-555): gate assignment entries with assignment v2 --------- Co-authored-by: Rogee --- .../components-next/sidebar/Sidebar.vue | 6 +- .../dashboard/i18n/locale/en/inboxMgmt.json | 2 - .../dashboard/i18n/locale/en/settings.json | 4 +- .../i18n/locale/zh_CN/inboxMgmt.json | 2 - .../dashboard/i18n/locale/zh_CN/settings.json | 4 +- .../settings/assignmentPolicy/Index.vue | 8 +- .../advancedAssignmentPaywall.spec.js | 224 ++++++++++++++++-- .../assignmentPolicy.routes.js | 6 +- .../components/AgentAssignmentPolicyForm.vue | 47 +--- .../inbox/settingsPage/CollaboratorsPage.vue | 91 +------ 10 files changed, 223 insertions(+), 171 deletions(-) diff --git a/frontend/app/javascript/dashboard/components-next/sidebar/Sidebar.vue b/frontend/app/javascript/dashboard/components-next/sidebar/Sidebar.vue index 32aaf968..b17274b0 100644 --- a/frontend/app/javascript/dashboard/components-next/sidebar/Sidebar.vue +++ b/frontend/app/javascript/dashboard/components-next/sidebar/Sidebar.vue @@ -54,10 +54,10 @@ const isFeatureEnabledonAccount = useMapGetter( 'accounts/isFeatureEnabledonAccount' ); -const hasAdvancedAssignment = computed(() => { +const hasAssignmentV2 = computed(() => { return isFeatureEnabledonAccount.value( accountId.value, - FEATURE_FLAGS.ADVANCED_ASSIGNMENT + FEATURE_FLAGS.ASSIGNMENT_V2 ); }); @@ -690,7 +690,7 @@ const menuItems = computed(() => { ], to: accountScopedRoute('settings_teams_list'), }, - ...(hasAdvancedAssignment.value + ...(hasAssignmentV2.value ? [ { name: 'Settings Agent Assignment', diff --git a/frontend/app/javascript/dashboard/i18n/locale/en/inboxMgmt.json b/frontend/app/javascript/dashboard/i18n/locale/en/inboxMgmt.json index 9a0dda77..3bf7cdc3 100644 --- a/frontend/app/javascript/dashboard/i18n/locale/en/inboxMgmt.json +++ b/frontend/app/javascript/dashboard/i18n/locale/en/inboxMgmt.json @@ -869,8 +869,6 @@ "ROUND_ROBIN": "Round robin", "BALANCED": "Balanced assignment" }, - "UPGRADE_PROMPT": "Custom assignment policies are available on the Business plan", - "UPGRADE_TO_BUSINESS": "Upgrade to Business", "DEFAULT_POLICY_LINKED": "Default policy linked", "DEFAULT_POLICY_DESCRIPTION": "Link a custom assignment policy to customize how conversations are assigned to agents in this inbox.", "LINK_EXISTING_POLICY": "Link existing policy", diff --git a/frontend/app/javascript/dashboard/i18n/locale/en/settings.json b/frontend/app/javascript/dashboard/i18n/locale/en/settings.json index 921fda25..71503d57 100644 --- a/frontend/app/javascript/dashboard/i18n/locale/en/settings.json +++ b/frontend/app/javascript/dashboard/i18n/locale/en/settings.json @@ -764,9 +764,7 @@ }, "BALANCED": { "LABEL": "Balanced", - "DESCRIPTION": "Assign conversations based on available capacity.", - "PREMIUM_MESSAGE": "Upgrade to access balanced assignment and agent capacity management.", - "PREMIUM_BADGE": "Premium" + "DESCRIPTION": "Assign conversations based on available capacity." } }, "ASSIGNMENT_PRIORITY": { diff --git a/frontend/app/javascript/dashboard/i18n/locale/zh_CN/inboxMgmt.json b/frontend/app/javascript/dashboard/i18n/locale/zh_CN/inboxMgmt.json index a7259946..2e31e28d 100644 --- a/frontend/app/javascript/dashboard/i18n/locale/zh_CN/inboxMgmt.json +++ b/frontend/app/javascript/dashboard/i18n/locale/zh_CN/inboxMgmt.json @@ -869,8 +869,6 @@ "ROUND_ROBIN": "轮询分配", "BALANCED": "均衡分配" }, - "UPGRADE_PROMPT": "自定义分配策略适用于商业版套餐", - "UPGRADE_TO_BUSINESS": "升级到商业版", "DEFAULT_POLICY_LINKED": "已关联默认策略", "DEFAULT_POLICY_DESCRIPTION": "关联自定义分配策略,以配置此收件箱如何将对话分配给客服。", "LINK_EXISTING_POLICY": "关联现有策略", diff --git a/frontend/app/javascript/dashboard/i18n/locale/zh_CN/settings.json b/frontend/app/javascript/dashboard/i18n/locale/zh_CN/settings.json index a852eb74..9c174f1d 100644 --- a/frontend/app/javascript/dashboard/i18n/locale/zh_CN/settings.json +++ b/frontend/app/javascript/dashboard/i18n/locale/zh_CN/settings.json @@ -764,9 +764,7 @@ }, "BALANCED": { "LABEL": "均衡分配", - "DESCRIPTION": "根据可用容量分配会话。", - "PREMIUM_MESSAGE": "升级以使用均衡分配和客服容量管理。", - "PREMIUM_BADGE": "高级" + "DESCRIPTION": "根据可用容量分配会话。" } }, "ASSIGNMENT_PRIORITY": { diff --git a/frontend/app/javascript/dashboard/routes/dashboard/settings/assignmentPolicy/Index.vue b/frontend/app/javascript/dashboard/routes/dashboard/settings/assignmentPolicy/Index.vue index f081cec2..3af7df81 100644 --- a/frontend/app/javascript/dashboard/routes/dashboard/settings/assignmentPolicy/Index.vue +++ b/frontend/app/javascript/dashboard/routes/dashboard/settings/assignmentPolicy/Index.vue @@ -39,18 +39,12 @@ const agentAssignments = computed(() => { }, ]; - // Only show Agent Capacity if BOTH assignment_v2 AND advanced_assignment are enabled - // advanced_assignment identifies premium users const hasAssignmentV2 = isFeatureEnabledonAccount.value( accountId.value, 'assignment_v2' ); - const hasAdvancedAssignment = isFeatureEnabledonAccount.value( - accountId.value, - 'advanced_assignment' - ); - if (hasAssignmentV2 && hasAdvancedAssignment) { + if (hasAssignmentV2) { assignments.push({ key: 'agent_capacity_policy_index', title: t('ASSIGNMENT_POLICY.INDEX.AGENT_CAPACITY_POLICY.TITLE'), diff --git a/frontend/app/javascript/dashboard/routes/dashboard/settings/assignmentPolicy/advancedAssignmentPaywall.spec.js b/frontend/app/javascript/dashboard/routes/dashboard/settings/assignmentPolicy/advancedAssignmentPaywall.spec.js index e8e7a8c4..787c7ac4 100644 --- a/frontend/app/javascript/dashboard/routes/dashboard/settings/assignmentPolicy/advancedAssignmentPaywall.spec.js +++ b/frontend/app/javascript/dashboard/routes/dashboard/settings/assignmentPolicy/advancedAssignmentPaywall.spec.js @@ -1,23 +1,211 @@ -import { readFileSync } from 'node:fs'; +import { flushPromises, shallowMount } from '@vue/test-utils'; +import { createMemoryHistory, createRouter } from 'vue-router'; +import AgentAssignmentPolicyForm from './pages/components/AgentAssignmentPolicyForm.vue'; +import AssignmentPolicyIndex from './Index.vue'; +import assignmentPolicyRoutes from './assignmentPolicy.routes'; +import CollaboratorsPage from '../inbox/settingsPage/CollaboratorsPage.vue'; +import RadioCard from 'dashboard/components-next/radioCard/RadioCard.vue'; +import AssignmentCard from 'dashboard/components-next/AssignmentPolicy/AssignmentCard/AssignmentCard.vue'; +import Sidebar from 'dashboard/components-next/sidebar/Sidebar.vue'; +import { FEATURE_FLAGS } from 'dashboard/featureFlags'; -describe('advanced assignment paywalls', () => { - it.each([ - [ - '../inbox/settingsPage/CollaboratorsPage.vue', - 'v-if="showAdvancedAssignmentPaywall"', - ], - [ - './pages/components/AgentAssignmentPolicyForm.vue', - 'disabled && showAdvancedAssignmentPaywall.value', - ], - ])('uses the deployment-aware paywall policy in %s', (path, condition) => { - const source = readFileSync( - `app/javascript/dashboard/routes/dashboard/settings/assignmentPolicy/${path}`, - 'utf8' +const SidebarGroupStub = { + name: 'SidebarGroup', + inheritAttrs: false, + props: ['name', 'children'], + template: '
', +}; + +const mocked = vi.hoisted(() => ({ + features: ['assignment_v2'], + dispatch: vi.fn(), + getInboxPolicy: vi.fn(), + getPolicies: vi.fn(), + push: vi.fn(), +})); + +const isFeatureEnabled = (_accountId, feature) => + mocked.features.includes(feature); +const store = { + dispatch: (...args) => mocked.dispatch(...args), + getters: { + 'agents/getAgents': [], + 'accounts/isFeatureEnabledonAccount': isFeatureEnabled, + getUISettings: {}, + }, +}; + +vi.mock('vue-router', async () => ({ + ...(await vi.importActual('vue-router')), + useRoute: () => ({ params: { accountId: '1' } }), + useRouter: () => ({ push: mocked.push }), +})); + +vi.mock('vuex', async () => ({ + ...(await vi.importActual('vuex')), + useStore: () => store, +})); + +vi.mock('dashboard/composables/useUISettings', async () => { + const { ref } = await vi.importActual('vue'); + return { + useUISettings: () => ({ + uiSettings: ref({ sidebar_width: 200 }), + updateUISettings: vi.fn(), + }), + }; +}); + +vi.mock('dashboard/composables/store', async () => { + const { computed } = await vi.importActual('vue'); + const emptyListGetters = new Set([ + 'inboxes/getInboxes', + 'labels/getLabelsOnSidebar', + 'teams/getMyTeams', + 'customViews/getContactCustomViews', + 'customViews/getConversationCustomViews', + ]); + const unreadCountGetters = new Set([ + 'conversationUnreadCounts/getInboxUnreadCount', + 'conversationUnreadCounts/getLabelUnreadCount', + 'conversationUnreadCounts/getTeamUnreadCount', + ]); + + return { + ...(await vi.importActual('dashboard/composables/store')), + useMapGetter: key => + computed(() => { + if (key === 'accounts/isFeatureEnabledonAccount') { + return isFeatureEnabled; + } + if (key === 'accounts/getAccount') return () => ({ id: 1 }); + if (key === 'getCurrentAccountId') return 1; + if (emptyListGetters.has(key)) return []; + if (unreadCountGetters.has(key)) return () => 0; + return false; + }), + }; +}); + +vi.mock('dashboard/api/assignmentPolicies', () => ({ + default: { + getInboxPolicy: (...args) => mocked.getInboxPolicy(...args), + get: (...args) => mocked.getPolicies(...args), + }, +})); + +describe('self-hosted advanced assignment', () => { + beforeEach(() => { + mocked.features = ['assignment_v2']; + mocked.dispatch.mockResolvedValue({ data: { payload: [] } }); + mocked.getInboxPolicy.mockResolvedValue({ data: null }); + mocked.getPolicies.mockResolvedValue({ data: [] }); + }); + + it('keeps balanced assignment available without a premium label', () => { + const wrapper = shallowMount(AgentAssignmentPolicyForm, { + props: { mode: 'CREATE' }, + global: { + stubs: { + WithLabel: { template: '
' }, + }, + }, + }); + const balancedOption = wrapper + .findAllComponents(RadioCard) + .find(option => option.props('id') === 'balanced'); + + expect(balancedOption.props()).toMatchObject({ + disabled: false, + disabledLabel: '', + disabledMessage: '', + }); + }); + + it('shows agent capacity when assignment v2 is enabled', () => { + const wrapper = shallowMount(AssignmentPolicyIndex, { + global: { + stubs: { + SettingsLayout: { + template: '
', + }, + }, + }, + }); + const cards = wrapper.findAllComponents(AssignmentCard); + + expect(cards).toHaveLength(2); + expect(cards.map(card => card.props('title'))).toContain( + 'Agent capacity policy' ); + }); - expect(source).toContain('shouldShowPaywall'); - expect(source).toContain('showAdvancedAssignmentPaywall'); - expect(source).toContain(condition); + it('shows the Agent Assignment sidebar entry without the legacy flag', () => { + const wrapper = shallowMount(Sidebar, { + global: { + mocks: { $store: store }, + stubs: { RouterLink: true, SidebarGroup: SidebarGroupStub }, + }, + }); + const settings = wrapper + .findAllComponents(SidebarGroupStub) + .find(group => group.props('name') === 'Settings'); + + expect(settings.props('children')).toEqual( + expect.arrayContaining([ + expect.objectContaining({ name: 'Settings Agent Assignment' }), + ]) + ); + }); + + it.each([ + 'agent_capacity_policy_index', + 'agent_capacity_policy_create', + 'agent_capacity_policy_edit', + ])('allows the %s route with assignment v2', async routeName => { + const router = createRouter({ + history: createMemoryHistory(), + routes: assignmentPolicyRoutes.routes, + }); + + const params = { + accountId: 1, + ...(routeName === 'agent_capacity_policy_edit' ? { id: 1 } : {}), + }; + await router.push({ name: routeName, params }); + + expect(router.currentRoute.value).toMatchObject({ + name: routeName, + meta: { featureFlag: FEATURE_FLAGS.ASSIGNMENT_V2 }, + }); + }); + + it('shows policy controls instead of a Business plan upgrade', async () => { + const wrapper = shallowMount(CollaboratorsPage, { + props: { + inbox: { + id: 1, + enable_auto_assignment: true, + }, + }, + global: { + stubs: { + SettingsAccordion: { template: '
' }, + SettingsToggleSection: { + template: '
', + }, + NextButton: { + props: ['label'], + template: '', + }, + WootInput: true, + }, + }, + }); + + await flushPromises(); + + expect(wrapper.text()).toContain('Create new policy'); + expect(wrapper.text()).not.toContain('Upgrade to Business'); }); }); diff --git a/frontend/app/javascript/dashboard/routes/dashboard/settings/assignmentPolicy/assignmentPolicy.routes.js b/frontend/app/javascript/dashboard/routes/dashboard/settings/assignmentPolicy/assignmentPolicy.routes.js index 0c161eba..d0987167 100644 --- a/frontend/app/javascript/dashboard/routes/dashboard/settings/assignmentPolicy/assignmentPolicy.routes.js +++ b/frontend/app/javascript/dashboard/routes/dashboard/settings/assignmentPolicy/assignmentPolicy.routes.js @@ -62,7 +62,7 @@ export default { name: 'agent_capacity_policy_index', component: AgentCapacityIndex, meta: { - featureFlag: FEATURE_FLAGS.ADVANCED_ASSIGNMENT, + featureFlag: FEATURE_FLAGS.ASSIGNMENT_V2, permissions: ['administrator'], }, }, @@ -71,7 +71,7 @@ export default { name: 'agent_capacity_policy_create', component: AgentCapacityCreate, meta: { - featureFlag: FEATURE_FLAGS.ADVANCED_ASSIGNMENT, + featureFlag: FEATURE_FLAGS.ASSIGNMENT_V2, permissions: ['administrator'], }, }, @@ -80,7 +80,7 @@ export default { name: 'agent_capacity_policy_edit', component: AgentCapacityEdit, meta: { - featureFlag: FEATURE_FLAGS.ADVANCED_ASSIGNMENT, + featureFlag: FEATURE_FLAGS.ASSIGNMENT_V2, permissions: ['administrator'], }, }, diff --git a/frontend/app/javascript/dashboard/routes/dashboard/settings/assignmentPolicy/pages/components/AgentAssignmentPolicyForm.vue b/frontend/app/javascript/dashboard/routes/dashboard/settings/assignmentPolicy/pages/components/AgentAssignmentPolicyForm.vue index d0097f69..02923ae1 100644 --- a/frontend/app/javascript/dashboard/routes/dashboard/settings/assignmentPolicy/pages/components/AgentAssignmentPolicyForm.vue +++ b/frontend/app/javascript/dashboard/routes/dashboard/settings/assignmentPolicy/pages/components/AgentAssignmentPolicyForm.vue @@ -1,10 +1,6 @@