Files
admin-web/src/modules/customers/components/CustomerBillingRulesPanel.test.ts
T

243 lines
14 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import ElementPlus, { ElDialog, ElMessageBox } 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: '柏悦物流', customerCode: 'C0008' }, global: { plugins: [ElementPlus] } })
await flushPromises()
;[...document.querySelectorAll<HTMLButtonElement>('button')].find((button) => button.textContent?.trim() === '编辑规则')?.click()
await flushPromises()
expect(wrapper.findComponent(AppDialog).props('title')).toBe('编辑箱规尺寸 / 计算规则 · C0008')
const length = document.querySelector<HTMLInputElement>('.editor-table input[placeholder="请输入内径长"]')!
length.value = '31'
length.dispatchEvent(new Event('input', { bubbles: true }))
length.dispatchEvent(new Event('change', { bubbles: true }))
await flushPromises()
document.querySelector<HTMLButtonElement>('button[aria-label="展开全屏"]')!.click()
await flushPromises()
expect(document.querySelector('.customer-density-dialog.is-fullscreen')).not.toBeNull()
expect(length.value).toBe('31.000')
const saveButton = [...document.querySelectorAll<HTMLButtonElement>('button')].find((button) => button.textContent?.trim() === '保存规则')
saveButton?.click(); await flushPromises()
expect(updateCustomerDimensionConfig).toHaveBeenCalledWith('8', { rules: expect.arrayContaining([expect.objectContaining({ boxType: 'SMALL', innerLengthCm: 31, 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<HTMLButtonElement>('button')].find((button) => button.textContent?.trim() === '编辑规则')?.click(); await flushPromises()
;[...document.querySelectorAll<HTMLButtonElement>('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<HTMLButtonElement>('button')].find((button) => button.textContent?.trim() === '编辑规则')?.click()
await flushPromises()
expect(document.body.textContent).toContain('箱规按权重系数计算')
;[...document.querySelectorAll<HTMLButtonElement>('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<HTMLInputElement>('input[type="checkbox"]')]
defaultRuleCheckboxes[1]?.click()
await flushPromises()
expect(updateCustomerDimensionDefaultCalculationMode).toHaveBeenCalledWith('8', 'MULTIPLY')
})
it('keeps dimension changes when header abandonment is declined', async () => {
const wrapper = mount(CustomerBillingRulesPanel, { attachTo: document.body, props: { customerId: '8', customerName: '柏悦物流', customerCode: 'C0008' }, global: { plugins: [ElementPlus] } })
await flushPromises()
;[...document.querySelectorAll<HTMLButtonElement>('button')].find(button => button.textContent?.trim() === '编辑规则')!.click()
await flushPromises()
const input = document.querySelector<HTMLInputElement>('.editor-table input[placeholder="请输入内径长"]')!
input.value = '32'
input.dispatchEvent(new Event('input', { bubbles: true }))
input.dispatchEvent(new Event('change', { bubbles: true }))
await flushPromises()
const confirmation = vi.spyOn(ElMessageBox, 'confirm').mockRejectedValueOnce('cancel')
document.querySelector<HTMLButtonElement>('.customer-density-dialog .el-dialog__headerbtn')!.click()
await flushPromises()
expect(confirmation).toHaveBeenCalledWith(expect.stringContaining('尚未保存'), '放弃编辑 · C0008', expect.objectContaining({ customClass: 'admin-density-confirm' }))
expect(wrapper.findComponent(AppDialog).props('modelValue')).toBe(true)
expect(input.value).toBe('32.000')
confirmation.mockRestore()
})
it('locks header and footer closing while coefficient rules are saving', async () => {
let finish!: (rule: CustomerChargeRule) => void
vi.mocked(createCustomerChargeRule).mockReturnValueOnce(new Promise(resolve => { finish = resolve }))
const wrapper = mount(CustomerBillingRulesPanel, { attachTo: document.body, props: { customerId: '8', customerName: '柏悦物流' }, global: { plugins: [ElementPlus] } })
await flushPromises()
;[...document.querySelectorAll<HTMLButtonElement>('button')].find(button => button.textContent?.trim() === '编辑规则')!.click()
await flushPromises()
;[...document.querySelectorAll<HTMLButtonElement>('button')].find(button => button.textContent?.trim() === '保存系数规则')!.click()
await flushPromises()
expect(wrapper.findComponent(AppDialog).props('confirmLoading')).toBe(true)
expect(wrapper.findComponent(ElDialog).props('showClose')).toBe(false)
expect(wrapper.findComponent(ElDialog).props('closeOnPressEscape')).toBe(false)
const done = vi.fn()
await wrapper.findComponent(AppDialog).props('beforeClose')!(done)
expect(done).not.toHaveBeenCalled()
finish(chargeRule)
await flushPromises()
expect(wrapper.findComponent(AppDialog).props('modelValue')).toBe(false)
})
})