259 lines
14 KiB
TypeScript
259 lines
14 KiB
TypeScript
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.querySelector('.formula-band')).toBeNull()
|
||
const help = document.querySelector<HTMLButtonElement>('button[aria-label="查看箱规计算说明"]')
|
||
expect(help).not.toBeNull()
|
||
help!.focus()
|
||
await vi.waitFor(() => 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('shows weighted volume and volume weight with the coefficient applied to all three dimensions', async () => {
|
||
vi.mocked(listCustomerChargeRules).mockResolvedValue([{ ...chargeRule, defaultRule: true, lengthFactor: 1.25 }])
|
||
const wrapper = mount(CustomerBillingRulesPanel, { attachTo: document.body, props: { customerId: '8', customerName: '柏悦物流' }, global: { plugins: [ElementPlus] } })
|
||
await flushPromises()
|
||
|
||
const chargeTable = wrapper.findAll('.el-table')[1]!
|
||
const largeRow = chargeTable.findAll('.el-table__body tbody tr')[3]!
|
||
const values = largeRow.findAll('td').map(cell => cell.text())
|
||
expect(values[4]).toBe('0.293')
|
||
expect(values[6]).toBe('48.93')
|
||
})
|
||
|
||
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 admin-workbench' }))
|
||
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)
|
||
})
|
||
|
||
})
|