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 { ApiResponseError } from '@/shared/api/http' import { useAccessPreviewStore } from '@/core/access/accessPreview' import { useAuthStore } from '@/core/auth/auth' import { adjustPublishedInboundFee, getLatestInboundFee, } from './api' import type { InboundFeeSnapshot } from './types' import InboundFeePanel from './InboundFeePanel.vue' vi.mock('./api', async () => { const actual = await vi.importActual('./api') return { ...actual, getLatestInboundFee: vi.fn(), adjustPublishedInboundFee: vi.fn(), } }) const snapshot: InboundFeeSnapshot = { feeId: '9007199254740993', feeNo: 'FBAF-88-2', feeVersion: 2, feeStatus: 'SENT', inboundId: '88', inboundCode: 'RK202608040088', chargeRuleId: '101', ruleCode: 'RULE-FBA-KG', chargeRuleVersion: 3, carrierRateId: '202', rateCode: 'RATE-LAX', carrierRateVersion: 5, customerId: '303', customerName: '测试客户', originWarehouseId: '404', originWarehouseName: '义乌仓', businessType: 'FBA_SEA', channelCode: 'SEA-TAX', carrierId: '505', carrierName: '测试船司', destinationCode: 'LAX9', taxMode: 'TAX_INCLUDED', chargeMode: 'KG', conversionFactor: 167, roundingScale: 2, roundingMode: 'HALF_UP', actualWeightKg: 100, actualVolumeCbm: 0.8, convertedQuantity: 133.6, chargeableQuantity: 133.6, chargeUnit: 'KG', unitPrice: 8.5, subtotalAmount: 1135.6, additionalAmount: 100, totalAmount: 1235.6, currency: 'CNY', rateEffectiveFrom: '2026-08-01T00:00:00', rateEffectiveTo: null, calculatedAt: '2026-08-04T10:00:00', confirmedAt: '2026-08-04T10:01:00', sentAt: '2026-08-04T10:02:00', nextProcessStatus: 'WAIT_SPLIT', } enableAutoUnmount(afterEach) function setupPermissions(permissions: string[]) { const pinia = createPinia() setActivePinia(pinia) const auth = useAuthStore() auth.token = 'token' auth.permissions = permissions useAccessPreviewStore().initializeAfterLogin() return pinia } function mountPanel(permissions = ['firstleg:fba-pricing:view', 'firstleg:fba-pricing:adjust']) { const pinia = setupPermissions(permissions) return mount(InboundFeePanel, { attachTo: document.body, props: { customerId: '10001', inboundId: '88' }, global: { plugins: [pinia, ElementPlus] }, }) } describe('InboundFeePanel', () => { beforeEach(() => { vi.clearAllMocks() document.body.innerHTML = '' vi.mocked(getLatestInboundFee).mockResolvedValue(snapshot) vi.mocked(adjustPublishedInboundFee).mockResolvedValue({ ...snapshot, feeVersion: 3, feeNo: 'FBAF-88-3' }) }) it('shows the immutable calculation basis, versions, amount, and timestamps', async () => { mountPanel() await flushPromises() expect(getLatestInboundFee).toHaveBeenCalledWith('10001', '88') expect(document.body.textContent).not.toContain('租户') expect(document.body.textContent).toContain('对客费用总额 · CNY') expect(document.body.textContent).toContain('¥1,235.60') expect(document.body.textContent).toContain('基础运费小计 ¥1,135.60') expect(document.body.textContent).toContain('附加费 ¥100.00') expect(document.body.textContent).toContain('133.6 KG') expect(document.body.textContent).toContain('按重量计费') expect(document.body.textContent).toContain('FBA 海运') expect(document.body.textContent).toContain('¥8.50 / KG') expect(document.body.textContent).toContain('RULE-FBA-KG · V3') expect(document.body.textContent).toContain('RATE-LAX · V5') expect(document.body.textContent).toContain('发布给客户') expect(document.body.textContent).not.toContain('重新试算') expect(document.body.textContent).not.toContain('发送费用') }) it('follows the admin detail interaction pattern without list-only scaffolding', async () => { const wrapper = mountPanel() await flushPromises() expect(wrapper.get('button[aria-label="刷新费用快照"]').classes()).toContain('is-circle') expect(wrapper.get('.version-status .el-tag').text()).toBe('已发布') expect(wrapper.find('.query-toolbar').exists()).toBe(false) expect(wrapper.find('.data-table').exists()).toBe(false) expect(wrapper.find('.surface-panel').exists()).toBe(false) }) it('keeps the legacy integration prop compatible while the page layer uses customer context', async () => { const pinia = setupPermissions(['firstleg:fba-pricing:view']) mount(InboundFeePanel, { props: { tenantId: 'legacy-10001', inboundId: '88' }, global: { plugins: [pinia, ElementPlus] }, }) await flushPromises() expect(getLatestInboundFee).toHaveBeenCalledWith('legacy-10001', '88') expect(document.body.textContent).not.toContain('租户') }) it('keeps the prototype-aligned detail order and renders long values with pending timestamps', async () => { const longRuleCode = `RULE-${'LONG-'.repeat(20)}` const longChannelCode = `CHANNEL-${'LONG-'.repeat(20)}` vi.mocked(getLatestInboundFee).mockResolvedValue({ ...snapshot, ruleCode: longRuleCode, channelCode: longChannelCode, confirmedAt: null, sentAt: null, }) const wrapper = mountPanel() await flushPromises() const content = wrapper.find('.fee-content').text() expect(wrapper.findAll('.fee-summary > div > span').map((node) => node.text())).toEqual([ '对客费用总额 · CNY', '最终计费量', '快照版本', ]) expect(wrapper.findAll('.calculation-flow > div > span').map((node) => node.text())).toEqual([ '实际重量', '实际体积', '折算量', '取大后的计费量', ]) expect(wrapper.findAll('.fee-detail-grid h4').map((node) => node.text())).toEqual([ '客户计费规则', '渠道价格', ]) expect(content).toContain(longRuleCode) expect(content).toContain(longChannelCode) const pendingTimelineItems = wrapper.findAll('.fee-timeline li.pending') expect(pendingTimelineItems).toHaveLength(2) expect(pendingTimelineItems.map((item) => item.find('strong').text())).toEqual(['—', '—']) }) it('does not request or expose fees without the view permission', async () => { mountPanel([]) await flushPromises() expect(getLatestInboundFee).not.toHaveBeenCalled() expect(document.body.textContent).toContain('没有查看入库费用的权限') }) it('treats A0404 as a neutral not-yet-generated state', async () => { vi.mocked(getLatestInboundFee).mockRejectedValue(new ApiResponseError('费用快照不存在', 'A0404')) mountPanel(['firstleg:fba-pricing:view']) await flushPromises() expect(document.body.textContent).toContain('尚未生成费用快照') expect(document.body.textContent).toContain('测量审核完成后') expect(document.querySelector('[role="alert"]')).toBeNull() }) it('shows adjustment only for authorized published inbound fees', async () => { mountPanel(['firstleg:fba-pricing:view']) await flushPromises() expect(document.querySelector('[data-testid="adjust-fee-button"]')).toBeNull() document.body.innerHTML = '' mountPanel() await flushPromises() expect(document.querySelector('[data-testid="adjust-fee-button"]')).not.toBeNull() }) it('allows adjustment for a calculated fee waiting for split', async () => { vi.mocked(getLatestInboundFee).mockResolvedValue({ ...snapshot, feeStatus: 'CALCULATED' }) const wrapper = mountPanel() await flushPromises() expect(wrapper.find('[data-testid="adjust-fee-button"]').exists()).toBe(true) }) it('submits a reasoned adjustment and emits the refreshed snapshot', async () => { const wrapper = mountPanel() await flushPromises() ;(document.querySelector('[data-testid="adjust-fee-button"]') as HTMLButtonElement).click() await flushPromises() const numbers = [...document.querySelectorAll('.adjustment-form input')] numbers[0]!.value = '140' numbers[0]!.dispatchEvent(new Event('input', { bubbles: true })) numbers[0]!.dispatchEvent(new Event('change', { bubbles: true })) numbers[1]!.value = '8.2' numbers[1]!.dispatchEvent(new Event('input', { bubbles: true })) numbers[1]!.dispatchEvent(new Event('change', { bubbles: true })) numbers[2]!.value = '80' numbers[2]!.dispatchEvent(new Event('input', { bubbles: true })) numbers[2]!.dispatchEvent(new Event('change', { bubbles: true })) const reason = document.querySelector('.adjustment-form textarea')! reason.value = '补充已确认的附加费用' reason.dispatchEvent(new Event('input', { bubbles: true })) reason.dispatchEvent(new Event('blur', { bubbles: true })) const confirm = [...document.querySelectorAll('button')] .find((button) => button.textContent?.includes('确认调整并生成新版本')) confirm?.click() await flushPromises() expect(adjustPublishedInboundFee).toHaveBeenCalledWith( '10001', '88', { expectedFeeVersion: 2, chargeableQuantity: 140, unitPrice: 8.2, additionalAmount: 80, reason: '补充已确认的附加费用' }, expect.stringMatching(/^fee-adjust-/), ) expect(wrapper.emitted('changed')?.[0]?.[0]).toMatchObject({ feeVersion: 3 }) }) it('refreshes the latest snapshot after a conflict while retaining the dialog', async () => { vi.mocked(adjustPublishedInboundFee).mockRejectedValue( new ApiResponseError('费用已被其他操作员修改,请刷新后重试', 'A0409', 409), ) mountPanel() await flushPromises() ;(document.querySelector('[data-testid="adjust-fee-button"]') as HTMLButtonElement).click() await flushPromises() const numbers = [...document.querySelectorAll('.adjustment-form input')] numbers[0]!.value = '140' numbers[0]!.dispatchEvent(new Event('input', { bubbles: true })) numbers[1]!.value = '8.2' numbers[1]!.dispatchEvent(new Event('input', { bubbles: true })) numbers[2]!.value = '80' numbers[2]!.dispatchEvent(new Event('input', { bubbles: true })) const reason = document.querySelector('.adjustment-form textarea')! reason.value = '并发场景价格修正' reason.dispatchEvent(new Event('input', { bubbles: true })) reason.dispatchEvent(new Event('blur', { bubbles: true })) ;[...document.querySelectorAll('button')] .find((button) => button.textContent?.includes('确认调整并生成新版本'))?.click() await flushPromises() expect(getLatestInboundFee).toHaveBeenCalledTimes(2) expect(document.body.textContent).toContain('系统已刷新最新快照') expect(document.body.textContent).toContain('当前输入仍保留') }) })