This commit is contained in:
2026-08-19 09:14:35 +08:00
parent c188482c24
commit 80cfca5056
36 changed files with 854 additions and 1044 deletions
@@ -9,24 +9,24 @@ import { useAuthStore } from '@/core/auth/auth'
import { ApiResponseError } from '@/shared/api/http'
import {
adjustInboundFeeDetail, createInboundFeeDetail, deleteInboundFeeDetail,
listInboundFeeDetails, listInboundFeeItemOptions, listInboundFeePartnerOptions,
listInboundFeeDetails, listInboundFeeItemOptions, listInboundFeeSupplierOptions,
reviewInboundFeeDetail, updateInboundFeeDetail, voidInboundFeeDetail,
} from '@/modules/unified-orders/api/inboundFeeDetails'
import InboundFeeDetailManager from './InboundFeeDetailManager.vue'
import type { InboundFeeDetail } from '@/modules/unified-orders/api/inboundFeeDetails'
vi.mock('@/modules/unified-orders/api/inboundFeeDetails', () => ({
listInboundFeeDetails: vi.fn(), listInboundFeeItemOptions: vi.fn(), listInboundFeePartnerOptions: vi.fn(),
listInboundFeeDetails: vi.fn(), listInboundFeeItemOptions: vi.fn(), listInboundFeeSupplierOptions: vi.fn(),
createInboundFeeDetail: vi.fn(), updateInboundFeeDetail: vi.fn(), deleteInboundFeeDetail: vi.fn(),
reviewInboundFeeDetail: vi.fn(), adjustInboundFeeDetail: vi.fn(), voidInboundFeeDetail: vi.fn(),
}))
const pending: InboundFeeDetail = {
id: '11', inboundId: '88', feeItemId: '31', sourceType: 'MANUAL', sourceFeeVersion: 1,
inboundCode: 'RK-88', direction: 'RECEIVABLE', partnerId: null, partnerName: null,
inboundCode: 'RK-88', direction: 'RECEIVABLE', supplierId: null, supplierName: null, supplierCategory: null,
businessDate: '2026-08-12', customerName: '测试客户', feeTypeNameSnapshot: '操作费',
unitPrice: 10, currency: 'CNY', amount: 100, remark: 'marker', reviewStatus: 'PENDING',
reviewReason: null, reviewerName: null, reviewedAt: null, publicationStatus: 'DRAFT', settlementIncluded: true,
reviewReason: null, reviewerName: null, reviewedAt: null, publicationStatus: 'DRAFT',
}
const approved: InboundFeeDetail = { ...pending, id: '12', sourceFeeVersion: 3, reviewStatus: 'APPROVED', publicationStatus: 'PUBLISHED' }
const source = readFileSync(join(process.cwd(), 'src/modules/unified-orders/components/InboundFeeDetailManager.vue'), 'utf8')
@@ -44,8 +44,8 @@ describe('InboundFeeDetailManager', () => {
beforeEach(() => {
vi.clearAllMocks(); document.body.innerHTML = ''
vi.mocked(listInboundFeeDetails).mockResolvedValue([pending, approved])
vi.mocked(listInboundFeeItemOptions).mockResolvedValue([{ id: '31', feeTypeName: '操作费', warehouseReceivable: true, warehousePayable: true }])
vi.mocked(listInboundFeePartnerOptions).mockResolvedValue([{ id: '41', partnerName: '测试往来单位' }])
vi.mocked(listInboundFeeItemOptions).mockResolvedValue([{ id: '31', feeTypeName: '操作费', feeCode: 'OPERATION', supplierCategory: 'TRUCKING', warehouseReceivable: true, warehousePayable: true }])
vi.mocked(listInboundFeeSupplierOptions).mockResolvedValue([{ id: '41', supplierName: '测试供应商', category: 'TRUCKING' }])
vi.mocked(createInboundFeeDetail).mockResolvedValue(pending)
vi.mocked(updateInboundFeeDetail).mockResolvedValue(pending)
vi.mocked(adjustInboundFeeDetail).mockResolvedValue({ ...approved, reviewStatus: 'PENDING' })
@@ -144,14 +144,14 @@ describe('InboundFeeDetailManager', () => {
expect(source).toContain('@keyframes fee-refresh-spin')
})
it('loads RECEIVABLE fee items without requesting counterparties', async () => {
it('loads RECEIVABLE fee items without requesting suppliers', async () => {
const wrapper = mountManager(['firstleg:inbound:fee-detail:view', 'firstleg:inbound:fee-detail:edit', 'firstleg:fee-item:view'])
await flushPromises()
await wrapper.findAll('button').find((button) => button.text().includes('新增费用'))?.trigger('click')
await flushPromises()
expect(listInboundFeeItemOptions).toHaveBeenCalledTimes(1)
expect(listInboundFeePartnerOptions).not.toHaveBeenCalled()
expect(document.body.textContent).toContain('缺少往来单位查看权限')
expect(listInboundFeeSupplierOptions).not.toHaveBeenCalled()
expect(document.body.textContent).toContain('缺少供应商查看权限')
expect(document.body.textContent).toContain('应收费用不受影响')
})
@@ -162,25 +162,41 @@ describe('InboundFeeDetailManager', () => {
expect(listInboundFeeItemOptions).not.toHaveBeenCalled()
})
it('keeps fee items usable when the independent counterparty request fails', async () => {
vi.mocked(listInboundFeeDetails).mockResolvedValue([{ ...pending, direction: 'PAYABLE', partnerId: '41', partnerName: '测试往来单位' }])
vi.mocked(listInboundFeePartnerOptions).mockRejectedValue(new ApiResponseError('往来单位不可用', 'A0403', 403, null, 'request-partner', 'trace-partner'))
it('keeps fee items usable but blocks payable save when the supplier request fails', async () => {
vi.mocked(listInboundFeeDetails).mockResolvedValue([{ ...pending, direction: 'PAYABLE', supplierId: '41', supplierName: '测试供应商', supplierCategory: 'TRUCKING' }])
vi.mocked(listInboundFeeSupplierOptions).mockRejectedValue(new ApiResponseError('供应商不可用', 'A0403', 403, null, 'request-supplier', 'trace-supplier'))
const wrapper = mountManager([
'firstleg:inbound:fee-detail:view', 'firstleg:inbound:fee-detail:edit',
'firstleg:fee-item:view', 'platform:counterparty:list',
'firstleg:fee-item:view', 'platform:supplier:list',
])
await flushPromises()
await wrapper.findAll('button').find((button) => button.text().includes('编辑'))?.trigger('click')
await flushPromises()
expect(listInboundFeeItemOptions).toHaveBeenCalledTimes(1)
expect(listInboundFeePartnerOptions).toHaveBeenCalledTimes(1)
expect(listInboundFeeSupplierOptions).toHaveBeenCalledWith('TRUCKING')
expect(document.body.textContent).toContain('当前账号无权访问该数据')
expect(document.body.textContent).toContain('A0403')
expect(document.body.textContent).toContain('request-partner')
expect(document.body.textContent).toContain('trace-partner')
expect(document.body.textContent).toContain('request-supplier')
expect(document.body.textContent).toContain('trace-supplier')
expect(document.querySelector('.fee-form .el-select')?.classList.contains('is-disabled')).toBe(false)
})
it('fails closed when a payable fee item has no supplier category', async () => {
vi.mocked(listInboundFeeItemOptions).mockResolvedValue([{ id: '31', feeTypeName: '未映射费用', feeCode: 'UNMAPPED', supplierCategory: null, warehouseReceivable: false, warehousePayable: true }])
const wrapper = mountManager([
'firstleg:inbound:fee-detail:view', 'firstleg:inbound:fee-detail:edit',
'firstleg:fee-item:view', 'platform:supplier:list',
])
await flushPromises()
await wrapper.findAll('button').find((button) => button.text().includes('新增费用'))?.trigger('click')
await flushPromises()
const state = (wrapper.vm.$ as unknown as { setupState: { save(): Promise<void>; form: { direction: string; feeItemId: string; supplierId: string | null; amount: number } } }).setupState
state.form.direction = 'PAYABLE'; state.form.feeItemId = '31'; state.form.supplierId = '41'; state.form.amount = 100; await flushPromises()
await state.save(); await flushPromises()
expect(createInboundFeeDetail).not.toHaveBeenCalled()
expect(document.body.textContent).toContain('该应付费用项目未配置供应商类别,已阻断保存')
})
it('refreshes fee rows and the order detail after every successful write', () => {
expect(source).toContain("async function finishAction(message: string) {\n await load(); emit('changed')")
expect(source).toMatch(/createInboundFeeDetail[\s\S]*updateInboundFeeDetail[\s\S]*adjustInboundFeeDetail[\s\S]*await finishAction\(/)