import ElementPlus from 'element-plus' import { createPinia, setActivePinia } from 'pinia' import { enableAutoUnmount, flushPromises, mount } from '@vue/test-utils' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { getCustomerDimensionConfig, updateCustomerDimensionConfig, updateCustomerDimensionDefaultCalculationMode, type CustomerDimensionConfig } from '@/modules/dimension-config' import { createCustomerChargeRule, listCustomerChargeRules, setCustomerChargeRuleDefault, type CustomerChargeRule, } from '@/modules/fba-pricing' import { useAccessPreviewStore } from '@/core/access/accessPreview' import { useAuthStore } from '@/core/auth/auth' import AppDialog from '@/shared/components/AppDialog.vue' import CustomerBillingRulesPanel from './CustomerBillingRulesPanel.vue' import { getPlatformTenantDimensionInitializationStatus } from '@/modules/iam' vi.mock('@/modules/dimension-config', () => ({ getCustomerDimensionConfig: vi.fn(), updateCustomerDimensionConfig: vi.fn(), updateCustomerDimensionDefaultCalculationMode: vi.fn() })) vi.mock('@/modules/fba-pricing', () => ({ createCustomerChargeRule: vi.fn(), listCustomerChargeRules: vi.fn(), retireCustomerChargeRule: vi.fn(), setCustomerChargeRuleDefault: vi.fn(), })) vi.mock('@/modules/iam', () => ({ getPlatformTenantDimensionInitializationStatus: vi.fn() })) const boxes = [ ['SMALL', '小箱', 30, 20, 20, '标准小箱'], ['MEDIUM', '中箱', 40, 30, 30, '标准中箱'], ['LARGE', '大箱', 50, 40, 40, '标准大箱'], ['EXTRA_LARGE', '特大箱', 60, 50, 50, '特大箱'], ] as const const config: CustomerDimensionConfig = { tenantId: '8', defaultCalculationMode: 'ADD', rules: boxes.map(([boxType, boxTypeName, innerLengthCm, innerWidthCm, innerHeightCm, remark], index) => ({ id: String(100 + index), boxType, boxTypeName, innerLengthCm, innerWidthCm, innerHeightCm, edgeAdditionCm: 2, outerLengthCm: innerLengthCm + 4, outerWidthCm: innerWidthCm + 4, outerHeightCm: innerHeightCm + 4, volumeCbm: Number(((innerLengthCm + 4) * (innerWidthCm + 4) * (innerHeightCm + 4) / 1_000_000).toFixed(3)), remark, version: 3, createdAt: '2026-08-07T10:00:00', updatedAt: '2026-08-07T10:20:00', })), } const chargeRule: CustomerChargeRule = { id: '201', tenantId: '8', ruleCode: 'RULE-201', ruleVersion: 1, customerId: '8', customerName: '柏悦物流', businessType: 'FBA', chargeMode: 'CBM', taxMode: 'TAX_INCLUDED', conversionFactor: 167, lengthFactor: 1, widthFactor: 1, heightFactor: 1, defaultRule: false, roundingScale: 2, roundingMode: 'HALF_UP', currency: 'CNY', effectiveFrom: '2026-09-02T10:00:00', effectiveTo: null, status: 'ACTIVE', remark: null, } enableAutoUnmount(afterEach) describe('CustomerBillingRulesPanel', () => { beforeEach(() => { vi.clearAllMocks() document.body.innerHTML = '' const pinia = createPinia(); setActivePinia(pinia) const auth = useAuthStore(); auth.token = 'token'; auth.permissions = [ 'firstleg:dimension-config:view', 'firstleg:dimension-config:edit', 'firstleg:fba-pricing:rule:view', 'firstleg:fba-pricing:rule:edit', ] useAccessPreviewStore().initializeAfterLogin() vi.mocked(getCustomerDimensionConfig).mockResolvedValue(config) vi.mocked(updateCustomerDimensionConfig).mockResolvedValue(config) vi.mocked(updateCustomerDimensionDefaultCalculationMode).mockResolvedValue(config) vi.mocked(listCustomerChargeRules).mockResolvedValue([chargeRule]) vi.mocked(createCustomerChargeRule).mockResolvedValue(chargeRule) vi.mocked(getPlatformTenantDimensionInitializationStatus).mockResolvedValue({ tenantId: '8', status: 'PENDING', attemptCount: 0, nextRetryAt: null, lastErrorCode: null, lastErrorMessage: null, updatedAt: null, }) }) it('renders the four fixed carton dimension rows and calculated values', async () => { mount(CustomerBillingRulesPanel, { attachTo: document.body, props: { customerId: '8', customerName: '柏悦物流' }, global: { plugins: [ElementPlus] } }) await flushPromises() expect(getCustomerDimensionConfig).toHaveBeenCalledWith('8') expect(document.body.textContent).toContain('按箱规尺寸计算') expect(document.body.textContent).toContain('长、宽、高各加 2X = 外径尺寸') expect(document.body.textContent).toContain('小箱') expect(document.body.textContent).toContain('特大箱') expect(document.body.textContent).toContain('34 × 24 × 24') expect(document.body.textContent).toContain('0.020') expect(document.body.textContent).toContain('标准中箱') }) it('hides permission codes in the no-access state', async () => { const auth = useAuthStore() auth.permissions = [] useAccessPreviewStore().initializeAfterLogin() mount(CustomerBillingRulesPanel, { attachTo: document.body, props: { customerId: '8', customerName: '柏悦物流' }, global: { plugins: [ElementPlus] } }) await flushPromises() expect(document.body.textContent).toContain('暂无尺寸计算规则查看权限') expect(document.body.textContent).toContain('请联系管理员申请尺寸计算规则查看权限') expect(document.body.textContent).not.toContain('firstleg:dimension-config:view') expect(getCustomerDimensionConfig).not.toHaveBeenCalled() }) it('does not request billing rules before customer defaults are ready', async () => { mount(CustomerBillingRulesPanel, { attachTo: document.body, props: { customerId: '8', customerName: '柏悦物流', dimensionInitializationStatus: 'PENDING', }, global: { plugins: [ElementPlus] }, }) await flushPromises() expect(document.body.textContent).toContain('客户计费规则正在初始化') expect(document.body.textContent).not.toContain('箱规尺寸规则加载失败') expect(getCustomerDimensionConfig).not.toHaveBeenCalled() expect(listCustomerChargeRules).not.toHaveBeenCalled() }) it('loads billing rules automatically when asynchronous initialization becomes ready', async () => { vi.mocked(getPlatformTenantDimensionInitializationStatus).mockResolvedValueOnce({ tenantId: '8', status: 'READY', attemptCount: 1, nextRetryAt: null, lastErrorCode: null, lastErrorMessage: null, updatedAt: '2026-09-10T20:04:06', }) mount(CustomerBillingRulesPanel, { attachTo: document.body, props: { customerId: '8', customerName: '柏悦物流', dimensionInitializationStatus: 'PENDING' }, global: { plugins: [ElementPlus] }, }) await flushPromises() expect(getPlatformTenantDimensionInitializationStatus).toHaveBeenCalledWith('8') expect(getCustomerDimensionConfig).toHaveBeenCalledWith('8') expect(listCustomerChargeRules).toHaveBeenCalledWith('8') expect(document.body.textContent).not.toContain('箱规尺寸规则加载失败') }) it('submits all four rows in one update request', async () => { const wrapper = mount(CustomerBillingRulesPanel, { attachTo: document.body, props: { customerId: '8', customerName: '柏悦物流' }, global: { plugins: [ElementPlus] } }) await flushPromises() ;[...document.querySelectorAll('button')].find((button) => button.textContent?.trim() === '编辑规则')?.click() await flushPromises() const saveButton = [...document.querySelectorAll('button')].find((button) => button.textContent?.trim() === '保存规则') saveButton?.click(); await flushPromises() expect(updateCustomerDimensionConfig).toHaveBeenCalledWith('8', { rules: expect.arrayContaining([expect.objectContaining({ boxType: 'SMALL', version: 3 }), expect.objectContaining({ boxType: 'EXTRA_LARGE', version: 3 })]) }) const submittedRules = vi.mocked(updateCustomerDimensionConfig).mock.calls[0][1].rules expect(submittedRules).toHaveLength(4) expect(Object.keys(submittedRules[0]).sort()).toEqual([ 'boxType', 'edgeAdditionCm', 'innerHeightCm', 'innerLengthCm', 'innerWidthCm', 'remark', 'version', ]) expect(wrapper.findComponent(AppDialog).props('modelValue')).toBe(false) }) it('keeps the editor open when the whole-set save fails', async () => { vi.mocked(updateCustomerDimensionConfig).mockRejectedValueOnce(new Error('规则版本冲突')) mount(CustomerBillingRulesPanel, { attachTo: document.body, props: { customerId: '8', customerName: '柏悦物流' }, global: { plugins: [ElementPlus] } }) await flushPromises() ;[...document.querySelectorAll('button')].find((button) => button.textContent?.trim() === '编辑规则')?.click(); await flushPromises() ;[...document.querySelectorAll('button')].find((button) => button.textContent?.trim() === '保存规则')?.click() await flushPromises() expect(document.body.textContent).toContain('编辑箱规尺寸') }) it('saves the coefficient set and can use it as the customer default calculation mode', async () => { vi.mocked(listCustomerChargeRules) .mockResolvedValueOnce([chargeRule]) .mockResolvedValueOnce([{ ...chargeRule, defaultRule: true }]) vi.mocked(updateCustomerDimensionDefaultCalculationMode).mockResolvedValue({ ...config, defaultCalculationMode: 'MULTIPLY', }) const wrapper = mount(CustomerBillingRulesPanel, { attachTo: document.body, props: { customerId: '8', customerName: '柏悦物流' }, global: { plugins: [ElementPlus] } }) await flushPromises() ;[...document.querySelectorAll('button')].find((button) => button.textContent?.trim() === '编辑规则')?.click() await flushPromises() expect(document.body.textContent).toContain('箱规按权重系数计算') ;[...document.querySelectorAll('button')].find((button) => button.textContent?.trim() === '保存系数规则')?.click() await flushPromises() expect(createCustomerChargeRule).toHaveBeenCalledWith('8', expect.objectContaining({ defaultRule: false, remark: expect.stringMatching(/SMALL:30,20,20;系数:\d+(?:\.\d+)?/), })) expect(setCustomerChargeRuleDefault).toHaveBeenCalledWith('8', '201') expect(wrapper.findComponent(AppDialog).props('modelValue')).toBe(false) const defaultRuleCheckboxes = [...document.querySelectorAll('input[type="checkbox"]')] defaultRuleCheckboxes[1]?.click() await flushPromises() expect(updateCustomerDimensionDefaultCalculationMode).toHaveBeenCalledWith('8', 'MULTIPLY') }) })