329 lines
23 KiB
TypeScript
329 lines
23 KiB
TypeScript
import ElementPlus, { ElDialog } from 'element-plus'
|
|
import { createPinia } from 'pinia'
|
|
import { enableAutoUnmount, flushPromises, mount, type VueWrapper } from '@vue/test-utils'
|
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
import { ApiResponseError } from '@/shared/api/http'
|
|
import OverseasFeeEditor from './OverseasFeeEditor.vue'
|
|
import AppDialog from '@/shared/components/AppDialog.vue'
|
|
import * as api from '../api/overseasFees'
|
|
import type { OverseasFee } from '../api/overseasFees'
|
|
import BusinessOrderFees from './BusinessOrderFees.vue'
|
|
import { adjustStorageRentFee } from '../api/storageRent'
|
|
vi.mock('../api/storageRent', () => ({ adjustStorageRentFee: vi.fn() }))
|
|
|
|
|
|
const can = vi.hoisted(() => vi.fn((_permission: string) => true))
|
|
vi.mock('@/core/access/accessPreview', () => ({ useAccessPreviewStore: () => ({ can }) }))
|
|
vi.mock('../api/overseasFees', () => ({
|
|
actOnOverseasFee: vi.fn(), getOverseasFee: vi.fn(), listOverseasFeeCustomers: vi.fn(), listOverseasFeeItems: vi.fn(),
|
|
listOverseasFeeRetries: vi.fn(),
|
|
listOverseasFees: vi.fn(), listBusinessOverseasFees: vi.fn(), listBusinessFeeOptions: vi.fn(), createOverseasFee: vi.fn(),
|
|
}))
|
|
const fee: OverseasFee = {
|
|
direction: 'RECEIVABLE',
|
|
id: '9223372036854775001', sourceFeeId: '9223372036854775001', sourceVersion: 3,
|
|
customerId: '9223372036854775002', customerName: '测试客户', sourceType: 'OVERSEAS_WAREHOUSE_FEE', businessType: 'WAREHOUSE_TRANSFER',
|
|
businessId: '9223372036854775003', businessNo: 'WT202610220001', orderId: null, warehouseId: '9223372036854775004',
|
|
feeItemCode: 'HANDLING', feeItemName: '操作费', businessDate: '2026-10-22', currency: 'CNY', amount: 70,
|
|
originalCurrency: 'USD', originalAmount: 10, chargeableQuantity: null, quantityUnit: null, unitPrice: null,
|
|
periodStart: null, periodEnd: null, reviewStatus: 'PENDING', sourceStatus: 'ACTIVE', publicationStatus: 'UNPUBLISHED',
|
|
collectionMode: 'WALLET', walletStatus: 'FROZEN', walletErrorMessage: null, reviewReason: null, reviewerId: null, reviewedAt: null,
|
|
createdAt: '2026-10-22T10:00:00', updatedAt: '', allowedActions: ['APPROVE', 'REJECT', 'CANCEL'],
|
|
}
|
|
const business = { businessId: fee.businessId!, businessType: fee.businessType, businessNo: fee.businessNo, customerId: fee.customerId, customerName: fee.customerName, warehouseId: fee.warehouseId, orderId: fee.orderId }
|
|
function deferred<T>() {
|
|
let resolve!: (value: T) => void, reject!: (reason: unknown) => void
|
|
const promise = new Promise<T>((yes, no) => { resolve = yes; reject = no })
|
|
return { promise, resolve, reject }
|
|
}
|
|
async function mountPage() {
|
|
const wrapper = mount(BusinessOrderFees, { props: { business }, attachTo: document.body, global: { plugins: [ElementPlus, createPinia()] } })
|
|
await flushPromises()
|
|
return wrapper
|
|
}
|
|
async function click(wrapper: VueWrapper, label: string, selector = 'button') {
|
|
const button = wrapper.findAll(selector).find(node => node.text().trim() === label)
|
|
expect(button, label).toBeDefined()
|
|
await button!.trigger('click'); await flushPromises()
|
|
}
|
|
function activeDialog(wrapper: VueWrapper) {
|
|
const dialog = wrapper.findAllComponents(AppDialog).find(component => component.props('modelValue'))
|
|
expect(dialog).toBeDefined()
|
|
return dialog!.getComponent({ name: 'ElDialogContent' })
|
|
}
|
|
enableAutoUnmount(afterEach)
|
|
beforeEach(() => {
|
|
vi.resetAllMocks(); can.mockReturnValue(true)
|
|
vi.mocked(api.listOverseasFeeRetries).mockResolvedValue([])
|
|
vi.stubGlobal('ResizeObserver', class { observe() {} unobserve() {} disconnect() {} })
|
|
vi.mocked(api.listOverseasFeeCustomers).mockResolvedValue([{ customerId: fee.customerId, customerName: fee.customerName }])
|
|
vi.mocked(api.listOverseasFeeItems).mockResolvedValue([{ feeCode: 'HANDLING', feeTypeName: '操作费', defaultPrice: 20 }])
|
|
vi.mocked(api.listBusinessOverseasFees).mockResolvedValue({ records: [fee], total: 1, page: 1, size: 20, pages: 1 })
|
|
vi.mocked(api.getOverseasFee).mockResolvedValue(fee)
|
|
vi.mocked(api.actOnOverseasFee).mockResolvedValue(fee)
|
|
vi.mocked(api.createOverseasFee).mockResolvedValue(fee)
|
|
vi.mocked(api.listBusinessFeeOptions).mockResolvedValue([{ businessId: fee.businessId!, businessType: fee.businessType, businessNo: fee.businessNo,
|
|
customerId: fee.customerId, customerName: fee.customerName, warehouseId: fee.warehouseId, orderId: null }])
|
|
})
|
|
afterEach(() => { vi.restoreAllMocks(); vi.unstubAllGlobals(); document.body.innerHTML = '' })
|
|
|
|
describe('单据费用操作', () => {
|
|
it.each(['RECEIVABLE', 'PAYABLE'] as const)('hides an empty %s block and reveals it when fees are added', async direction => {
|
|
vi.mocked(api.listBusinessOverseasFees).mockResolvedValue({ records: [], total: 0, page: 1, size: 20, pages: 0 })
|
|
const wrapper = mount(BusinessOrderFees, { props: { business, direction, hideWhenEmpty: true, canCreate: false, heading: direction === 'RECEIVABLE' ? '应收明细' : '应付明细' }, attachTo: document.body, global: { plugins: [ElementPlus, createPinia()] } })
|
|
await flushPromises()
|
|
expect(wrapper.isVisible()).toBe(false)
|
|
vi.mocked(api.listBusinessOverseasFees).mockResolvedValue({ records: [{ ...fee, direction }], total: 1, page: 1, size: 20, pages: 1 })
|
|
await wrapper.vm.refresh(); await flushPromises()
|
|
expect(wrapper.isVisible()).toBe(true)
|
|
expect(wrapper.get('h3').text()).toBe(direction === 'RECEIVABLE' ? '应收明细' : '应付明细')
|
|
expect(wrapper.text()).toContain('操作费')
|
|
expect(wrapper.findAll('button').some(button => button.text().trim() === '新增费用')).toBe(false)
|
|
vi.mocked(api.listBusinessOverseasFees).mockResolvedValue({ records: [], total: 0, page: 1, size: 20, pages: 0 })
|
|
await wrapper.vm.refresh(); await flushPromises()
|
|
expect(wrapper.isVisible()).toBe(false)
|
|
})
|
|
it('queries the exact document and creates without a second order selector or catalog default amount', async () => {
|
|
const wrapper = await mountPage()
|
|
expect(api.listBusinessOverseasFees).toHaveBeenCalledWith([business], { page: 1, size: 20 }, 'RECEIVABLE')
|
|
await click(wrapper, '新增费用')
|
|
expect(api.listBusinessFeeOptions).not.toHaveBeenCalled()
|
|
const dialog = activeDialog(wrapper)
|
|
expect(dialog.text()).not.toContain('业务类型')
|
|
expect(dialog.text()).not.toContain('业务订单')
|
|
expect(dialog.findComponent({ name: 'ElDatePicker' }).exists()).toBe(false)
|
|
const selects = dialog.findAllComponents({ name: 'ElSelect' })
|
|
selects[1]!.vm.$emit('update:modelValue', 'CNY')
|
|
dialog.getComponent({ name: 'ElInputNumber' }).vm.$emit('update:modelValue', 12.34)
|
|
selects[0]!.vm.$emit('update:modelValue', 'HANDLING')
|
|
selects[0]!.vm.$emit('change', 'HANDLING')
|
|
await flushPromises()
|
|
expect(dialog.getComponent({ name: 'ElInputNumber' }).props('modelValue')).toBe(12.34)
|
|
expect(dialog.text()).toContain('USD 10.00 · 待审核')
|
|
await click(dialog, '保存')
|
|
expect(api.createOverseasFee).not.toHaveBeenCalled()
|
|
dialog.findAllComponents({ name: 'ElInput' }).find(input => input.props('type') === 'textarea')!.vm.$emit('update:modelValue', '补收本次额外操作费用')
|
|
await flushPromises()
|
|
await click(dialog, '保存')
|
|
expect(api.createOverseasFee).toHaveBeenCalledWith(expect.objectContaining({ ...business, feeRemark: '补收本次额外操作费用', amount: 12.34, currency: 'CNY', businessDate: new Date().toLocaleDateString('sv-SE') }), expect.any(String))
|
|
expect(wrapper.emitted('changed')).toHaveLength(1)
|
|
})
|
|
it('checks refreshed eligibility and rejects a fee belonging to another order', async () => {
|
|
const wrapper = await mountPage()
|
|
vi.mocked(api.getOverseasFee).mockResolvedValueOnce({ ...fee, allowedActions: [] })
|
|
await click(wrapper, '审核', '.el-table__body-wrapper button')
|
|
expect(wrapper.findAllComponents(ElDialog).some(dialog => dialog.props('modelValue'))).toBe(false)
|
|
vi.mocked(api.getOverseasFee).mockResolvedValueOnce({ ...fee, businessId: 'different-order' })
|
|
await click(wrapper, '审核', '.el-table__body-wrapper button')
|
|
expect(wrapper.findAllComponents(ElDialog).some(dialog => dialog.props('modelValue'))).toBe(false)
|
|
expect(api.actOnOverseasFee).not.toHaveBeenCalled()
|
|
})
|
|
it('keeps supplier payables separate and reviews them for settlement without customer billing hints', async () => {
|
|
const payable: OverseasFee = { ...fee, businessType: 'LASTLEG_ORDER', direction: 'PAYABLE', supplierId: 'supplier-1', supplierName: '测试供应商', settlementMode: 'ON_ACCOUNT', collectionMode: 'BILL', walletStatus: 'NOT_REQUIRED' }
|
|
vi.mocked(api.listBusinessOverseasFees).mockResolvedValue({ records: [payable], total: 1, page: 1, size: 20, pages: 1 })
|
|
vi.mocked(api.getOverseasFee).mockResolvedValue(payable)
|
|
const wrapper = mount(BusinessOrderFees, { props: { business: { ...business, businessType: 'LASTLEG_ORDER' }, direction: 'PAYABLE' }, attachTo: document.body, global: { plugins: [ElementPlus, createPinia()] } })
|
|
await flushPromises()
|
|
expect(api.listBusinessOverseasFees).toHaveBeenLastCalledWith([expect.objectContaining({ businessType: 'LASTLEG_ORDER' })], { page: 1, size: 20 }, 'PAYABLE')
|
|
expect(wrapper.text()).toContain('测试供应商'); expect(wrapper.text()).toContain('推送结算')
|
|
expect(wrapper.findAll('button').map(button => button.text())).not.toContain('新增费用')
|
|
await click(wrapper, '审核', '.el-table__body-wrapper button')
|
|
const dialog = activeDialog(wrapper)
|
|
expect(dialog.text()).not.toContain('请核对原报价')
|
|
await click(dialog, '审核通过')
|
|
expect(api.actOnOverseasFee).toHaveBeenCalledWith(fee.sourceFeeId, 'APPROVE', expect.objectContaining({ decision: 'APPROVE' }))
|
|
})
|
|
it('cannot review the opposite direction returned by a refreshed fee', async () => {
|
|
const wrapper = await mountPage()
|
|
vi.mocked(api.getOverseasFee).mockResolvedValueOnce({ ...fee, direction: 'PAYABLE' })
|
|
await click(wrapper, '审核', '.el-table__body-wrapper button')
|
|
expect(wrapper.findAllComponents(AppDialog).some(dialog => dialog.props('modelValue'))).toBe(false)
|
|
expect(api.actOnOverseasFee).not.toHaveBeenCalled()
|
|
})
|
|
it('keeps read-only documents read-only and closes an editor when the order changes', async () => {
|
|
const wrapper = await mountPage()
|
|
await click(wrapper, '新增费用')
|
|
await wrapper.setProps({ business: { ...business, businessId: 'other' } })
|
|
await flushPromises()
|
|
expect(wrapper.getComponent(OverseasFeeEditor).props('modelValue')).toBe(false)
|
|
await wrapper.setProps({ readonly: true })
|
|
const visibleButtons = wrapper.findAll('button').filter(button => button.isVisible()).map(button => button.text())
|
|
expect(visibleButtons).not.toContain('新增费用')
|
|
expect(visibleButtons).not.toContain('审核')
|
|
expect(visibleButtons).not.toContain('驳回')
|
|
expect(visibleButtons).not.toContain('取消费用')
|
|
})
|
|
it('does not create against an order or customer changed after the editor was opened', async () => {
|
|
const wrapper = mount(OverseasFeeEditor, { props: { modelValue: false, fee: null, business, items: [{ feeCode: 'HANDLING', feeTypeName: '操作费', defaultPrice: 20 }] }, attachTo: document.body, global: { plugins: [ElementPlus, createPinia()] } })
|
|
await wrapper.setProps({ modelValue: true }); await flushPromises()
|
|
const dialog = activeDialog(wrapper)
|
|
dialog.findAllComponents({ name: 'ElSelect' })[0]!.vm.$emit('update:modelValue', 'HANDLING')
|
|
dialog.getComponent({ name: 'ElInputNumber' }).vm.$emit('update:modelValue', 10)
|
|
await wrapper.setProps({ business: { ...business, customerId: 'different-customer' } })
|
|
await flushPromises(); await click(dialog, '保存')
|
|
expect(api.createOverseasFee).not.toHaveBeenCalled()
|
|
})
|
|
it('blocks automatic storage rent from manual addition', async () => {
|
|
vi.mocked(api.listOverseasFeeItems).mockResolvedValue([{ feeCode: 'FEE_TYPE_077', feeTypeName: '仓储费', defaultPrice: null }])
|
|
const wrapper = await mountPage()
|
|
await click(wrapper, '新增费用')
|
|
const dialog = activeDialog(wrapper)
|
|
const select = dialog.findAllComponents({ name: 'ElSelect' })[0]!
|
|
expect(select.findAllComponents({ name: 'ElOption' })).toHaveLength(0)
|
|
select.vm.$emit('update:modelValue', 'FEE_TYPE_077')
|
|
dialog.getComponent({ name: 'ElInputNumber' }).vm.$emit('update:modelValue', 10)
|
|
await flushPromises(); await click(dialog, '保存')
|
|
expect(api.createOverseasFee).not.toHaveBeenCalled()
|
|
})
|
|
it('includes verified related orders but rejects another customer and keeps adjustment on the original order', async () => {
|
|
const related = { ...business, businessId: 'return-1', businessType: 'RETURN_ORDER' }
|
|
const relatedFee = { ...fee, ...related, allowedActions: ['ADJUST'] }
|
|
vi.mocked(api.listBusinessOverseasFees).mockResolvedValue({ records: [relatedFee], total: 1, page: 1, size: 20, pages: 1 })
|
|
vi.mocked(api.getOverseasFee).mockResolvedValue(relatedFee)
|
|
const wrapper = await mountPage()
|
|
await wrapper.setProps({ relatedBusinesses: [related, { ...business, customerId: 'other-customer' }] })
|
|
await flushPromises()
|
|
expect(api.listBusinessOverseasFees).toHaveBeenLastCalledWith([business, related], { page: 1, size: 20 }, 'RECEIVABLE')
|
|
await click(wrapper, '调整', '.el-table__body-wrapper button')
|
|
expect(wrapper.getComponent(OverseasFeeEditor).props('business')).toEqual(related)
|
|
const dialog = activeDialog(wrapper)
|
|
dialog.getComponent({ name: 'ElInputNumber' }).vm.$emit('update:modelValue', 15)
|
|
await flushPromises(); await click(dialog, '保存')
|
|
expect(api.actOnOverseasFee).toHaveBeenCalledWith(fee.sourceFeeId, 'ADJUST', expect.objectContaining({
|
|
expectedVersion: fee.sourceVersion, fee: expect.objectContaining({ businessId: related.businessId, businessType: 'RETURN_ORDER', amount: 15 }),
|
|
}))
|
|
})
|
|
it('shows a voided freeze-failure fixture as closed instead of pending review', async () => {
|
|
const closed = { ...fee, sourceStatus: 'VOIDED', reviewStatus: 'PENDING', walletStatus: 'FREEZE_FAILED', walletErrorMessage: '可用余额和授信额度不足', allowedActions: [] }
|
|
vi.mocked(api.listBusinessOverseasFees).mockResolvedValue({ records: [closed], total: 1, page: 1, size: 20, pages: 1 })
|
|
vi.mocked(api.getOverseasFee).mockResolvedValue(closed)
|
|
const wrapper = await mountPage()
|
|
expect(wrapper.text()).toContain('已作废')
|
|
expect(wrapper.findAll('.el-table__body-wrapper button').map(button => button.text().trim())).toEqual(['详情'])
|
|
await click(wrapper, '详情', '.el-table__body-wrapper button')
|
|
expect(activeDialog(wrapper).text()).toContain('冻结失败')
|
|
expect(activeDialog(wrapper).text()).toContain('此费用已作废,无需继续审核')
|
|
expect(activeDialog(wrapper).text()).not.toContain('待审核')
|
|
})
|
|
it('retries a failed debit only when the refreshed server action is allowed', async () => {
|
|
const failed = { ...fee, walletStatus: 'SETTLE_FAILED', allowedActions: ['RETRY_SETTLE'] }
|
|
vi.mocked(api.listBusinessOverseasFees).mockResolvedValue({ records: [failed], total: 1, page: 1, size: 20, pages: 1 })
|
|
vi.mocked(api.getOverseasFee).mockResolvedValue(failed)
|
|
const wrapper = await mountPage()
|
|
await click(wrapper, '重试扣费', '.el-table__body-wrapper button')
|
|
await click(activeDialog(wrapper), '重试扣费')
|
|
expect(api.actOnOverseasFee).toHaveBeenCalledWith(fee.sourceFeeId, 'RETRY_SETTLE', { idempotencyKey: expect.any(String) })
|
|
expect(wrapper.emitted('changed')).toHaveLength(1)
|
|
})
|
|
it('requires a rejection reason and submits the review contract', async () => {
|
|
const wrapper = await mountPage()
|
|
vi.mocked(api.getOverseasFee).mockResolvedValueOnce({ ...fee, sourceVersion: 5 })
|
|
await click(wrapper, '审核', '.el-table__body-wrapper button')
|
|
const dialog = activeDialog(wrapper)
|
|
const reject = dialog.findAll('button').find(button => button.text().trim() === '驳回')
|
|
expect(reject?.attributes('disabled')).toBeDefined()
|
|
expect(api.actOnOverseasFee).not.toHaveBeenCalled()
|
|
await dialog.get('textarea').setValue(' 金额不符 ')
|
|
await click(dialog, '驳回')
|
|
expect(api.actOnOverseasFee).toHaveBeenCalledWith(fee.sourceFeeId, 'REJECT', { decision: 'REJECT', reason: '金额不符', expectedVersion: 5, idempotencyKey: expect.any(String) })
|
|
})
|
|
it('keeps rejected fees read-only while allowing failed frozen funds to be released', async () => {
|
|
vi.mocked(api.listBusinessOverseasFees).mockResolvedValue({ records: [{ ...fee, reviewStatus: 'REJECTED', allowedActions: ['SUBMIT', 'RETRY_RELEASE', 'CANCEL'] }], total: 1, page: 1, size: 20, pages: 1 })
|
|
vi.mocked(api.getOverseasFee).mockResolvedValue({ ...fee, reviewStatus: 'REJECTED', allowedActions: ['SUBMIT', 'RETRY_RELEASE', 'CANCEL'] })
|
|
const wrapper = await mountPage()
|
|
const visibleButtons = wrapper.findAll('.el-table__body-wrapper button').map(button => button.text().trim())
|
|
expect(visibleButtons).toContain('详情')
|
|
expect(visibleButtons).not.toContain('审核')
|
|
expect(visibleButtons).not.toContain('驳回')
|
|
expect(visibleButtons).not.toContain('提交审核')
|
|
expect(visibleButtons).toContain('重试释放')
|
|
await click(wrapper, '重试释放', '.el-table__body-wrapper button')
|
|
await click(activeDialog(wrapper), '重试释放')
|
|
expect(api.actOnOverseasFee).toHaveBeenCalledWith(fee.sourceFeeId, 'RETRY_RELEASE', expect.objectContaining({ idempotencyKey: expect.any(String) }))
|
|
await click(wrapper, '详情', '.el-table__body-wrapper button')
|
|
const dialog = activeDialog(wrapper)
|
|
expect(dialog.text()).toContain('审核状态')
|
|
expect(dialog.findAll('button').some(button => button.text().trim() === '审核')).toBe(false)
|
|
})
|
|
it('voids with the freshly loaded version and a required reason', async () => {
|
|
vi.mocked(api.listBusinessOverseasFees).mockResolvedValue({ records: [{ ...fee, allowedActions: ['VOID'] }], total: 1, page: 1, size: 20, pages: 1 })
|
|
vi.mocked(api.getOverseasFee).mockResolvedValue({ ...fee, sourceVersion: 5, allowedActions: ['VOID'] })
|
|
const wrapper = await mountPage()
|
|
await click(wrapper, '作废', '.el-table__body-wrapper button')
|
|
const dialog = activeDialog(wrapper)
|
|
await click(dialog, '作废')
|
|
expect(api.actOnOverseasFee).not.toHaveBeenCalled()
|
|
await dialog.get('textarea').setValue(' 重复费用 ')
|
|
await click(dialog, '作废')
|
|
expect(api.actOnOverseasFee).toHaveBeenCalledWith(fee.sourceFeeId, 'VOID', { expectedVersion: 5, reason: '重复费用', idempotencyKey: expect.any(String) })
|
|
})
|
|
it('locks in-flight approvals and preserves diagnostics and idempotency when retried', async () => {
|
|
const pending = deferred<OverseasFee>()
|
|
vi.mocked(api.actOnOverseasFee).mockReturnValueOnce(pending.promise)
|
|
const wrapper = await mountPage()
|
|
await click(wrapper, '审核', '.el-table__body-wrapper button')
|
|
const dialog = activeDialog(wrapper)
|
|
await click(dialog, '审核通过')
|
|
expect(wrapper.findAllComponents(ElDialog).find(component => component.props('modelValue'))!.props()).toMatchObject({ showClose: false, closeOnPressEscape: false })
|
|
expect(dialog.get('textarea').attributes('disabled')).toBeDefined()
|
|
pending.reject(new ApiResponseError('余额服务暂不可用', 'FUNDING_ERROR', 409, null, 'test-request-1'))
|
|
await flushPromises()
|
|
expect(wrapper.findAllComponents(AppDialog).some(component => component.props('modelValue'))).toBe(true)
|
|
expect(dialog.text()).toContain('余额服务暂不可用')
|
|
expect((dialog.get('textarea').element as HTMLTextAreaElement).value).toBe('')
|
|
const firstKey = vi.mocked(api.actOnOverseasFee).mock.calls[0]![2].idempotencyKey
|
|
await click(dialog, '审核通过')
|
|
expect(vi.mocked(api.actOnOverseasFee).mock.calls[1]![2].idempotencyKey).toBe(firstKey)
|
|
})
|
|
})
|
|
|
|
|
|
describe('仓租费用审核与调价', () => {
|
|
const rent: OverseasFee = { ...fee, feeItemCode: 'FEE_TYPE_077', feeItemName: '仓储费', allowedActions: ['APPROVE', 'REJECT', 'ADJUST_RENT'],
|
|
storageRent: { outboundNo: 'OUT-1', outboundAt: '2026-10-22T10:00:00Z', calculatedAmount: 10, chargedAmount: null,
|
|
lines: [
|
|
{ lotId: '1', inboundAt: '2026-10-01T10:00:00Z', quantity: 2, days: 21, calculatedAmount: 5, sourceBusinessType: 'LASTLEG_ORDER', sourceBusinessNo: 'WC-SOURCE-1', batchNo: 'B-SOURCE-1' },
|
|
{ lotId: '2', inboundAt: '2026-10-01T10:00:00Z', quantity: 2, days: 21, calculatedAmount: 5, sourceBusinessType: 'RETURN_ORDER', sourceBusinessNo: 'TH-SOURCE-2', batchNo: 'B-SOURCE-2' },
|
|
] } }
|
|
async function setupRent() {
|
|
vi.mocked(api.listBusinessOverseasFees).mockResolvedValue({ records: [rent], total: 1, page: 1, size: 20, pages: 1 })
|
|
vi.mocked(api.getOverseasFee).mockResolvedValue(rent)
|
|
return mountPage()
|
|
}
|
|
it('审核显示实际出入库与原计算额,批准以前不请求扣款', async () => {
|
|
const wrapper = await setupRent()
|
|
await click(wrapper, '审核', '.el-table__body-wrapper button')
|
|
const dialog = activeDialog(wrapper)
|
|
expect(dialog.text()).toContain('入库时间'); expect(dialog.text()).toContain('出库时间')
|
|
expect(dialog.text()).toContain('原计算金额'); expect(dialog.text()).toContain('实扣金额')
|
|
expect(dialog.text()).toContain('来源单号'); expect(dialog.text()).toContain('头程尾程订单')
|
|
expect(dialog.text()).toContain('WC-SOURCE-1'); expect(dialog.text()).toContain('TH-SOURCE-2')
|
|
expect(dialog.text()).toContain('退货单'); expect(dialog.text()).toContain('B-SOURCE-2')
|
|
expect(dialog.text()).toContain('2026-10-01'); expect(api.actOnOverseasFee).not.toHaveBeenCalled()
|
|
await click(dialog, '审核通过')
|
|
expect(api.actOnOverseasFee).toHaveBeenCalledWith(rent.sourceFeeId, 'APPROVE', expect.objectContaining({ decision: 'APPROVE' }))
|
|
})
|
|
it('调价说明必填,失败保留金额说明且同参数重试复用请求编号', async () => {
|
|
const wrapper = await setupRent()
|
|
vi.mocked(adjustStorageRentFee).mockRejectedValueOnce(new Error('暂时不可用')).mockResolvedValue(rent)
|
|
await click(wrapper, '调整仓租', '.el-table__body-wrapper button')
|
|
const dialog = activeDialog(wrapper)
|
|
expect(dialog.getComponent({ name: 'ElInputNumber' }).props('modelValue')).toBe(10)
|
|
expect(dialog.findAll('button').find(button => button.text() === '保存调整')!.attributes('disabled')).toBeDefined()
|
|
dialog.getComponent({ name: 'ElInputNumber' }).vm.$emit('update:modelValue', 12)
|
|
dialog.findAllComponents({ name: 'ElInput' }).find(input => input.props('type') === 'textarea')!.vm.$emit('update:modelValue', '人工核对调整')
|
|
await flushPromises(); await click(dialog, '保存调整')
|
|
expect(dialog.text()).toContain('仓租操作失败')
|
|
await click(dialog, '保存调整')
|
|
const calls = vi.mocked(adjustStorageRentFee).mock.calls
|
|
expect(calls).toHaveLength(2); expect(calls[0]).toEqual(calls[1])
|
|
expect(calls[0]).toEqual([rent.sourceFeeId, expect.objectContaining({ amount: 12, reason: '人工核对调整', expectedVersion: 3 })])
|
|
})
|
|
it('只读单据在仓租弹框已打开时同样禁止修改', async () => {
|
|
const wrapper = await setupRent(); await click(wrapper, '调整仓租', '.el-table__body-wrapper button')
|
|
await wrapper.setProps({ readonly: true }); await flushPromises()
|
|
expect(activeDialog(wrapper).findAll('button').some(button => button.text() === '保存调整')).toBe(false)
|
|
expect(adjustStorageRentFee).not.toHaveBeenCalled()
|
|
})
|
|
})
|